···11-export { useFocusLoop } from './useFocusLoop';
11+export * from './useModalFocus';
22+export * from './useDialogFocus';
33+export * from './useMenuFocus';
+139
src/useDialogFocus.ts
···11+import { snapshotSelection, restoreSelection } from './utils/selection';
22+import { getFocusTargets, getNextFocusTarget } from './utils/focus';
33+import { useLayoutEffect } from './utils/react';
44+import { contains, isInputElement } from './utils/element';
55+import { makePriorityHook } from './usePriority';
66+import { Ref } from './types';
77+88+const usePriority = makePriorityHook();
99+1010+export interface DialogFocusOptions {
1111+ disabled?: boolean;
1212+ ownerRef?: Ref<HTMLElement>;
1313+}
1414+1515+export function useDialogFocus<T extends HTMLElement>(
1616+ ref: Ref<T>,
1717+ options?: DialogFocusOptions
1818+) {
1919+ const ownerRef = options && options.ownerRef;
2020+ const disabled = !!(options && options.disabled);
2121+ const hasPriority = usePriority(ref, disabled);
2222+2323+ useLayoutEffect(() => {
2424+ if (!ref.current || disabled || !hasPriority) return;
2525+2626+ let selection = snapshotSelection(ownerRef && ownerRef.current);
2727+ let willReceiveFocus = false;
2828+ let focusMovesForward = true;
2929+3030+ function onClick(event: MouseEvent) {
3131+ if (!ref.current || event.defaultPrevented) return;
3232+3333+ const target = event.target as HTMLElement | null;
3434+ if (target && getFocusTargets(ref.current).indexOf(target) > -1) {
3535+ selection = null;
3636+ willReceiveFocus = true;
3737+ }
3838+ }
3939+4040+ function onFocus(event: FocusEvent) {
4141+ if (!ref.current || event.defaultPrevented) return;
4242+4343+ const active = document.activeElement as HTMLElement;
4444+ const owner = (ownerRef && ownerRef.current) || selection && selection.element;
4545+4646+ if (willReceiveFocus || (owner && event.target === owner)) {
4747+ if (!contains(ref.current, active)) selection = snapshotSelection(owner);
4848+ willReceiveFocus = false;
4949+ return;
5050+ }
5151+5252+ const { relatedTarget, target } = event;
5353+ // Check whether focus is about to move into the container and prevent it
5454+ if (contains(ref.current, target) && !contains(ref.current, relatedTarget)) {
5555+ // Get the next focus target of the container
5656+ const focusTarget = getNextFocusTarget(ref.current, !focusMovesForward);
5757+ if (focusTarget) {
5858+ focusMovesForward = true;
5959+ event.preventDefault();
6060+ focusTarget.focus();
6161+ }
6262+ }
6363+ }
6464+6565+ function onKey(event: KeyboardEvent) {
6666+ if (!ref.current || event.defaultPrevented) return;
6767+6868+ // Mark whether focus is moving forward for the `onFocus` handler
6969+ if (event.code === 'Tab') {
7070+ focusMovesForward = !event.shiftKey;
7171+ }
7272+7373+ const active = document.activeElement as HTMLElement;
7474+ const owner = (ownerRef && ownerRef.current) || selection && selection.element;
7575+ const focusTargets = getFocusTargets(ref.current);
7676+7777+ if (
7878+ !focusTargets.length ||
7979+ (!contains(owner, active) && !contains(ref.current, active))
8080+ ) {
8181+ // Do nothing if no targets are available or the listbox or owner don't have focus
8282+ return;
8383+ } else if (event.code === 'Tab') {
8484+ // Skip over the listbox via the parent if we press tab
8585+ const currentTarget = contains(owner, active) ? owner! : ref.current;
8686+ const focusTarget = getNextFocusTarget(currentTarget, event.shiftKey);
8787+ if (focusTarget) {
8888+ event.preventDefault();
8989+ focusTarget.focus();
9090+ }
9191+ } else if (
9292+ (!isInputElement(active) && event.code === 'ArrowRight') ||
9393+ event.code === 'ArrowDown'
9494+ ) {
9595+ // Implement forward movement in focus targets
9696+ event.preventDefault();
9797+ const focusIndex = focusTargets.indexOf(active);
9898+ const nextIndex = focusIndex < focusTargets.length - 1 ? focusIndex + 1 : 0;
9999+ willReceiveFocus = true;
100100+ focusTargets[nextIndex].focus();
101101+ } else if (
102102+ (!isInputElement(active) && event.code === 'ArrowLeft') ||
103103+ event.code === 'ArrowUp'
104104+ ) {
105105+ // Implement backward movement in focus targets
106106+ event.preventDefault();
107107+ const focusIndex = focusTargets.indexOf(active);
108108+ const nextIndex = focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1;
109109+ willReceiveFocus = true;
110110+ focusTargets[nextIndex].focus();
111111+ } else if (selection && event.code === 'Escape') {
112112+ // Restore selection if escape is pressed
113113+ event.preventDefault();
114114+ willReceiveFocus = false;
115115+ restoreSelection(selection);
116116+ } else if (
117117+ owner &&
118118+ active !== owner &&
119119+ isInputElement(owner) &&
120120+ /^(?:Key|Digit)/.test(event.code)
121121+ ) {
122122+ // Restore selection if a key is pressed on input
123123+ event.preventDefault();
124124+ willReceiveFocus = false;
125125+ restoreSelection(selection);
126126+ }
127127+ }
128128+129129+ ref.current.addEventListener('mousedown', onClick, true);
130130+ document.body.addEventListener('focusin', onFocus);
131131+ document.addEventListener('keydown', onKey);
132132+133133+ return () => {
134134+ ref.current!.removeEventListener('mousedown', onClick);
135135+ document.body.removeEventListener('focusin', onFocus);
136136+ document.removeEventListener('keydown', onKey);
137137+ };
138138+ }, [ref, hasPriority, disabled]);
139139+}
-53
src/useFocusLoop.ts
···11-import { useLayoutEffect } from 'react';
22-import { getFirstFocusTarget, getFocusTargets } from './utils/focus';
33-import { contains } from './utils/element';
44-import { Ref } from './types';
55-66-export function useFocusLoop<T extends HTMLElement>(ref: Ref<T>) {
77- useLayoutEffect(() => {
88- if (!ref.current) return;
99-1010- let active = document.activeElement as HTMLElement | null;
1111- if (!active || !ref.current.contains(active)) {
1212- active = getFirstFocusTarget(ref.current);
1313- if (active) active.focus();
1414- }
1515-1616- function onBlur(event: FocusEvent) {
1717- const parent = ref.current;
1818- if (!parent || event.defaultPrevented) return;
1919-2020- if (contains(parent, event.target) && !contains(parent, event.relatedTarget)) {
2121- const target = getFirstFocusTarget(parent);
2222- if (target) target.focus();
2323- }
2424- }
2525-2626- function onKeyDown(event: KeyboardEvent) {
2727- const parent = ref.current;
2828- if (!parent || event.defaultPrevented) return;
2929-3030- if (event.code === 'Tab') {
3131- const activeElement = document.activeElement as HTMLElement;
3232- const targets = getFocusTargets(parent);
3333- const index = targets.indexOf(activeElement);
3434- if (event.shiftKey && index === 0) {
3535- event.preventDefault();
3636- targets[targets.length - 1].focus();
3737- } else if (!event.shiftKey && index === targets.length - 1) {
3838- event.preventDefault();
3939- targets[0].focus();
4040- }
4141-4242- }
4343- }
4444-4545- document.body.addEventListener('focusout', onBlur);
4646- document.addEventListener('keydown', onKeyDown);
4747-4848- return () => {
4949- document.body.removeEventListener('focusout', onBlur);
5050- document.removeEventListener('keydown', onKeyDown);
5151- };
5252- }, [ref]);
5353-}
+99
src/useMenuFocus.ts
···11+import { RestoreSelection, snapshotSelection, restoreSelection } from './utils/selection';
22+import { getFocusTargets } from './utils/focus';
33+import { useLayoutEffect } from './utils/react';
44+import { contains, isInputElement } from './utils/element';
55+import { Ref } from './types';
66+77+export interface MenuFocusOptions {
88+ disabled?: boolean;
99+ ownerRef?: Ref<HTMLElement>;
1010+}
1111+1212+export function useMenuFocus<T extends HTMLElement>(ref: Ref<T>, options?: MenuFocusOptions) {
1313+ const ownerRef = options && options.ownerRef;
1414+ const disabled = !!(options && options.disabled);
1515+1616+ useLayoutEffect(() => {
1717+ if (!ref.current || disabled) return;
1818+1919+ let selection: RestoreSelection | null = null;
2020+2121+ function onFocus(event: FocusEvent) {
2222+ if (!ref.current || event.defaultPrevented) return;
2323+2424+ const owner = (ownerRef && ownerRef.current) || selection && selection.element;
2525+ const { relatedTarget, target } = event;
2626+ if (relatedTarget === owner) {
2727+ // When owner is explicitly passed we can make a snapshot early
2828+ selection = snapshotSelection(owner);
2929+ } else if (contains(ref.current, target) && !contains(ref.current, relatedTarget)) {
3030+ // Check whether focus is about to move into the container and snapshot last focus
3131+ selection = snapshotSelection(owner);
3232+ } else if (contains(ref.current, relatedTarget) && !contains(ref.current, target)) {
3333+ // Reset focus if it's lost and has left the menu
3434+ selection = null;
3535+ }
3636+ }
3737+3838+ function onKey(event: KeyboardEvent) {
3939+ if (!ref.current || event.defaultPrevented) return;
4040+4141+ const owner = (ownerRef && ownerRef.current) || selection && selection.element;
4242+ const active = document.activeElement as HTMLElement;
4343+ const focusTargets = getFocusTargets(ref.current);
4444+ if (!focusTargets.length || !contains(ref.current, active) || !contains(owner, active)) {
4545+ // Do nothing if container doesn't contain focus or not targets are available
4646+ return;
4747+ }
4848+4949+ if (
5050+ (!isInputElement(active) && event.code === 'ArrowRight') ||
5151+ event.code === 'ArrowDown'
5252+ ) {
5353+ // Implement forward movement in focus targets
5454+ event.preventDefault();
5555+ const focusIndex = focusTargets.indexOf(active);
5656+ const nextIndex = focusIndex < focusTargets.length - 1 ? focusIndex + 1 : 0;
5757+ focusTargets[nextIndex].focus();
5858+ } else if (
5959+ (!isInputElement(active) && event.code === 'ArrowLeft') ||
6060+ event.code === 'ArrowUp'
6161+ ) {
6262+ // Implement backward movement in focus targets
6363+ event.preventDefault();
6464+ const focusIndex = focusTargets.indexOf(active);
6565+ const nextIndex = focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1;
6666+ focusTargets[nextIndex].focus();
6767+ } else if (event.code === 'Home') {
6868+ // Implement Home => first item
6969+ event.preventDefault();
7070+ focusTargets[0].focus();
7171+ } else if (event.code === 'End') {
7272+ // Implement End => last item
7373+ event.preventDefault();
7474+ focusTargets[focusTargets.length - 1].focus();
7575+ } else if (owner && active !== owner && event.code === 'Escape') {
7676+ // Restore selection if escape is pressed
7777+ event.preventDefault();
7878+ restoreSelection(selection);
7979+ } else if (
8080+ owner &&
8181+ active !== owner &&
8282+ isInputElement(owner) &&
8383+ /^(?:Key|Digit)/.test(event.code)
8484+ ) {
8585+ // Restore selection if a key is pressed on input
8686+ event.preventDefault();
8787+ restoreSelection(selection);
8888+ }
8989+ }
9090+9191+ document.body.addEventListener('focusin', onFocus);
9292+ document.addEventListener('keydown', onKey);
9393+9494+ return () => {
9595+ document.body.removeEventListener('focusin', onFocus);
9696+ document.removeEventListener('keydown', onKey);
9797+ };
9898+ }, [ref, disabled]);
9999+}