react-beautiful-dnd-next
Version:
Beautiful and accessible drag and drop for lists with React
109 lines (89 loc) • 3.06 kB
JavaScript
// @flow
import invariant from 'tiny-invariant';
import { useRef } from 'react';
import { useCallback } from 'use-memo-one';
import type { Args } from './drag-handle-types';
import usePrevious from '../use-previous-ref';
import focusRetainer from './util/focus-retainer';
import getDragHandleRef from './util/get-drag-handle-ref';
import useLayoutEffect from '../use-isomorphic-layout-effect';
export type Result = {|
onBlur: () => void,
onFocus: () => void,
|};
function noop() {}
export default function useFocusRetainer(args: Args): Result {
const isFocusedRef = useRef<boolean>(false);
const lastArgsRef = usePrevious<Args>(args);
const { getDraggableRef } = args;
const onFocus = useCallback(() => {
isFocusedRef.current = true;
}, []);
const onBlur = useCallback(() => {
isFocusedRef.current = false;
}, []);
// This effect handles:
// - giving focus on mount
// - registering focus on unmount
useLayoutEffect(() => {
// mounting: try to restore focus
const first: Args = lastArgsRef.current;
if (!first.isEnabled) {
return noop;
}
const draggable: ?HTMLElement = getDraggableRef();
invariant(draggable, 'Drag handle could not obtain draggable ref');
const dragHandle: HTMLElement = getDragHandleRef(draggable);
focusRetainer.tryRestoreFocus(first.draggableId, dragHandle);
// unmounting: try to retain focus
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const last: Args = lastArgsRef.current;
const shouldRetainFocus = ((): boolean => {
// will not restore if not enabled
if (!last.isEnabled) {
return false;
}
// not focused
if (!isFocusedRef.current) {
return false;
}
// a drag is finishing
return last.isDragging || last.isDropAnimating;
})();
if (shouldRetainFocus) {
focusRetainer.retain(last.draggableId);
}
};
}, [getDraggableRef, lastArgsRef]);
// will always be null on the first render as nothing has mounted yet
const lastDraggableRef = useRef<?HTMLElement>(null);
// This effect restores focus to an element when a
// ref changes while a component is still mounted.
// This can happen when a drag handle is moved into a portal
useLayoutEffect(() => {
// this can happen on the first mount - no draggable ref is set
// this effect is not handling initial mounting
if (!lastDraggableRef.current) {
return;
}
const draggableRef: ?HTMLElement = getDraggableRef();
// Cannot focus on nothing
if (!draggableRef) {
return;
}
// no change in ref
if (draggableRef === lastDraggableRef.current) {
return;
}
// ref has changed - let's do this
if (isFocusedRef.current && lastArgsRef.current.isEnabled) {
getDragHandleRef(draggableRef).focus();
}
// Doing our own should run check
});
useLayoutEffect(() => {
lastDraggableRef.current = getDraggableRef();
});
return { onBlur, onFocus };
}