Compare commits

...

4 Commits

Author SHA1 Message Date
Elizabeth Thompson
f63142e5e2 fix(a11y): suppress visible text for icon-only toolbar buttons
label was added to the Delete/Column settings/Row settings IconButtons
for their accessible name, but IconButton renders any truthy label as
visible text via StyledSpan, so these compact toolbar icons started
showing "Delete component"/"Column settings"/"Row settings" text next
to the icon.

Adds an opt-in hideVisibleLabel prop that keeps aria-label but skips
the visible span, applied only at the three new call sites. The
DashboardBuilder "Collapse tab content" menu item, which intentionally
shows visible label text, is untouched.

Per richardfogaca/rusackas review feedback (rebenitez1802's original
finding).
2026-07-26 18:25:55 +00:00
Elizabeth Thompson
2821c5760b test(dashboard): update Header delete-button query for new accessible name
DeleteComponentButton now labels its IconButton 'Delete component', so the
role/name query for the old icon-derived name 'delete' no longer matches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 00:41:58 +00:00
Elizabeth Thompson
055c9dc136 Merge branch 'master' into fix/dashboard-icon-button-aria-label 2026-07-22 00:37:59 +00:00
Elizabeth Thompson
4f0434ba1d fix(a11y): add aria-label to dashboard IconButton and unlabeled call sites
Icon-only buttons (delete, row settings, column settings) had no
accessible name, making them invisible to screen readers despite being
keyboard-focusable via tabIndex=0. Wires the existing label prop through
as aria-label on the underlying div[role=button] and provides labels at
all three call sites that previously passed none.

WCAG 2.1 SC 4.1.2 (Name, Role, Value) — Level A.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 00:44:35 +00:00
6 changed files with 28 additions and 2 deletions

View File

@@ -18,6 +18,7 @@
*/
import { MouseEventHandler, FC } from 'react';
import { t } from '@apache-superset/core/translation';
import { Icons } from '@superset-ui/core/components/Icons';
import type { IconType } from '@superset-ui/core/components/Icons/types';
import IconButton from './IconButton';
@@ -33,6 +34,8 @@ const DeleteComponentButton: FC<DeleteComponentButtonProps> = ({
}) => (
<IconButton
onClick={onDelete}
label={t('Delete component')}
hideVisibleLabel
icon={<Icons.DeleteOutlined iconSize={iconSize ?? 'l'} />}
/>
);

View File

@@ -73,3 +73,17 @@ test('renders the provided label', () => {
expect(screen.getByText('My Label')).toBeInTheDocument();
});
test('hideVisibleLabel suppresses visible text but keeps the accessible name', () => {
render(
<IconButton
icon={icon}
onClick={jest.fn()}
label="My Label"
hideVisibleLabel
/>,
);
expect(screen.queryByText('My Label')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'My Label' })).toBeInTheDocument();
});

View File

@@ -22,6 +22,7 @@ import { styled, SupersetTheme } from '@apache-superset/core/theme';
interface IconButtonProps extends HTMLAttributes<HTMLDivElement> {
icon: JSX.Element;
label?: string;
hideVisibleLabel?: boolean;
onClick: MouseEventHandler<HTMLDivElement>;
disabled?: boolean;
'data-test'?: string;
@@ -59,6 +60,7 @@ const IconButton = forwardRef<HTMLDivElement, IconButtonProps>(
{
icon,
label,
hideVisibleLabel,
onClick,
onKeyDown,
disabled,
@@ -72,6 +74,7 @@ const IconButton = forwardRef<HTMLDivElement, IconButtonProps>(
ref={ref}
tabIndex={disabled ? -1 : 0}
role="button"
aria-label={label}
isDisabled={disabled}
aria-disabled={disabled}
data-test={dataTest}
@@ -88,7 +91,7 @@ const IconButton = forwardRef<HTMLDivElement, IconButtonProps>(
}}
>
{icon}
{label && <StyledSpan>{label}</StyledSpan>}
{label && !hideVisibleLabel && <StyledSpan>{label}</StyledSpan>}
</StyledDiv>
),
);

View File

@@ -247,6 +247,8 @@ const Column = (props: ColumnProps) => {
/>
<IconButton
onClick={() => handleChangeFocus(true)}
label={t('Column settings')}
hideVisibleLabel
icon={<Icons.SettingOutlined iconSize="m" />}
/>
</HoverMenu>

View File

@@ -145,7 +145,9 @@ describe('Header', () => {
const deleteComponent = jest.fn();
setup({ editMode: true, deleteComponent });
const trashButton = screen.getByRole('button', { name: 'delete' });
const trashButton = screen.getByRole('button', {
name: 'Delete component',
});
fireEvent.click(trashButton);
expect(deleteComponent).toHaveBeenCalledTimes(1);

View File

@@ -293,6 +293,8 @@ const Row = memo((props: RowProps) => {
<DeleteComponentButton onDelete={handleDeleteComponent} />
<IconButton
onClick={() => handleChangeFocus(true)}
label={t('Row settings')}
hideVisibleLabel
icon={<Icons.SettingOutlined iconSize="l" />}
/>
</HoverMenu>