···11+import React, { useState, useRef } from 'react';
22+import { mount } from '@cypress/react';
33+44+import { useDialogFocus } from '../useDialogFocus';
55+66+it('allows dialogs to be navigated without an owner', () => {
77+ const Dialog = () => {
88+ const ref = useRef<HTMLUListElement>(null);
99+ useDialogFocus(ref);
1010+ return (
1111+ <ul ref={ref} role="dialog">
1212+ <li tabIndex={0}>#1</li>
1313+ <li tabIndex={0}>#2</li>
1414+ <li tabIndex={0}>#3</li>
1515+ </ul>
1616+ );
1717+ };
1818+1919+ const App = () => {
2020+ const [hasDialog, setDialog] = useState(false);
2121+ return (
2222+ <main>
2323+ <input type="text" name="text" onFocus={() => setDialog(true)} />
2424+ {hasDialog && <Dialog />}
2525+ </main>
2626+ );
2727+ };
2828+2929+ mount(<App />);
3030+3131+ cy.get('input').first().as('input').focus();
3232+ cy.focused().should('have.property.name', 'text');
3333+3434+ // ArrowRight/ArrowLeft shouldn't affect the selection for inputs
3535+ cy.realPress('ArrowRight');
3636+ cy.realPress('ArrowLeft');
3737+ cy.get('@input').should('have.focus');
3838+3939+ // Navigation with arrow keys is normal otherwise
4040+ cy.realPress('ArrowDown');
4141+ cy.realPress('ArrowDown');
4242+ cy.focused().contains('#2');
4343+ cy.realPress('ArrowRight');
4444+ cy.focused().contains('#3');
4545+ cy.realPress('ArrowLeft');
4646+ cy.focused().contains('#2');
4747+4848+ // permits special key navigation
4949+ cy.realPress('Home');
5050+ cy.focused().contains('#1');
5151+ cy.realPress('End');
5252+ cy.focused().contains('#3');
5353+5454+ // releases focus to original element on escape
5555+ cy.realPress('Escape');
5656+ cy.get('@input').should('have.focus');
5757+});
5858+5959+it('should not allow the dialog to be tabbable', () => {
6060+ const Dialog = () => {
6161+ const ref = useRef<HTMLUListElement>(null);
6262+ useDialogFocus(ref);
6363+ return (
6464+ <ul ref={ref} role="dialog">
6565+ <li tabIndex={0}>#1</li>
6666+ <li tabIndex={0}>#2</li>
6767+ <li tabIndex={0}>#3</li>
6868+ </ul>
6969+ );
7070+ };
7171+7272+ const App = () => {
7373+ const [hasDialog, setDialog] = useState(false);
7474+ return (
7575+ <main>
7676+ <button>before</button>
7777+ <input type="text" name="text" onFocus={() => setDialog(true)} />
7878+ {hasDialog && <Dialog />}
7979+ <button>after</button>
8080+ </main>
8181+ );
8282+ };
8383+8484+ mount(<App />);
8585+8686+ cy.get('input').first().as('input').focus();
8787+ cy.focused().should('have.property.name', 'text');
8888+8989+ // Tabbing should skip over the dialog
9090+ cy.realPress('Tab');
9191+ cy.focused().contains('after');
9292+ // Tabbing back should skip over the dialog
9393+ cy.realPress(['Shift', 'Tab']);
9494+ cy.get('@input').should('have.focus');
9595+ // Tabbing back on the owner shouldn't affect the dialog
9696+ cy.realPress(['Shift', 'Tab']);
9797+ cy.focused().contains('before');
9898+ // It should still know which element the owner was
9999+ cy.realPress('Tab');
100100+ cy.realPress('ArrowDown');
101101+ cy.focused().contains('#1');
102102+ // From inside the dialog tabbing should skip out of the dialog
103103+ cy.realPress('Tab');
104104+ cy.focused().contains('after');
105105+});
106106+107107+it('supports being attached to an owner element', () => {
108108+ const Dialog = () => {
109109+ const ownerRef = useRef<HTMLInputElement>(null);
110110+ const ref = useRef<HTMLUListElement>(null);
111111+112112+ useDialogFocus(ref, { ownerRef });
113113+114114+ return (
115115+ <main>
116116+ <input type="text" name="text" ref={ownerRef} />
117117+ <ul ref={ref} role="dialog">
118118+ <li tabIndex={0}>#1</li>
119119+ <li tabIndex={0}>#2</li>
120120+ <li tabIndex={0}>#3</li>
121121+ </ul>
122122+ </main>
123123+ );
124124+ };
125125+126126+ mount(<Dialog />);
127127+128128+ cy.get('input').first().as('input').focus();
129129+ cy.focused().should('have.property.name', 'text');
130130+131131+ // pressing escape on input shouldn't change focus
132132+ cy.realPress('Escape');
133133+ cy.get('@input').should('have.focus');
134134+135135+ // pressing arrow down should start focusing the menu
136136+ cy.get('@input').focus();
137137+ cy.realPress('ArrowDown');
138138+ cy.focused().contains('#1');
139139+ cy.realPress('ArrowDown');
140140+ cy.focused().contains('#2');
141141+142142+ // tabbing should skip over the dialog items
143143+ cy.realPress(['Shift', 'Tab']);
144144+ cy.get('@input').should('have.focus');
145145+146146+ // pressing arrow up should start focusing the last item
147147+ cy.get('@input').focus();
148148+ cy.realPress('ArrowUp');
149149+ cy.focused().contains('#3');
150150+151151+ // pressing enter should start focusing the first item
152152+ cy.get('@input').focus();
153153+ cy.realPress('Enter');
154154+ cy.focused().contains('#1');
155155+156156+ // typing regular values should refocus the owner input
157157+ cy.realType('test');
158158+ cy.get('@input')
159159+ .should('have.focus')
160160+ .should('have.value', 'test');
161161+162162+ // pressing escape should refocus input
163163+ cy.get('li').first().focus();
164164+ cy.realPress('Escape');
165165+ cy.get('@input').should('have.focus');
166166+});
-7
src/__tests__/useDialogFocus.tsx
···11-import React, { useRef } from 'react';
22-import { mount } from '@cypress/react';
33-44-import { useDialogFocus } from '../useDialogFocus';
55-66-it('allows dialogs to be navigated', () => {
77-});
+1-1
src/__tests__/useMenuFocus.test.tsx
···125125 cy.realPress('ArrowUp');
126126 cy.focused().contains('#3');
127127128128- // pressing arrow up should start focusing the last item
128128+ // pressing enter should start focusing the first item
129129 cy.get('@input').focus();
130130 cy.realPress('Enter');
131131 cy.focused().contains('#1');
+2-2
src/useDialogFocus.ts
···50505151 if (
5252 willReceiveFocus ||
5353- (hasPriority && owner && event.target === owner)
5353+ (hasPriority && owner && contains(event.target, owner))
5454 ) {
5555 if (!contains(ref.current, active))
5656 selection = snapshotSelection(owner);
···160160 }
161161 } else if (
162162 owner &&
163163- contains(owner, active) &&
164163 isInputElement(owner) &&
164164+ !contains(owner, active) &&
165165 /^(?:Key|Digit)/.test(event.code)
166166 ) {
167167 // Restore selection if a key is pressed on input
+1-1
src/utils/focus.ts
···7676 while (
7777 (next = reverse ? next.previousElementSibling : next.nextElementSibling)
7878 ) {
7979- if (isVisible(next) && !!node.matches(focusableSelectors)) {
7979+ if (isVisible(next) && !!next.matches(focusableSelectors)) {
8080 return next as HTMLElement;
8181 } else if (hasFocusTargets(next)) {
8282 const targets = getFocusTargets(next);