From 6399653836ef943bcfd4569ee992f9310513d323 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:56:24 +0100 Subject: [PATCH] Fix Assistant autocomplete screen reader focus The composer kept DOM focus in its textarea while only visually indicating the active autocomplete option. Screen reader users therefore could not identify the controlled listbox or follow mention and slash-command selection changes. Change-source: pulse-maintainer Contract-Neutral: Assistant autocomplete accessibility and responsive presentation only; no API or subsystem contract changed --- frontend-modern/browser-verification.json | 46 ++++++++-------- .../AI/Chat/MentionAutocomplete.tsx | 37 ++++++++++++- .../AI/Chat/SlashCommandAutocomplete.tsx | 38 ++++++++++++- .../AI/Chat/__tests__/AIChat.test.tsx | 9 ++- .../__tests__/MentionAutocomplete.test.tsx | 34 +++++++++++- .../SlashCommandAutocomplete.test.tsx | 55 ++++++++++++++++++- .../src/components/AI/Chat/index.tsx | 31 ++++++++++- 7 files changed, 219 insertions(+), 31 deletions(-) diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index e489265f1..144c4d492 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,19 +1,21 @@ { "version": 1, - "base_sha": "6c5af82b522b437db91ab077c79cee3d59833a34", - "verified_at": "2026-09-02T08:58:36Z", + "base_sha": "fc0adf7073d99447e8042d5053d331d323b8a372", + "verified_at": "2026-09-02T10:25:49Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/shared/SearchInputEnhancements.tsx", - "frontend-modern/src/components/shared/searchInputEnhancementsModel.ts", - "frontend-modern/src/components/shared/useSearchInputEnhancements.ts" + "frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx", + "frontend-modern/src/components/AI/Chat/SlashCommandAutocomplete.tsx", + "frontend-modern/src/components/AI/Chat/index.tsx" ], "content_sha256": { - "frontend-modern/src/components/shared/SearchInputEnhancements.tsx": "9df1147f092b523655a3503cfe27bbac88fb0572bd3ccb219895d2ffa2f762a7", - "frontend-modern/src/components/shared/searchInputEnhancementsModel.ts": "b56c756423156cdce3bf1c7f867aa3b6c2751419f070c2d8e5edad91d3c23dec", - "frontend-modern/src/components/shared/useSearchInputEnhancements.ts": "15d750671b420d6566749eac60bc4cb6c87db36037e90a4117cebc60c1635dad" + "frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx": "fde53ef7a82b0ff854a759087a0fc7dfaafd89e7e8933b68335598100adb8955", + "frontend-modern/src/components/AI/Chat/SlashCommandAutocomplete.tsx": "2073d3c388590dad8c291dfc7a0d8a3132e4c18a50014918c0a2a90510401c28", + "frontend-modern/src/components/AI/Chat/index.tsx": "010def979665a1db9be3dfa3537ebfa8714125fb7ce9212102fb49465d5d22db" }, - "routes": ["/standalone/machines"], + "routes": [ + "/" + ], "viewports": [ { "width": 1280, @@ -25,20 +27,20 @@ } ], "states": [ - "Empty recent-search menu with explanatory copy at desktop width", - "Populated three-entry menu with keyboard focus on its first, removal, and clear actions at desktop width", - "Selected search restored from persisted history after reload at desktop and narrow widths", - "Populated menu over the filtered no-match Machines state at narrow width", - "Menu closed after outside pointer dismissal and Escape at desktop and narrow widths" + "Closed named Assistant composer with no listbox relationship in desktop docked and narrow overlay layouts", + "Open slash-command list with the selected active option and the wrapped final option scrolled into view", + "Filtered slash-command empty result with listbox ownership and no stale active descendant", + "Slash command selected with Tab and popup closed with composer focus restored", + "Open resource mention list with the selected active option and the wrapped final option scrolled into view", + "Mention selected by pointer and popup closed with composer focus retained", + "Autocomplete closed by Escape and outside pointer dismissal without stale relationships" ], "interactions": [ - "opened the history menu by pointer, ArrowDown, and ArrowUp and verified menu semantics, aria-controls, and expanded state", - "moved focus through entry, item-specific removal, and clear actions with ArrowDown, ArrowUp, Home, and End", - "selected a history entry and verified the menu closed while the search field received focus and the selected value", - "removed the focused first history entry and verified focus moved to the next item-specific removal action", - "cleared all history and verified the menu closed, storage emptied, and search focus returned", - "committed a search, reloaded the route, and verified the persisted entry remained available", - "dismissed the menu by outside pointer action and Escape and verified Escape returned focus to the toggle", - "inspected desktop and narrow pixels for placement, clipping, stacking, responsive width, and visible keyboard focus" + "opened Pulse Assistant from the current-view launcher and verified the composer accessible name and focus", + "opened slash commands and mentions by typing, then checked aria-controls, aria-activedescendant, aria-selected, and option tab exclusion", + "moved active options with ArrowDown and wrapped with ArrowUp, hovered options by pointer, and verified active options remained visible", + "selected /new with Tab and selected a mention by pointer while verifying focus remained in the composer", + "exercised Escape and outside pointer dismissal, including the no-match slash-command state", + "inspected desktop and narrow pixels for popup placement, viewport containment, clipping, stacking, scrolling, and visible composer focus" ] } diff --git a/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx b/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx index 8421f5039..afff643d5 100644 --- a/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx +++ b/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx @@ -17,8 +17,12 @@ interface MentionAutocompleteProps { onSelect: (resource: MentionResource) => void; onClose: () => void; visible: boolean; + onActiveDescendantChange?: (id: string | undefined) => void; } +export const ASSISTANT_MENTION_LISTBOX_ID = 'assistant-mention-listbox'; +export const getAssistantMentionOptionId = (index: number) => `assistant-mention-option-${index}`; + export function MentionAutocomplete(props: MentionAutocompleteProps) { const [selectedIndex, setSelectedIndex] = createSignal(0); @@ -36,6 +40,27 @@ export function MentionAutocomplete(props: MentionAutocompleteProps) { setSelectedIndex(0); }); + createEffect(() => { + const total = filteredResources().length; + setSelectedIndex((index) => (total > 0 ? Math.min(index, total - 1) : 0)); + }); + + createEffect(() => { + const resources = filteredResources(); + const activeId = + props.visible && resources.length > 0 + ? getAssistantMentionOptionId(selectedIndex()) + : undefined; + props.onActiveDescendantChange?.(activeId); + if (activeId) { + queueMicrotask(() => { + document.getElementById(activeId)?.scrollIntoView?.({ block: 'nearest' }); + }); + } + }); + + onCleanup(() => props.onActiveDescendantChange?.(undefined)); + const consumeMentionKey = (e: KeyboardEvent) => { e.preventDefault(); e.stopPropagation(); @@ -159,7 +184,7 @@ export function MentionAutocomplete(props: MentionAutocompleteProps) { return ( 0}>
event.stopPropagation()} >
Resources
-
+
{(resource, index) => (
= (props) => { onSelect={handleSlashCommandSelect} onClose={() => closeSlashCommandAutocomplete({ clearTransientDraft: true })} visible={slashCommandActive()} + onActiveDescendantChange={setSlashCommandActiveDescendant} />