From 737cd3b6f093473fc65b003295261614e469ced5 Mon Sep 17 00:00:00 2001 From: Mustafa Date: Fri, 10 Apr 2026 16:58:10 +0530 Subject: [PATCH 1/3] fix: debounce list update announcements to prevent race conditions during rapid filtering --- .../FilteredActionList/useAnnouncements.tsx | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/packages/react/src/FilteredActionList/useAnnouncements.tsx b/packages/react/src/FilteredActionList/useAnnouncements.tsx index dcc3a03062a..de8a6704362 100644 --- a/packages/react/src/FilteredActionList/useAnnouncements.tsx +++ b/packages/react/src/FilteredActionList/useAnnouncements.tsx @@ -105,40 +105,44 @@ export const useAnnouncements = ( function announceListUpdates() { if (isFirstRender) return // ignore on first render as announceInitialFocus will also announce - liveRegion?.clear() // clear previous announcements + const timeoutId = window.setTimeout(() => { + liveRegion?.clear() // clear previous announcements - if (items.length === 0 && !loading) { - announce(`${message?.title}. ${message?.description}`, {delayMs}) - return - } + if (items.length === 0 && !loading) { + announce(`${message?.title}. ${message?.description}`, {delayMs}) + return + } - if (usingRovingTabindex) { - const announcementText = `${items.length} item${items.length > 1 ? 's' : ''} available, ${selectedItems} selected.` - - announce(announcementText, { - delayMs, - from: liveRegion ? liveRegion : undefined, - }) - } else { - // give @primer/behaviors a moment to update active-descendant - window.requestAnimationFrame(() => { - const activeItem = getItemWithActiveDescendant(listContainerRef, items) - if (!activeItem) return - const {index, text, selected} = activeItem - - const announcementText = [ - `List updated`, - `Focused item: ${text}`, - `${selected ? 'selected' : 'not selected'}`, - `${index + 1} of ${items.length}`, - ].join(', ') + if (usingRovingTabindex) { + const announcementText = `${items.length} item${items.length > 1 ? 's' : ''} available, ${selectedItems} selected.` announce(announcementText, { delayMs, - from: liveRegion ? liveRegion : undefined, // announce will create a liveRegion if it doesn't find one + from: liveRegion ? liveRegion : undefined, }) - }) - } + } else { + // give @primer/behaviors a moment to update active-descendant + window.requestAnimationFrame(() => { + const activeItem = getItemWithActiveDescendant(listContainerRef, items) + if (!activeItem) return + const {index, text, selected} = activeItem + + const announcementText = [ + `List updated`, + `Focused item: ${text}`, + `${selected ? 'selected' : 'not selected'}`, + `${index + 1} of ${items.length}`, + ].join(', ') + + announce(announcementText, { + delayMs, + from: liveRegion ? liveRegion : undefined, // announce will create a liveRegion if it doesn't find one + }) + }) + } + }, delayMs) + + return () => window.clearTimeout(timeoutId) }, [ announce, From b6d17cb25d66daa0eab16406850d6dd553d5f103 Mon Sep 17 00:00:00 2001 From: Mustafa Date: Fri, 10 Apr 2026 17:04:01 +0530 Subject: [PATCH 2/3] fix: debounce list update announcements in useAnnouncements to prevent race conditions --- .changeset/use-announcements.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/use-announcements.md diff --git a/.changeset/use-announcements.md b/.changeset/use-announcements.md new file mode 100644 index 00000000000..1265668b413 --- /dev/null +++ b/.changeset/use-announcements.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Debounce list update announcements in `useAnnouncements` to prevent race conditions during rapid filtering. This ensures screen readers don't overwhelm users with intermediate states during fast typing. From 5fa21769c61ba164fe8a5c854585d1bd9c401efe Mon Sep 17 00:00:00 2001 From: Mustafa Date: Sat, 11 Apr 2026 11:50:55 +0530 Subject: [PATCH 3/3] refactor: update announcement delay to 300ms in useAnnouncements hook --- packages/react/src/FilteredActionList/useAnnouncements.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react/src/FilteredActionList/useAnnouncements.tsx b/packages/react/src/FilteredActionList/useAnnouncements.tsx index de8a6704362..daff3bb3f17 100644 --- a/packages/react/src/FilteredActionList/useAnnouncements.tsx +++ b/packages/react/src/FilteredActionList/useAnnouncements.tsx @@ -8,6 +8,7 @@ import type {ItemInput} from '../SelectPanel' // we add a delay so that it does not interrupt default screen reader announcement and queues after it const delayMs = 500 +const debounceMs = 300 const useFirstRender = () => { const firstRender = useRef(true) @@ -140,7 +141,7 @@ export const useAnnouncements = ( }) }) } - }, delayMs) + }, debounceMs) return () => window.clearTimeout(timeoutId) },