-
-
Notifications
You must be signed in to change notification settings - Fork 656
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Accessibility] Introduce activator node refs and automatic focus man…
…agement (#748) * Introduce activator node ref for `useDraggable` and `useSortable` Introducing the concept of activator node refs for `useDraggable` allows @dnd-kit to handle common use-cases such as restoring focus on the activator node after dragging via the keyboard or only allowing the activator node to instantiate the keyboard sensor. Consumers of `useDraggable` and `useSortable` may now optionally set the activator node ref on the element that receives listeners It's common for the activator element (the element that receives the sensor listeners) to differ from the draggable node. When this happens, @dnd-kit has no reliable way to get a reference to the activator node after dragging ends, as the original `event.target` that instantiated the sensor may no longer be mounted in the DOM or associated with the draggable node that was previously active. * Automatically restore focus on the first focusable node Focus management is now automatically handled by @dnd-kit. When the activator event is a Keyboard event, @dnd-kit will now attempt to automatically restore focus back to the first focusable node of the activator node or draggable node. If no activator node is specified via the setActivatorNodeRef setter function of useDraggble and useSortable, @dnd-kit will automatically restore focus on the first focusable node of the draggable node set via the setNodeRef setter function of useDraggable and useSortable. If you were previously managing focus manually and would like to opt-out of automatic focus management, use the newly introduced restoreFocus property of the accessibility prop of <DndContext>:
- Loading branch information
Showing
26 changed files
with
288 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
--- | ||
'@dnd-kit/core': minor | ||
'@dnd-kit/sortable': minor | ||
--- | ||
|
||
#### Introducing activator node refs | ||
|
||
Introducing the concept of activator node refs for `useDraggable` and `useSortable`. This allows @dnd-kit to handle common use-cases such as restoring focus on the activator node after dragging via the keyboard or only allowing the activator node to instantiate the keyboard sensor. | ||
|
||
Consumers of `useDraggable` and `useSortable` may now optionally set the activator node ref on the element that receives listeners: | ||
|
||
```diff | ||
import {useDraggable} from '@dnd-kit/core'; | ||
|
||
function Draggable(props) { | ||
const { | ||
listeners, | ||
setNodeRef, | ||
+ setActivatorNodeRef, | ||
} = useDraggable({id: props.id}); | ||
|
||
return ( | ||
<div ref={setNodeRef}> | ||
Draggable element | ||
<button | ||
{...listeners} | ||
+ ref={setActivatorNodeRef} | ||
> | ||
:: Drag Handle | ||
</button> | ||
</div> | ||
) | ||
} | ||
``` | ||
|
||
It's common for the activator element (the element that receives the sensor listeners) to differ from the draggable node. When this happens, @dnd-kit has no reliable way to get a reference to the activator node after dragging ends, as the original `event.target` that instantiated the sensor may no longer be mounted in the DOM or associated with the draggable node that was previously active. | ||
|
||
#### Automatically restoring focus | ||
|
||
Focus management is now automatically handled by @dnd-kit. When the activator event is a Keyboard event, @dnd-kit will now attempt to automatically restore focus back to the first focusable node of the activator node or draggable node. | ||
|
||
If no activator node is specified via the `setActivatorNodeRef` setter function of `useDraggble` and `useSortable`, @dnd-kit will automatically restore focus on the first focusable node of the draggable node set via the `setNodeRef` setter function of `useDraggable` and `useSortable`. | ||
|
||
If you were previously managing focus manually and would like to opt-out of automatic focus management, use the newly introduced `restoreFocus` property of the `accessibility` prop of `<DndContext>`: | ||
|
||
```diff | ||
<DndContext | ||
accessibility={{ | ||
+ restoreFocus: false | ||
}} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@dnd-kit/utilities': minor | ||
--- | ||
|
||
Introduced the `findFirstFocusableNode` utility function that returns the first focusable node within a given HTMLElement, or the element itself if it is focusable. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
packages/core/src/components/Accessibility/components/RestoreFocus.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import {useContext, useEffect} from 'react'; | ||
import { | ||
findFirstFocusableNode, | ||
isKeyboardEvent, | ||
usePrevious, | ||
} from '@dnd-kit/utilities'; | ||
|
||
import {InternalContext} from '../../../store'; | ||
|
||
interface Props { | ||
disabled: boolean; | ||
} | ||
|
||
export function RestoreFocus({disabled}: Props) { | ||
const {active, activatorEvent, draggableNodes} = useContext(InternalContext); | ||
const previousActivatorEvent = usePrevious(activatorEvent); | ||
const previousActiveId = usePrevious(active?.id); | ||
|
||
// Restore keyboard focus on the activator node | ||
useEffect(() => { | ||
if (disabled) { | ||
return; | ||
} | ||
|
||
if (!activatorEvent && previousActivatorEvent && previousActiveId != null) { | ||
if (!isKeyboardEvent(previousActivatorEvent)) { | ||
return; | ||
} | ||
|
||
if (document.activeElement === previousActivatorEvent.target) { | ||
// No need to restore focus | ||
return; | ||
} | ||
|
||
const draggableNode = draggableNodes[previousActiveId]; | ||
|
||
if (!draggableNode) { | ||
return; | ||
} | ||
|
||
const {activatorNode, node} = draggableNode; | ||
|
||
if (!activatorNode.current && !node.current) { | ||
return; | ||
} | ||
|
||
requestAnimationFrame(() => { | ||
for (const element of [activatorNode.current, node.current]) { | ||
if (!element) { | ||
continue; | ||
} | ||
|
||
const focusableNode = findFirstFocusableNode(element); | ||
|
||
if (focusableNode) { | ||
focusableNode.focus(); | ||
break; | ||
} | ||
} | ||
}); | ||
} | ||
}, [ | ||
activatorEvent, | ||
disabled, | ||
draggableNodes, | ||
previousActiveId, | ||
previousActivatorEvent, | ||
]); | ||
|
||
return null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export {RestoreFocus} from './RestoreFocus'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export {useMeasuringConfiguration} from './useMeasuringConfiguration'; | ||
export {useLayoutShiftScrollCompensation} from './useLayoutShiftScrollCompensation'; |
37 changes: 6 additions & 31 deletions
37
...s/core/src/components/DndContext/hooks.ts → ...hooks/useLayoutShiftScrollCompensation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
packages/core/src/components/DndContext/hooks/useMeasuringConfiguration.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import {useMemo} from 'react'; | ||
import type {DeepRequired} from '@dnd-kit/utilities'; | ||
|
||
import {defaultMeasuringConfiguration} from '../defaults'; | ||
import type {MeasuringConfiguration} from '../types'; | ||
|
||
export function useMeasuringConfiguration( | ||
config: MeasuringConfiguration | undefined | ||
): DeepRequired<MeasuringConfiguration> { | ||
return useMemo( | ||
() => ({ | ||
draggable: { | ||
...defaultMeasuringConfiguration.draggable, | ||
...config?.draggable, | ||
}, | ||
droppable: { | ||
...defaultMeasuringConfiguration.droppable, | ||
...config?.droppable, | ||
}, | ||
dragOverlay: { | ||
...defaultMeasuringConfiguration.dragOverlay, | ||
...config?.dragOverlay, | ||
}, | ||
}), | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[config?.draggable, config?.droppable, config?.dragOverlay] | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.