Skip to content

Commit 78862d6

Browse files
committed
Allow useArrowKeyNavigation consumers tell if container is visible
1 parent a20156e commit 78862d6

File tree

3 files changed

+70
-7
lines changed

3 files changed

+70
-7
lines changed

src/components/input/SelectNext.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,12 @@ function SelectMain<T>({
150150
useKeyPress(['Escape'], closeListbox);
151151

152152
// Vertical arrow key for options in the listbox
153-
useArrowKeyNavigation(wrapperRef, { horizontal: false, loop: false });
153+
useArrowKeyNavigation(listboxRef, {
154+
horizontal: false,
155+
loop: false,
156+
autofocus: true,
157+
containerVisible: listboxOpen,
158+
});
154159

155160
useLayoutEffect(() => {
156161
// Focus toggle button after closing listbox, only if previously focused

src/hooks/test/use-arrow-key-navigation-test.js

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import { render } from 'preact';
2-
import { useRef } from 'preact/hooks';
2+
import { useCallback, useMemo, useRef, useState } from 'preact/hooks';
33
import { act } from 'preact/test-utils';
44

55
import { waitFor } from '../../test-util/wait';
66
import { useArrowKeyNavigation } from '../use-arrow-key-navigation';
77

88
function Toolbar({ navigationOptions = {} }) {
99
const containerRef = useRef();
10+
const visible = navigationOptions.containerVisible ?? true;
1011

1112
useArrowKeyNavigation(containerRef, navigationOptions);
1213

1314
return (
14-
<div ref={containerRef} data-testid="toolbar" tabIndex={-1}>
15+
<div
16+
ref={containerRef}
17+
data-testid="toolbar"
18+
tabIndex={-1}
19+
style={{ display: visible ? 'block' : 'none' }}
20+
>
1521
<button data-testid="bold">Bold</button>
1622
<button data-testid="italic">Italic</button>
1723
<button data-testid="underline">Underline</button>
@@ -22,6 +28,24 @@ function Toolbar({ navigationOptions = {} }) {
2228
);
2329
}
2430

31+
function ToolbarWithToggle({ navigationOptions = {} }) {
32+
const [visible, setVisible] = useState(false);
33+
const toggleVisible = useCallback(() => setVisible(prev => !prev), []);
34+
const options = useMemo(
35+
() => ({ ...navigationOptions, containerVisible: visible }),
36+
[navigationOptions, visible],
37+
);
38+
39+
return (
40+
<>
41+
<button data-testid="toggle" onClick={toggleVisible}>
42+
Toggle
43+
</button>
44+
<Toolbar navigationOptions={options} />
45+
</>
46+
);
47+
}
48+
2549
describe('useArrowKeyNavigation', () => {
2650
let container;
2751
let toolbar;
@@ -349,4 +373,20 @@ describe('useArrowKeyNavigation', () => {
349373
pressKey('ArrowLeft');
350374
assert.equal(currentItem(), 'Bold');
351375
});
376+
377+
it('should re-check focus sequence when container visibility changes', async () => {
378+
await act(() =>
379+
render(
380+
<ToolbarWithToggle navigationOptions={{ autofocus: true }} />,
381+
container,
382+
),
383+
);
384+
385+
// No button should be initially focused
386+
assert.equal(document.activeElement, document.body);
387+
388+
// Once we toggle the list open, the first item will be focused
389+
findElementByTestId('toggle').click();
390+
await waitFor(() => document.activeElement === findElementByTestId('bold'));
391+
});
352392
});

src/hooks/use-arrow-key-navigation.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ export type UseArrowKeyNavigationOptions = {
3333
* CSS selector which specifies the elements that navigation moves between
3434
*/
3535
selector?: string;
36+
37+
/**
38+
* Indicates if the container element is currently visible.
39+
* This information is used to focus the current element when the container
40+
* transitions from hidden to visible.
41+
*
42+
* Defaults to `true`.
43+
*/
44+
containerVisible?: boolean;
3645
};
3746

3847
/**
@@ -77,11 +86,12 @@ export function useArrowKeyNavigation(
7786
horizontal = true,
7887
vertical = true,
7988
selector = 'a,button',
89+
containerVisible = true,
8090
}: UseArrowKeyNavigationOptions = {},
8191
) {
8292
// Keep track of the element that was last focused by this hook such that
83-
// navigation can be restored if focus moves outside of the container
84-
// and then back to/into it.
93+
// navigation can be restored if focus moves outside the container and then
94+
// back to/into it.
8595
const lastFocusedItem = useRef<HTMLOrSVGElement | null>(null);
8696

8797
useEffect(() => {
@@ -182,7 +192,7 @@ export function useArrowKeyNavigation(
182192
event.stopPropagation();
183193
};
184194

185-
updateTabIndexes(getNavigableElements(), 0, autofocus);
195+
updateTabIndexes(getNavigableElements(), 0, containerVisible && autofocus);
186196

187197
const listeners = new ListenerCollection();
188198

@@ -222,5 +232,13 @@ export function useArrowKeyNavigation(
222232
listeners.removeAll();
223233
mo.disconnect();
224234
};
225-
}, [autofocus, containerRef, horizontal, loop, selector, vertical]);
235+
}, [
236+
autofocus,
237+
containerRef,
238+
horizontal,
239+
loop,
240+
selector,
241+
vertical,
242+
containerVisible,
243+
]);
226244
}

0 commit comments

Comments
 (0)