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/buttons/IconButton.tsx b/frontend/src/buttons/IconButton.tsx index c88292d9b..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,6 +115,11 @@ export const IconButton = React.forwardRef( const button = ( 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/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/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')} 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": "", 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 && (