Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.

Fix first focus targets for modals/dialogs

In modals and dialogs we'll generally only move focus to
the first target, if said target is marked with `[autofocus]`
otherwise we'll focus the dialog/modal element itself and
let the regular tab order kick in.

See: https://github.com/KittyGiraudel/a11y-dialog/blob/b7da40433682ee01798aa601c41c3ae8869e6719/a11y-dialog.js#L310-L314

Changed files
+27 -11
src
+4 -2
src/__tests__/useModalFocus.test.tsx
···
</main>
);
-
// starts out with first element available
-
cy.focused().contains('Focus 1');
+
// starts out with first modal element available
+
cy.focused().should('have.attr', 'aria-modal', 'true')
// cycles through the modal's focusable targets only
+
cy.realPress('Tab');
+
cy.focused().contains('Focus 1');
cy.realPress('Tab');
cy.focused().contains('Focus 2');
cy.realPress('Tab');
+10 -6
src/useModalFocus.ts
···
snapshotSelection,
restoreSelection,
} from './utils/selection';
-
import { getFirstFocusTarget, getFocusTargets } from './utils/focus';
+
+
import {
+
getAutofocusTarget,
+
getFirstFocusTarget,
+
getFocusTargets,
+
} from './utils/focus';
+
import { useLayoutEffect } from './utils/react';
import { contains } from './utils/element';
import { makePriorityHook } from './usePriority';
···
!document.activeElement ||
!ref.current.contains(document.activeElement)
) {
-
const newTarget = getFirstFocusTarget(ref.current);
-
if (newTarget) {
-
selection = snapshotSelection(ref.current);
-
newTarget.focus();
-
}
+
const newTarget = getAutofocusTarget(ref.current);
+
selection = snapshotSelection(ref.current);
+
newTarget.focus();
}
function onBlur(event: FocusEvent) {
+13 -3
src/utils/focus.ts
···
};
/** Returns the first focus target that should be focused automatically. */
-
export const getFirstFocusTarget = (node: HTMLElement): HTMLElement | null => {
-
const targets = getFocusTargets(node);
-
return targets.find(x => x.matches('[autofocus]')) || targets[0] || null;
+
export const getFirstFocusTarget = (node: HTMLElement): HTMLElement | null =>
+
getFocusTargets(node)[0] || null;
+
+
/** Returns the first focus target that should be focused automatically in a modal/dialog. */
+
export const getAutofocusTarget = (node: HTMLElement): HTMLElement => {
+
const elements = node.querySelectorAll(focusableSelectors);
+
for (let i = 0, l = elements.length; i < l; i++) {
+
const element = elements[i] as HTMLElement;
+
if (isVisible(element) && element.matches('[autofocus]')) return element;
+
}
+
+
node.setAttribute('tabindex', '-1');
+
return node;
};
/** Returns the next (optionally in reverse) focus target given a target node. */