From 9a78da0ec3b5eb6efdcab2c6d241b13533f27f13 Mon Sep 17 00:00:00 2001 From: Evan Bowers Date: Mon, 14 Sep 2026 11:37:36 -0700 Subject: [PATCH 1/3] Icon buttons get their accessible name; the chat's close is "Close chat" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IconButton wraps its Tooltip around a that wraps the real MuiIconButton, so MUI's own aria-label (derived from `title`) landed on the span — a wrapper nothing focuses or reads — and every icon-only button in the app was nameless: to a screen reader, and to the e2e suite's getByRole('button', { name }), which found "New chat" (a text button) and "Usage" but neither the chat's pop-out, its new-chat, nor its close. The button itself now carries aria-label from a string title; a ReactNode title is left to the Tooltip as before. The chat panel's close button is titled "Close chat" (new key chat.closeChat) rather than "Close": the title is the button's name, and a bare "Close" is ambiguous beside every other close on the page — for assistive tech, and for the suite, which dismisses a docked chat by that name. Context: a DEV build (PENDING_FEATURES) and the AI portal open the chat by default, and at 1280px that docks 400px and folds the sidebar into the hamburger by the layout's own rule — so a suite that keys on the sidebar's Devices link needs a way to put the sidebar back. typecheck clean, frontend 49/49, i18n:check + extract-idempotency clean, vite build green. Co-Authored-By: Claude Opus 5 --- frontend/src/buttons/IconButton.tsx | 5 +++++ frontend/src/components/Chat/ChatPanel.tsx | 5 ++++- frontend/src/i18n/locales/de/app.json | 1 + frontend/src/i18n/locales/en/app.json | 1 + frontend/src/i18n/locales/es/app.json | 1 + frontend/src/i18n/locales/ja/app.json | 1 + 6 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/buttons/IconButton.tsx b/frontend/src/buttons/IconButton.tsx index c88292d9b..832cd902f 100644 --- a/frontend/src/buttons/IconButton.tsx +++ b/frontend/src/buttons/IconButton.tsx @@ -107,6 +107,11 @@ export const IconButton = React.forwardRef( const button = ( around the + // button, so MUI's own aria-label landed on the span — a wrapper nothing focuses or reads + // — and every icon button in the app was nameless to assistive tech and to the e2e suite's + // getByRole('button', { name }). Name the button itself; a string title is the label. + aria-label={typeof title === 'string' ? title : undefined} sx={updatedSx} size={buttonBaseSize} onClick={clickHandler} diff --git a/frontend/src/components/Chat/ChatPanel.tsx b/frontend/src/components/Chat/ChatPanel.tsx index 31ab6d2d6..43697357e 100644 --- a/frontend/src/components/Chat/ChatPanel.tsx +++ b/frontend/src/components/Chat/ChatPanel.tsx @@ -150,7 +150,10 @@ export const ChatPanel: React.FC = () => { /> )} - dispatch.chat.set({ open: false })} /> + {/* "Close chat", not "Close": the title is this icon-only button's accessible name, and a + bare "Close" is ambiguous beside every other close on the page — for a screen reader, + and for the e2e suite, which dismisses a docked chat by that name (helpers/app.ts). */} + dispatch.chat.set({ open: false })} /> diff --git a/frontend/src/i18n/locales/de/app.json b/frontend/src/i18n/locales/de/app.json index 81f1330f3..7f41cd0f8 100644 --- a/frontend/src/i18n/locales/de/app.json +++ b/frontend/src/i18n/locales/de/app.json @@ -293,6 +293,7 @@ "chat": { "approve": "", "close": "", + "closeChat": "", "deleteConfirmAction": "", "deleteConfirmTitle": "", "deleteConversation": "", diff --git a/frontend/src/i18n/locales/en/app.json b/frontend/src/i18n/locales/en/app.json index 828806613..b4fde2029 100644 --- a/frontend/src/i18n/locales/en/app.json +++ b/frontend/src/i18n/locales/en/app.json @@ -293,6 +293,7 @@ "chat": { "approve": "Approve", "close": "Close", + "closeChat": "Close chat", "deleteConfirmAction": "Delete", "deleteConfirmTitle": "Delete this conversation?", "deleteConversation": "Delete", diff --git a/frontend/src/i18n/locales/es/app.json b/frontend/src/i18n/locales/es/app.json index ea99ea057..2ef27e54b 100644 --- a/frontend/src/i18n/locales/es/app.json +++ b/frontend/src/i18n/locales/es/app.json @@ -296,6 +296,7 @@ "chat": { "approve": "", "close": "", + "closeChat": "", "deleteConfirmAction": "", "deleteConfirmTitle": "", "deleteConversation": "", diff --git a/frontend/src/i18n/locales/ja/app.json b/frontend/src/i18n/locales/ja/app.json index 42c25c5f8..7bd87ab9d 100644 --- a/frontend/src/i18n/locales/ja/app.json +++ b/frontend/src/i18n/locales/ja/app.json @@ -290,6 +290,7 @@ "chat": { "approve": "", "close": "", + "closeChat": "", "deleteConfirmAction": "", "deleteConfirmTitle": "", "deleteConversation": "", From 7fdd371793baa3cc0ebd635c6844f77743a81fd6 Mon Sep 17 00:00:00 2001 From: Evan Bowers Date: Mon, 14 Sep 2026 11:53:47 -0700 Subject: [PATCH 2/3] IconButton: a `label` for the name when the title cannot be it (Codex) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two shapes of title the string-title rule could not name, both raised in review: • a React-node title — ServiceKeySetting's "Get the Node.js package" + launch icon — named nothing (that button had been nameless before this branch too, but the fix should reach it); • a title that swaps in a disabled-state EXPLANATION — RegisterMenu's "Manage permission required…", ProductsActionBar's "Admin permissions required" — would have become the name of what the button DOES. `label` is the control's stable accessible name and wins over the title; the three call sites pass it (ConfirmIconButton forwards it). The explanation stays visible in the tooltip. typecheck clean, frontend 49/49, no new i18n keys, vite build green. Co-Authored-By: Claude Opus 5 --- frontend/src/buttons/IconButton.tsx | 18 +++++++++++++----- frontend/src/components/ProductsActionBar.tsx | 1 + frontend/src/components/RegisterMenu.tsx | 1 + frontend/src/components/ServiceKeySetting.tsx | 1 + 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend/src/buttons/IconButton.tsx b/frontend/src/buttons/IconButton.tsx index 832cd902f..903b47dd8 100644 --- a/frontend/src/buttons/IconButton.tsx +++ b/frontend/src/buttons/IconButton.tsx @@ -7,7 +7,14 @@ type VariantType = 'text' | 'contained' | 'outlined' export type ButtonProps = Omit & { to?: string + /** The tooltip. A STRING title is also the button's accessible name — unless `label` says + * otherwise (a title that is a React node, or one that swaps in an explanation). */ title?: React.ReactNode + /** The control's stable accessible name, for when `title` cannot be it: a node title + * (ServiceKeySetting's "Get the Node.js package" + launch icon), or a title that changes to + * a disabled-state explanation ("Manage permission required…") — which must not become + * the name of what the button DOES. */ + label?: string forceTitle?: boolean icon?: string name?: string @@ -35,6 +42,7 @@ export const IconButton = React.forwardRef( to, sx = {}, title, + label, forceTitle, icon, name, @@ -107,11 +115,11 @@ export const IconButton = React.forwardRef( const button = ( around the - // button, so MUI's own aria-label landed on the span — a wrapper nothing focuses or reads - // — and every icon button in the app was nameless to assistive tech and to the e2e suite's - // getByRole('button', { name }). Name the button itself; a string title is the label. - aria-label={typeof title === 'string' ? title : undefined} + // Name the BUTTON. The Tooltip below wraps a around it, so MUI's own aria-label + // landed on the span — a wrapper nothing focuses or reads — and every icon button in the + // app was nameless to assistive tech and to the e2e suite's getByRole('button', { name }). + // `label` wins; otherwise a string title is the name; a node title names nothing here. + aria-label={label ?? (typeof title === 'string' ? title : undefined)} sx={updatedSx} size={buttonBaseSize} onClick={clickHandler} diff --git a/frontend/src/components/ProductsActionBar.tsx b/frontend/src/components/ProductsActionBar.tsx index 6eb57e0eb..e2c76f3a6 100644 --- a/frontend/src/components/ProductsActionBar.tsx +++ b/frontend/src/components/ProductsActionBar.tsx @@ -69,6 +69,7 @@ export const ProductsActionBar: React.FC = ({ select }) => { = ({ fab, buttonSize = 38, sidebar, . '&.Mui-disabled': { backgroundColor: 'white.main', color: 'gray.main' }, }), }} + label={t('registerMenu.addDevice', 'Add device')} title={ unauthorized ? t( diff --git a/frontend/src/components/ServiceKeySetting.tsx b/frontend/src/components/ServiceKeySetting.tsx index e8652498f..938a42c0a 100644 --- a/frontend/src/components/ServiceKeySetting.tsx +++ b/frontend/src/components/ServiceKeySetting.tsx @@ -101,6 +101,7 @@ export const ServiceKeySetting: React.FC = ({ connection, service, permis {t('serviceKeySetting.nodePackage', 'Get the Node.js package')} From b02f2201ac2936c222b8e047c23f0fd8389992b6 Mon Sep 17 00:00:00 2001 From: Evan Bowers Date: Mon, 14 Sep 2026 13:05:02 -0700 Subject: [PATCH 3/3] Copy controls keep their action as the name while the tooltip flashes "Copied!" (Codex) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CopyIconButton swaps its title for "Copied!" for 800ms (ScriptEditPage's copy for two seconds); with the title now the button's accessible name, the control was briefly renamed to a status — findable by neither screen reader, voice control nor a role-based locator. The stable `label` carries the action; the tooltip keeps the feedback. The remaining "Copied!" title in the tree (ListItemCopy) is a ListItemButton named by its visible text, not an IconButton. typecheck clean, frontend 49/49, vite build green. Co-Authored-By: Claude Opus 5 --- frontend/src/buttons/CopyIconButton.tsx | 4 ++++ frontend/src/pages/ScriptEditPage.tsx | 1 + 2 files changed, 5 insertions(+) diff --git a/frontend/src/buttons/CopyIconButton.tsx b/frontend/src/buttons/CopyIconButton.tsx index 93a047ab9..a9544d386 100644 --- a/frontend/src/buttons/CopyIconButton.tsx +++ b/frontend/src/buttons/CopyIconButton.tsx @@ -56,6 +56,9 @@ export const CopyIconButton = React.forwardRef setOpen(false) + // The name stays the ACTION ("Copy command") while the tooltip flashes the status: a control + // renamed "Copied!" for 800ms is one a screen reader or voice control can no longer find. + const label = props.label ?? (typeof title === 'string' ? title : undefined) title = clipboard.copied ? 'Copied!' : title return ( @@ -66,6 +69,7 @@ export const CopyIconButton = React.forwardRef diff --git a/frontend/src/pages/ScriptEditPage.tsx b/frontend/src/pages/ScriptEditPage.tsx index 595eff56e..a9681d83b 100644 --- a/frontend/src/pages/ScriptEditPage.tsx +++ b/frontend/src/pages/ScriptEditPage.tsx @@ -169,6 +169,7 @@ export const ScriptEditPage: React.FC = ({ isNew }) => { {!!editForm?.script && !loading && (