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

Add initial useDialogFocus tests

+166
src/__tests__/useDialogFocus.test.tsx
···
···
+
import React, { useState, useRef } from 'react';
+
import { mount } from '@cypress/react';
+
+
import { useDialogFocus } from '../useDialogFocus';
+
+
it('allows dialogs to be navigated without an owner', () => {
+
const Dialog = () => {
+
const ref = useRef<HTMLUListElement>(null);
+
useDialogFocus(ref);
+
return (
+
<ul ref={ref} role="dialog">
+
<li tabIndex={0}>#1</li>
+
<li tabIndex={0}>#2</li>
+
<li tabIndex={0}>#3</li>
+
</ul>
+
);
+
};
+
+
const App = () => {
+
const [hasDialog, setDialog] = useState(false);
+
return (
+
<main>
+
<input type="text" name="text" onFocus={() => setDialog(true)} />
+
{hasDialog && <Dialog />}
+
</main>
+
);
+
};
+
+
mount(<App />);
+
+
cy.get('input').first().as('input').focus();
+
cy.focused().should('have.property.name', 'text');
+
+
// ArrowRight/ArrowLeft shouldn't affect the selection for inputs
+
cy.realPress('ArrowRight');
+
cy.realPress('ArrowLeft');
+
cy.get('@input').should('have.focus');
+
+
// Navigation with arrow keys is normal otherwise
+
cy.realPress('ArrowDown');
+
cy.realPress('ArrowDown');
+
cy.focused().contains('#2');
+
cy.realPress('ArrowRight');
+
cy.focused().contains('#3');
+
cy.realPress('ArrowLeft');
+
cy.focused().contains('#2');
+
+
// permits special key navigation
+
cy.realPress('Home');
+
cy.focused().contains('#1');
+
cy.realPress('End');
+
cy.focused().contains('#3');
+
+
// releases focus to original element on escape
+
cy.realPress('Escape');
+
cy.get('@input').should('have.focus');
+
});
+
+
it('should not allow the dialog to be tabbable', () => {
+
const Dialog = () => {
+
const ref = useRef<HTMLUListElement>(null);
+
useDialogFocus(ref);
+
return (
+
<ul ref={ref} role="dialog">
+
<li tabIndex={0}>#1</li>
+
<li tabIndex={0}>#2</li>
+
<li tabIndex={0}>#3</li>
+
</ul>
+
);
+
};
+
+
const App = () => {
+
const [hasDialog, setDialog] = useState(false);
+
return (
+
<main>
+
<button>before</button>
+
<input type="text" name="text" onFocus={() => setDialog(true)} />
+
{hasDialog && <Dialog />}
+
<button>after</button>
+
</main>
+
);
+
};
+
+
mount(<App />);
+
+
cy.get('input').first().as('input').focus();
+
cy.focused().should('have.property.name', 'text');
+
+
// Tabbing should skip over the dialog
+
cy.realPress('Tab');
+
cy.focused().contains('after');
+
// Tabbing back should skip over the dialog
+
cy.realPress(['Shift', 'Tab']);
+
cy.get('@input').should('have.focus');
+
// Tabbing back on the owner shouldn't affect the dialog
+
cy.realPress(['Shift', 'Tab']);
+
cy.focused().contains('before');
+
// It should still know which element the owner was
+
cy.realPress('Tab');
+
cy.realPress('ArrowDown');
+
cy.focused().contains('#1');
+
// From inside the dialog tabbing should skip out of the dialog
+
cy.realPress('Tab');
+
cy.focused().contains('after');
+
});
+
+
it('supports being attached to an owner element', () => {
+
const Dialog = () => {
+
const ownerRef = useRef<HTMLInputElement>(null);
+
const ref = useRef<HTMLUListElement>(null);
+
+
useDialogFocus(ref, { ownerRef });
+
+
return (
+
<main>
+
<input type="text" name="text" ref={ownerRef} />
+
<ul ref={ref} role="dialog">
+
<li tabIndex={0}>#1</li>
+
<li tabIndex={0}>#2</li>
+
<li tabIndex={0}>#3</li>
+
</ul>
+
</main>
+
);
+
};
+
+
mount(<Dialog />);
+
+
cy.get('input').first().as('input').focus();
+
cy.focused().should('have.property.name', 'text');
+
+
// pressing escape on input shouldn't change focus
+
cy.realPress('Escape');
+
cy.get('@input').should('have.focus');
+
+
// pressing arrow down should start focusing the menu
+
cy.get('@input').focus();
+
cy.realPress('ArrowDown');
+
cy.focused().contains('#1');
+
cy.realPress('ArrowDown');
+
cy.focused().contains('#2');
+
+
// tabbing should skip over the dialog items
+
cy.realPress(['Shift', 'Tab']);
+
cy.get('@input').should('have.focus');
+
+
// pressing arrow up should start focusing the last item
+
cy.get('@input').focus();
+
cy.realPress('ArrowUp');
+
cy.focused().contains('#3');
+
+
// pressing enter should start focusing the first item
+
cy.get('@input').focus();
+
cy.realPress('Enter');
+
cy.focused().contains('#1');
+
+
// typing regular values should refocus the owner input
+
cy.realType('test');
+
cy.get('@input')
+
.should('have.focus')
+
.should('have.value', 'test');
+
+
// pressing escape should refocus input
+
cy.get('li').first().focus();
+
cy.realPress('Escape');
+
cy.get('@input').should('have.focus');
+
});
-7
src/__tests__/useDialogFocus.tsx
···
-
import React, { useRef } from 'react';
-
import { mount } from '@cypress/react';
-
-
import { useDialogFocus } from '../useDialogFocus';
-
-
it('allows dialogs to be navigated', () => {
-
});
···
+1 -1
src/__tests__/useMenuFocus.test.tsx
···
cy.realPress('ArrowUp');
cy.focused().contains('#3');
-
// pressing arrow up should start focusing the last item
cy.get('@input').focus();
cy.realPress('Enter');
cy.focused().contains('#1');
···
cy.realPress('ArrowUp');
cy.focused().contains('#3');
+
// pressing enter should start focusing the first item
cy.get('@input').focus();
cy.realPress('Enter');
cy.focused().contains('#1');
+2 -2
src/useDialogFocus.ts
···
if (
willReceiveFocus ||
-
(hasPriority && owner && event.target === owner)
) {
if (!contains(ref.current, active))
selection = snapshotSelection(owner);
···
}
} else if (
owner &&
-
contains(owner, active) &&
isInputElement(owner) &&
/^(?:Key|Digit)/.test(event.code)
) {
// Restore selection if a key is pressed on input
···
if (
willReceiveFocus ||
+
(hasPriority && owner && contains(event.target, owner))
) {
if (!contains(ref.current, active))
selection = snapshotSelection(owner);
···
}
} else if (
owner &&
isInputElement(owner) &&
+
!contains(owner, active) &&
/^(?:Key|Digit)/.test(event.code)
) {
// Restore selection if a key is pressed on input
+1 -1
src/utils/focus.ts
···
while (
(next = reverse ? next.previousElementSibling : next.nextElementSibling)
) {
-
if (isVisible(next) && !!node.matches(focusableSelectors)) {
return next as HTMLElement;
} else if (hasFocusTargets(next)) {
const targets = getFocusTargets(next);
···
while (
(next = reverse ? next.previousElementSibling : next.nextElementSibling)
) {
+
if (isVisible(next) && !!next.matches(focusableSelectors)) {
return next as HTMLElement;
} else if (hasFocusTargets(next)) {
const targets = getFocusTargets(next);