You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2026/07/31 22:05:39 UTC

(superset) branch master updated: fix(a11y): add aria-label to dashboard IconButton and unlabeled call sites (#41470)

This is an automated email from the ASF dual-hosted git repository.

eschutho pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new b452c1634dc fix(a11y): add aria-label to dashboard IconButton and unlabeled call sites (#41470)
b452c1634dc is described below

commit b452c1634dcacddf01466625a608a0bf7260ca7d
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Fri Jul 31 15:05:32 2026 -0700

    fix(a11y): add aria-label to dashboard IconButton and unlabeled call sites (#41470)
    
    Co-authored-by: Claude Sonnet 4.6 <no...@anthropic.com>
---
 ...ntButton.tsx => DeleteComponentButton.test.tsx} | 34 ++++++++++------------
 .../dashboard/components/DeleteComponentButton.tsx |  3 ++
 .../src/dashboard/components/IconButton.test.tsx   | 14 +++++++++
 .../src/dashboard/components/IconButton.tsx        |  5 +++-
 .../gridComponents/Column/Column.test.tsx          | 11 ++++++-
 .../components/gridComponents/Column/Column.tsx    |  2 ++
 .../gridComponents/Header/Header.test.tsx          |  4 ++-
 .../components/gridComponents/Row/Row.test.tsx     |  9 ++++++
 .../components/gridComponents/Row/Row.tsx          |  2 ++
 9 files changed, 63 insertions(+), 21 deletions(-)

diff --git a/superset-frontend/src/dashboard/components/DeleteComponentButton.tsx b/superset-frontend/src/dashboard/components/DeleteComponentButton.test.tsx
similarity index 53%
copy from superset-frontend/src/dashboard/components/DeleteComponentButton.tsx
copy to superset-frontend/src/dashboard/components/DeleteComponentButton.test.tsx
index 37c24a180f3..519e31cdba7 100644
--- a/superset-frontend/src/dashboard/components/DeleteComponentButton.tsx
+++ b/superset-frontend/src/dashboard/components/DeleteComponentButton.test.tsx
@@ -16,25 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { MouseEventHandler, FC } from 'react';
+import { render, screen, fireEvent } from 'spec/helpers/testing-library';
+import DeleteComponentButton from './DeleteComponentButton';
 
-import { Icons } from '@superset-ui/core/components/Icons';
-import type { IconType } from '@superset-ui/core/components/Icons/types';
-import IconButton from './IconButton';
+test('exposes an accessible name without rendering visible label text', () => {
+  render(<DeleteComponentButton onDelete={jest.fn()} />);
 
-type DeleteComponentButtonProps = {
-  onDelete: MouseEventHandler<HTMLButtonElement>;
-  iconSize?: IconType['iconSize'];
-};
+  expect(
+    screen.getByRole('button', { name: 'Delete component' }),
+  ).toBeInTheDocument();
+  expect(screen.queryByText('Delete component')).not.toBeInTheDocument();
+});
 
-const DeleteComponentButton: FC<DeleteComponentButtonProps> = ({
-  onDelete,
-  iconSize,
-}) => (
-  <IconButton
-    onClick={onDelete}
-    icon={<Icons.DeleteOutlined iconSize={iconSize ?? 'l'} />}
-  />
-);
+test('calls onDelete when clicked', () => {
+  const onDelete = jest.fn();
+  render(<DeleteComponentButton onDelete={onDelete} />);
 
-export default DeleteComponentButton;
+  fireEvent.click(screen.getByRole('button', { name: 'Delete component' }));
+
+  expect(onDelete).toHaveBeenCalledTimes(1);
+});
diff --git a/superset-frontend/src/dashboard/components/DeleteComponentButton.tsx b/superset-frontend/src/dashboard/components/DeleteComponentButton.tsx
index 37c24a180f3..65a440b10d3 100644
--- a/superset-frontend/src/dashboard/components/DeleteComponentButton.tsx
+++ b/superset-frontend/src/dashboard/components/DeleteComponentButton.tsx
@@ -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'} />}
   />
 );
diff --git a/superset-frontend/src/dashboard/components/IconButton.test.tsx b/superset-frontend/src/dashboard/components/IconButton.test.tsx
index 289cafd4a74..a021e63a275 100644
--- a/superset-frontend/src/dashboard/components/IconButton.test.tsx
+++ b/superset-frontend/src/dashboard/components/IconButton.test.tsx
@@ -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();
+});
diff --git a/superset-frontend/src/dashboard/components/IconButton.tsx b/superset-frontend/src/dashboard/components/IconButton.tsx
index 3e48730cff1..8e5b842508d 100644
--- a/superset-frontend/src/dashboard/components/IconButton.tsx
+++ b/superset-frontend/src/dashboard/components/IconButton.tsx
@@ -22,6 +22,7 @@ import { styled, SupersetTheme } from '@apache-superset/core/theme';
 interface IconButtonProps extends HTMLAttributes<HTMLButtonElement> {
   icon: JSX.Element;
   label?: string;
+  hideVisibleLabel?: boolean;
   onClick: MouseEventHandler<HTMLButtonElement>;
   disabled?: boolean;
   'data-test'?: string;
@@ -63,6 +64,7 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
     {
       icon,
       label,
+      hideVisibleLabel,
       onClick,
       onKeyDown,
       disabled,
@@ -75,6 +77,7 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
       {...rest}
       ref={ref}
       type="button"
+      aria-label={label}
       isDisabled={disabled}
       aria-disabled={disabled}
       data-test={dataTest}
@@ -91,7 +94,7 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
       }}
     >
       {icon}
