@hello-pangea/dnd
Version:
Beautiful and accessible drag and drop for lists with React
130 lines (110 loc) • 3.52 kB
text/typescript
import { useRef } from 'react';
import { useMemo, useCallback } from 'use-memo-one';
import type { DraggableId, ContextId } from '../../types';
import type { FocusMarshal, Unregister } from './focus-marshal-types';
import { dragHandle as dragHandleAttr } from '../data-attributes';
import useLayoutEffect from '../use-isomorphic-layout-effect';
import findDragHandle from '../get-elements/find-drag-handle';
interface Entry {
id: DraggableId;
focus: () => void;
}
type EntryMap = {
[id in DraggableId]: Entry;
};
export default function useFocusMarshal(contextId: ContextId): FocusMarshal {
const entriesRef = useRef<EntryMap>({});
const recordRef = useRef<DraggableId | null>(null);
const restoreFocusFrameRef = useRef<AnimationFrameID | null>(null);
const isMountedRef = useRef<boolean>(false);
const register = useCallback(function register(
id: DraggableId,
focus: () => void,
): Unregister {
const entry: Entry = { id, focus };
entriesRef.current[id] = entry;
return function unregister() {
const entries: EntryMap = entriesRef.current;
const current: Entry = entries[id];
// entry might have been overrided by another registration
if (current !== entry) {
delete entries[id];
}
};
}, []);
const tryGiveFocus = useCallback(
function tryGiveFocus(tryGiveFocusTo: DraggableId) {
const handle: HTMLElement | null = findDragHandle(
contextId,
tryGiveFocusTo,
);
if (handle && handle !== document.activeElement) {
handle.focus();
}
},
[contextId],
);
const tryShiftRecord = useCallback(function tryShiftRecord(
previous: DraggableId,
redirectTo: DraggableId,
) {
if (recordRef.current === previous) {
recordRef.current = redirectTo;
}
}, []);
const tryRestoreFocusRecorded = useCallback(
function tryRestoreFocusRecorded() {
// frame already queued
if (restoreFocusFrameRef.current) {
return;
}
// cannot give focus if unmounted
// this code path is generally not hit expect for some hot-reloading flows
if (!isMountedRef.current) {
return;
}
restoreFocusFrameRef.current = requestAnimationFrame(() => {
restoreFocusFrameRef.current = null;
const record: DraggableId | null = recordRef.current;
if (record) {
tryGiveFocus(record);
}
});
},
[tryGiveFocus],
);
const tryRecordFocus = useCallback(function tryRecordFocus(id: DraggableId) {
// clear any existing record
recordRef.current = null;
const focused: Element | null = document.activeElement;
// no item focused so it cannot be our item
if (!focused) {
return;
}
// focused element is not a drag handle or does not have the right id
if (focused.getAttribute(dragHandleAttr.draggableId) !== id) {
return;
}
recordRef.current = id;
}, []);
useLayoutEffect(() => {
isMountedRef.current = true;
return function clearFrameOnUnmount() {
isMountedRef.current = false;
const frameId: AnimationFrameID | null = restoreFocusFrameRef.current;
if (frameId) {
cancelAnimationFrame(frameId);
}
};
}, []);
const marshal: FocusMarshal = useMemo(
() => ({
register,
tryRecordFocus,
tryRestoreFocusRecorded,
tryShiftRecord,
}),
[register, tryRecordFocus, tryRestoreFocusRecorded, tryShiftRecord],
);
return marshal;
}