From 387d21d7b3c9a260ca6c31ba6e34519b96685e0c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 1 Oct 2025 15:13:53 +0200 Subject: [PATCH 1/4] fix: improve keyboard navigation on `RichList` --- src/shared-components/hooks/useListKeyDown.ts | 32 +++++++++++++++++-- .../rich-list/RichItem/RichItem.tsx | 2 +- .../rich-list/RichList/RichList.tsx | 3 +- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/shared-components/hooks/useListKeyDown.ts b/src/shared-components/hooks/useListKeyDown.ts index 9948511b9f4..e8d838dac20 100644 --- a/src/shared-components/hooks/useListKeyDown.ts +++ b/src/shared-components/hooks/useListKeyDown.ts @@ -5,7 +5,15 @@ * 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. @@ -13,9 +21,29 @@ import { useCallback, useRef, type RefObject, type KeyboardEvent, type KeyboardE export function useListKeyDown(): { 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 2d7a0ae57fb..3cef2690fc4 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({
  • ): JSX.Element { const id = useId(); - const { listRef, onKeyDown } = useListKeyDown(); + const { listRef, onKeyDown, onFocus } = useListKeyDown(); return ( @@ -70,6 +70,7 @@ export function RichList({ aria-labelledby={id} tabIndex={0} onKeyDown={onKeyDown} + onFocus={onFocus} > {children} From f64db4a91d3177744de8720045e344eca5d7b90d Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 1 Oct 2025 15:25:10 +0200 Subject: [PATCH 2/4] test: list focus handling --- .../hooks/useListKeyDown.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/shared-components/hooks/useListKeyDown.test.ts b/src/shared-components/hooks/useListKeyDown.test.ts index 31d93aef3b5..fad5a333694 100644 --- a/src/shared-components/hooks/useListKeyDown.test.ts +++ b/src/shared-components/hooks/useListKeyDown.test.ts @@ -51,6 +51,7 @@ describe("useListKeyDown", () => { current: { listRef: React.RefObject; onKeyDown: React.KeyboardEventHandler; + onFocus: React.FocusEventHandler; }; } { const { result } = renderHook(() => useListKeyDown()); @@ -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); + }); }); From 5cbd5f7ae4dd182c10197c8f2dd99a3b75cf71a8 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 1 Oct 2025 15:25:19 +0200 Subject: [PATCH 3/4] test: update snapshot --- .../RichItem/__snapshots__/RichItem.test.tsx.snap | 6 +++--- .../RichList/__snapshots__/RichList.test.tsx.snap | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/shared-components/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap b/src/shared-components/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap index fced60db2aa..84f6669b7ab 100644 --- a/src/shared-components/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap +++ b/src/shared-components/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap @@ -10,7 +10,7 @@ exports[`RichItem renders the item in default state 1`] = ` aria-label="Rich Item Title" class="richItem" role="option" - tabindex="0" + tabindex="-1" >