Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/use-announcements.md
Original file line number Diff line number Diff line change
@@ -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.
61 changes: 33 additions & 28 deletions packages/react/src/FilteredActionList/useAnnouncements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -105,40 +106,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
}
Comment on lines +109 to +115
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delayMs is already passed to announce(...) to intentionally delay announcements (see file-level comment). Using the same delayMs as the setTimeout debounce means list updates are delayed twice (timeout + announce delay), likely ~1000ms instead of the previous 500ms. Consider separating debounce timing from the announcement delay (e.g., debounceMs for the outer timeout, and keep delayMs only for announce, or vice-versa).

This issue also appears in the following locations of the same file:

  • line 108
  • line 111
  • line 124

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense, perhaps we should remove the delay for this function in lieu of the added debounce

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the changes in pr, please validate


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
})
})
}
}, debounceMs)

return () => window.clearTimeout(timeoutId)
},
[
announce,
Expand Down
Loading