+6
-76
src/__tests__/useDialogFocus.test.tsx
+6
-76
src/__tests__/useDialogFocus.test.tsx
···
59
59
it('should not allow the dialog to be tabbable', () => {
60
60
const Dialog = () => {
61
61
const ref = useRef<HTMLUListElement>(null);
62
-
useDialogFocus(ref);
62
+
const ownerRef = useRef<HTMLInputElement>(null);
63
+
useDialogFocus(ref, { ownerRef });
63
64
return (
64
65
<div>
65
-
<input type="text" name="text" />
66
+
<input type="text" name="text" ref={ownerRef} />
66
67
<ul ref={ref} role="dialog">
67
68
<li tabIndex={0}>#1</li>
68
69
<li tabIndex={0}>#2</li>
···
183
184
const [visible, setVisible] = useState(false);
184
185
const [nested, setNested] = useState(false);
185
186
const ref = useRef<HTMLUListElement>(null);
187
+
const ownerRef = useRef<HTMLInputElement>(null);
186
188
187
-
useDialogFocus(ref, { disabled: !visible });
189
+
useDialogFocus(ref, { disabled: !visible, ownerRef });
188
190
189
191
return (
190
192
<main>
191
-
<input type="text" name="text" onFocus={() => setVisible(true)} />
192
-
{visible && (
193
-
<ul ref={ref} role="dialog">
194
-
<li tabIndex={0}>Outer #1</li>
195
-
<li tabIndex={0} onFocus={() => setNested(true)}>Outer #2</li>
196
-
{nested && <InnerDialog />}
197
-
</ul>
198
-
)}
199
-
<button>after</button>
200
-
</main>
201
-
);
202
-
};
203
-
204
-
mount(<OuterDialog />);
205
-
206
-
cy.get('input').first().as('input').focus();
207
-
cy.focused().should('have.property.name', 'text');
208
-
209
-
// select first dialog
210
-
cy.realPress('ArrowDown');
211
-
cy.focused().contains('Outer #1');
212
-
cy.realPress('ArrowDown');
213
-
cy.focused().contains('Outer #2');
214
-
215
-
// select second dialog
216
-
cy.realPress('ArrowDown');
217
-
cy.focused().contains('Inner #1');
218
-
cy.realPress('ArrowDown');
219
-
cy.focused().contains('Inner #2');
220
-
221
-
// remains in inner dialog
222
-
cy.realPress('ArrowDown');
223
-
cy.focused().contains('Inner #1');
224
-
225
-
// tabs to last dialog
226
-
cy.realPress(['Shift', 'Tab']);
227
-
cy.focused().contains('Outer #2');
228
-
229
-
// arrows bring us back to the inner dialog
230
-
cy.realPress('ArrowUp');
231
-
cy.focused().contains('Inner #2');
232
-
233
-
// tab out of dialogs
234
-
cy.realPress('Tab');
235
-
cy.focused().contains('after');
236
-
// we can't reenter the dialogs
237
-
cy.realPress(['Shift', 'Tab']);
238
-
cy.get('@input').should('have.focus');
239
-
});
240
-
241
-
it('supports nested dialogs', () => {
242
-
const InnerDialog = () => {
243
-
const ref = useRef<HTMLUListElement>(null);
244
-
useDialogFocus(ref);
245
-
246
-
return (
247
-
<ul ref={ref} role="dialog">
248
-
<li tabIndex={0}>Inner #1</li>
249
-
<li tabIndex={0}>Inner #2</li>
250
-
</ul>
251
-
);
252
-
};
253
-
254
-
const OuterDialog = () => {
255
-
const [visible, setVisible] = useState(false);
256
-
const [nested, setNested] = useState(false);
257
-
const ref = useRef<HTMLUListElement>(null);
258
-
259
-
useDialogFocus(ref, { disabled: !visible });
260
-
261
-
return (
262
-
<main>
263
-
<input type="text" name="text" onFocus={() => setVisible(true)} />
193
+
<input type="text" name="text" onFocus={() => setVisible(true)} ref={ownerRef} />
264
194
{visible && (
265
195
<ul ref={ref} role="dialog">
266
196
<li tabIndex={0}>Outer #1</li>
+6
-4
src/useDialogFocus.ts
+6
-4
src/useDialogFocus.ts
···
45
45
function onFocus(event: FocusEvent) {
46
46
if (!element || event.defaultPrevented) return;
47
47
48
-
const active = document.activeElement as HTMLElement;
49
48
const owner =
50
49
(ownerRef && ownerRef.current) || (selection && selection.element);
51
50
···
53
52
willReceiveFocus ||
54
53
(hasPriority && owner && contains(event.target, owner))
55
54
) {
56
-
if (!contains(ref.current, active))
55
+
if (!contains(ref.current, event.relatedTarget))
57
56
selection = snapshotSelection(owner);
58
57
willReceiveFocus = false;
59
58
return;
60
59
}
61
60
62
61
// Check whether focus is about to move into the container and prevent it
63
-
if (contains(ref.current, event.target)) {
62
+
if (
63
+
(hasPriority || !contains(ref.current, event.relatedTarget)) &&
64
+
contains(ref.current, event.target)
65
+
) {
64
66
event.preventDefault();
65
67
// Get the next focus target of the container
66
68
const focusTarget = getNextFocusTarget(element, !focusMovesForward);
···
157
159
/^(?:Key|Digit)/.test(event.code)
158
160
) {
159
161
// Restore selection if a key is pressed on input
160
-
event.preventDefault();
162
+
event.stopPropagation();
161
163
willReceiveFocus = false;
162
164
restoreSelection(selection);
163
165
}