-      {label && <StyledSpan>{label}</StyledSpan>}
+      {label && !hideVisibleLabel && <StyledSpan>{label}</StyledSpan>}
     </StyledButton>
   ),
 );
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Column/Column.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Column/Column.test.tsx
index c402942fe63..278548d213c 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Column/Column.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Column/Column.test.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import React from 'react';
-import { fireEvent, render } from 'spec/helpers/testing-library';
+import { fireEvent, render, screen } from 'spec/helpers/testing-library';
 
 import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundStyleDropdown';
 import IconButton from 'src/dashboard/components/IconButton';
@@ -200,6 +200,15 @@ test('should call deleteComponent when deleted', () => {
   expect(deleteComponent).toHaveBeenCalledTimes(1);
 });
 
+test('settings IconButton exposes an accessible name without visible label text', () => {
+  setup({ component: columnWithoutChildren, editMode: true });
+
+  expect(
+    screen.getByRole('button', { name: 'Column settings' }),
+  ).toBeInTheDocument();
+  expect(screen.queryByText('Column settings')).not.toBeInTheDocument();
+});
+
 test('should pass its own width as availableColumnCount to children', () => {
   const { getByTestId } = setup();
   expect(getByTestId('mock-dashboard-component')).toHaveTextContent(
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Column/Column.tsx b/superset-frontend/src/dashboard/components/gridComponents/Column/Column.tsx
index 72be4b59a67..edb36d1da85 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Column/Column.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Column/Column.tsx
@@ -247,6 +247,8 @@ const Column = (props: ColumnProps) => {
               />
               <IconButton
                 onClick={() => handleChangeFocus(true)}
+                label={t('Column settings')}
+                hideVisibleLabel
                 icon={<Icons.SettingOutlined iconSize="m" />}
               />
             </HoverMenu>
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.tsx
index 5c13f038f03..f51d74d14c0 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.tsx
@@ -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);
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx
index 69635012d20..39b2b718ec6 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx
@@ -240,6 +240,15 @@ test('should call deleteComponent when deleted', () => {
   expect(deleteComponent).toHaveBeenCalledTimes(1);
 });
 
+test('settings IconButton exposes an accessible name without visible label text', () => {
+  setup({ component: rowWithoutChildren, editMode: true });
+
+  expect(
+    screen.getByRole('button', { name: 'Row settings' }),
+  ).toBeInTheDocument();
+  expect(screen.queryByText('Row settings')).not.toBeInTheDocument();
+});
+
 test('should pass appropriate availableColumnCount to children', () => {
   const { getByTestId } = setup();
   expect(getByTestId('mock-dashboard-component')).toHaveTextContent(
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx
index c5ec0c497f5..b644a65fef8 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx
@@ -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>