diff --git a/src/shared-components/hooks/useListKeyDown.test.ts b/src/shared-components/hooks/useListKeyboardNavigation.test.ts similarity index 83% rename from src/shared-components/hooks/useListKeyDown.test.ts rename to src/shared-components/hooks/useListKeyboardNavigation.test.ts index 31d93aef3b..9381619edc 100644 --- a/src/shared-components/hooks/useListKeyDown.test.ts +++ b/src/shared-components/hooks/useListKeyboardNavigation.test.ts @@ -8,7 +8,7 @@ import { type KeyboardEvent } from "react"; import { renderHook } from "jest-matrix-react"; -import { useListKeyDown } from "./useListKeyDown"; +import { useListKeyboardNavigation } from "./useListKeyboardNavigation"; describe("useListKeyDown", () => { let mockList: HTMLUListElement; @@ -51,9 +51,10 @@ describe("useListKeyDown", () => { current: { listRef: React.RefObject; onKeyDown: React.KeyboardEventHandler; + onFocus: React.FocusEventHandler; }; } { - const { result } = renderHook(() => useListKeyDown()); + const { result } = renderHook(() => useListKeyboardNavigation()); result.current.listRef.current = mockList; return result; } @@ -137,4 +138,18 @@ describe("useListKeyDown", () => { expect(mockEvent.preventDefault).not.toHaveBeenCalled(); }); + + it("should focus the first item if list itself is focused", () => { + const result = render(); + result.current.onFocus({ target: mockList } as React.FocusEvent); + expect(mockItems[0].focus).toHaveBeenCalledTimes(1); + }); + + it("should focus the selected item if list itself is focused", () => { + mockItems[1].setAttribute("aria-selected", "true"); + const result = render(); + + result.current.onFocus({ target: mockList } as React.FocusEvent); + expect(mockItems[1].focus).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/shared-components/hooks/useListKeyDown.ts b/src/shared-components/hooks/useListKeyboardNavigation.ts similarity index 66% rename from src/shared-components/hooks/useListKeyDown.ts rename to src/shared-components/hooks/useListKeyboardNavigation.ts index 9948511b9f..71d87aa12a 100644 --- a/src/shared-components/hooks/useListKeyDown.ts +++ b/src/shared-components/hooks/useListKeyboardNavigation.ts @@ -5,17 +5,45 @@ * Please see LICENSE files in the repository root for full details. */ -import { useCallback, useRef, type RefObject, type KeyboardEvent, type KeyboardEventHandler } from "react"; +import { + useCallback, + useRef, + type RefObject, + type KeyboardEvent, + type KeyboardEventHandler, + type FocusEventHandler, + type FocusEvent, +} from "react"; /** * A hook that provides keyboard navigation for a list of options. */ -export function useListKeyDown(): { +export function useListKeyboardNavigation(): { listRef: RefObject; onKeyDown: KeyboardEventHandler; + onFocus: FocusEventHandler; } { const listRef = useRef(null); + const onFocus = useCallback((evt: FocusEvent) => { + if (!listRef.current) return; + + if (evt.target === listRef.current) { + // By default, focus the selected item + let selectedChild = listRef.current?.firstElementChild; + + // If there is a selected item, focus that instead + for (const child of listRef.current.children) { + if (child.getAttribute("aria-selected") === "true") { + selectedChild = child; + break; + } + } + + (selectedChild as HTMLElement)?.focus(); + } + }, []); + const onKeyDown = useCallback((evt: KeyboardEvent) => { const { key } = evt; @@ -60,5 +88,5 @@ export function useListKeyDown(): { evt.preventDefault(); } }, []); - return { listRef, onKeyDown }; + return { listRef, onKeyDown, onFocus }; } diff --git a/src/shared-components/rich-list/RichItem/RichItem.tsx b/src/shared-components/rich-list/RichItem/RichItem.tsx index 2d7a0ae57f..3cef2690fc 100644 --- a/src/shared-components/rich-list/RichItem/RichItem.tsx +++ b/src/shared-components/rich-list/RichItem/RichItem.tsx @@ -67,7 +67,7 @@ export const RichItem = memo(function RichItem({