Skip to content
Merged
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
4 changes: 4 additions & 0 deletions frontend/src/buttons/CopyIconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export const CopyIconButton = React.forwardRef<HTMLButtonElement, CopyButtonProp

const onClose = () => 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 (
Expand All @@ -66,6 +69,7 @@ export const CopyIconButton = React.forwardRef<HTMLButtonElement, CopyButtonProp
onClick={check}
color={clipboard.copied ? colorCopied : props.color}
icon={clipboard.copied ? 'check' : icon}
label={label}
title={title}
size={size}
/>
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/buttons/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ type VariantType = 'text' | 'contained' | 'outlined'

export type ButtonProps = Omit<IconProps, 'title'> & {
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
Expand Down Expand Up @@ -35,6 +42,7 @@ export const IconButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
to,
sx = {},
title,
label,
forceTitle,
icon,
name,
Expand Down Expand Up @@ -107,6 +115,11 @@ export const IconButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
const button = (
<MuiIconButton
{...{ ref, disabled, onMouseDown, onMouseEnter, onMouseLeave, className }}
// Name the BUTTON. The Tooltip below wraps a <span> 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)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep copy controls named for their action after activation

When a titled copy control succeeds, this derives its accessible name from the temporary feedback tooltip: CopyIconButton.tsx:59 replaces titles such as “Copy command” with “Copied!” for 800 ms, and ScriptEditPage.tsx:172 does the same for two seconds. The button remains actionable during that interval, but screen-reader and voice-control users can no longer identify it by its copy action, and role-based automation can transiently lose the locator. Pass a stable label for these controls while allowing title to display the status feedback.

Useful? React with 👍 / 👎.

sx={updatedSx}
size={buttonBaseSize}
onClick={clickHandler}
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/Chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@ export const ChatPanel: React.FC = () => {
/>
)}
<NewChatButton />
<IconButton icon="times" title={t('chat.close', 'Close')} onClick={() => 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). */}
<IconButton icon="times" title={t('chat.closeChat', 'Close chat')} onClick={() => dispatch.chat.set({ open: false })} />
</ChatHeader>
<ChatBody />
</Box>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ProductsActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const ProductsActionBar: React.FC<Props> = ({ select }) => {
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<ConfirmIconButton
icon="trash"
label={t('productsActionBar.deleteSelected', 'Delete selected')}
title={
admin
? t('productsActionBar.deleteSelected', 'Delete selected')
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/RegisterMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const RegisterMenu: React.FC<Props> = ({ fab, buttonSize = 38, sidebar, .
'&.Mui-disabled': { backgroundColor: 'white.main', color: 'gray.main' },
}),
}}
label={t('registerMenu.addDevice', 'Add device')}
title={
unauthorized
? t(
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ServiceKeySetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const ServiceKeySetting: React.FC<Props> = ({ connection, service, permis
<IconButton
type="brands"
name="github"
label={t('serviceKeySetting.nodePackage', 'Get the Node.js package')}
title={
<>
{t('serviceKeySetting.nodePackage', 'Get the Node.js package')}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/de/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@
"chat": {
"approve": "",
"close": "",
"closeChat": "",
"deleteConfirmAction": "",
"deleteConfirmTitle": "",
"deleteConversation": "",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@
"chat": {
"approve": "Approve",
"close": "Close",
"closeChat": "Close chat",
"deleteConfirmAction": "Delete",
"deleteConfirmTitle": "Delete this conversation?",
"deleteConversation": "Delete",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/es/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@
"chat": {
"approve": "",
"close": "",
"closeChat": "",
"deleteConfirmAction": "",
"deleteConfirmTitle": "",
"deleteConversation": "",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/ja/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@
"chat": {
"approve": "",
"close": "",
"closeChat": "",
"deleteConfirmAction": "",
"deleteConfirmTitle": "",
"deleteConversation": "",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/ScriptEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export const ScriptEditPage: React.FC<Props> = ({ isNew }) => {
{!!editForm?.script && !loading && (
<IconButton
name={copied ? 'check' : 'copy'}
label={t('scriptEditPage.copyScript', 'Copy Script')} // the name stays the action while the tooltip flashes the status
title={copied ? t('scriptEditPage.copied', 'Copied!') : t('scriptEditPage.copyScript', 'Copy Script')}
color={copied ? 'success' : 'grayDark'}
size="sm"
Expand Down
Loading