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

Add test for nested or duplicated useDialogFocus

Changed files
+192
src
+192
src/__tests__/useDialogFocus.test.tsx
···
cy.realPress('Escape');
cy.get('@input').should('have.focus');
});
+
+
it('supports nested dialogs', () => {
+
const InnerDialog = () => {
+
const ref = useRef<HTMLUListElement>(null);
+
useDialogFocus(ref);
+
+
return (
+
<ul ref={ref} role="dialog">
+
<li tabIndex={0}>Inner #1</li>
+
<li tabIndex={0}>Inner #2</li>
+
</ul>
+
);
+
};
+
+
const OuterDialog = () => {
+
const [visible, setVisible] = useState(false);
+
const [nested, setNested] = useState(false);
+
const ref = useRef<HTMLUListElement>(null);
+
+
useDialogFocus(ref, { disabled: !visible });
+
+
return (
+
<main>
+
<input type="text" name="text" onFocus={() => setVisible(true)} />
+
{visible && (
+
<ul ref={ref} role="dialog">
+
<li tabIndex={0}>Outer #1</li>
+
<li tabIndex={0} onFocus={() => setNested(true)}>Outer #2</li>
+
{nested && <InnerDialog />}
+
</ul>
+
)}
+
<button>after</button>
+
</main>
+
);
+
};
+
+
mount(<OuterDialog />);
+
+
cy.get('input').first().as('input').focus();
+
cy.focused().should('have.property.name', 'text');
+
+
// select first dialog
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Outer #1');
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Outer #2');
+
+
// select second dialog
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Inner #1');
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Inner #2');
+
+
// remains in inner dialog
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Inner #1');
+
+
// tabs to last dialog
+
cy.realPress(['Shift', 'Tab']);
+
cy.focused().contains('Outer #2');
+
+
// arrows bring us back to the inner dialog
+
cy.realPress('ArrowUp');
+
cy.focused().contains('Inner #2');
+
+
// tab out of dialogs
+
cy.realPress('Tab');
+
cy.focused().contains('after');
+
// we can't reenter the dialogs
+
cy.realPress(['Shift', 'Tab']);
+
cy.get('@input').should('have.focus');
+
});
+
+
it('supports nested dialogs', () => {
+
const InnerDialog = () => {
+
const ref = useRef<HTMLUListElement>(null);
+
useDialogFocus(ref);
+
+
return (
+
<ul ref={ref} role="dialog">
+
<li tabIndex={0}>Inner #1</li>
+
<li tabIndex={0}>Inner #2</li>
+
</ul>
+
);
+
};
+
+
const OuterDialog = () => {
+
const [visible, setVisible] = useState(false);
+
const [nested, setNested] = useState(false);
+
const ref = useRef<HTMLUListElement>(null);
+
+
useDialogFocus(ref, { disabled: !visible });
+
+
return (
+
<main>
+
<input type="text" name="text" onFocus={() => setVisible(true)} />
+
{visible && (
+
<ul ref={ref} role="dialog">
+
<li tabIndex={0}>Outer #1</li>
+
<li tabIndex={0} onFocus={() => setNested(true)}>Outer #2</li>
+
{nested && <InnerDialog />}
+
</ul>
+
)}
+
<button>after</button>
+
</main>
+
);
+
};
+
+
mount(<OuterDialog />);
+
+
cy.get('input').first().as('input').focus();
+
cy.focused().should('have.property.name', 'text');
+
+
// select first dialog
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Outer #1');
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Outer #2');
+
+
// select second dialog
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Inner #1');
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Inner #2');
+
+
// remains in inner dialog
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Inner #1');
+
+
// tabs to last dialog
+
cy.realPress(['Shift', 'Tab']);
+
cy.focused().contains('Outer #2');
+
+
// arrows bring us back to the inner dialog
+
cy.realPress('ArrowUp');
+
cy.focused().contains('Inner #2');
+
+
// tab out of dialogs
+
cy.realPress('Tab');
+
cy.focused().contains('after');
+
// we can't reenter the dialogs
+
cy.realPress(['Shift', 'Tab']);
+
cy.get('@input').should('have.focus');
+
});
+
+
it('allows dialogs in semantic order', () => {
+
const Dialog = ({ name }) => {
+
const ownerRef = useRef<HTMLInputElement>(null);
+
const ref = useRef<HTMLUListElement>(null);
+
+
useDialogFocus(ref, { ownerRef });
+
+
return (
+
<div>
+
<input type="text" className={name} ref={ownerRef} tabIndex={-1} />
+
<ul ref={ref} role="dialog">
+
<li tabIndex={0}>{name} #1</li>
+
<li tabIndex={0}>{name} #2</li>
+
</ul>
+
</div>
+
);
+
};
+
+
mount(
+
<main>
+
<Dialog name="First" />
+
<Dialog name="Second" />
+
<button>after</button>
+
</main>
+
);
+
+
cy.get('.First').first().as('first');
+
cy.get('.Second').first().as('second');
+
+
// focus first dialog
+
cy.get('@first').focus();
+
cy.get('.First').first().as('first').focus();
+
+
// tabs over both subsequent dialogs
+
cy.realPress('Tab');
+
cy.focused().contains('after');
+
+
// given a focused first input, doesn't allow the first dialog to be used
+
cy.get('@first').focus();
+
cy.realPress('ArrowDown');
+
cy.get('@first').should('have.focus');
+
+
// given a focused second input, does allow the second dialog to be used
+
cy.get('@second').focus();
+
cy.realPress('ArrowDown');
+
cy.focused().contains('Second #1');
+
});