From b0a66f4063b45d3e67e1c16a2beabbacbf0f0480 Mon Sep 17 00:00:00 2001 From: Arturo Castillo Delgado Date: Thu, 23 Oct 2025 12:23:11 +0200 Subject: [PATCH 1/5] [EuiBasicTable][EuiInMemoryTable] Test tooltip screen-reader output in action button when `name` and `description` are the same, the tooltip should be visual-only, to avoid the same text being announced twice --- .../collapsed_item_actions.test.tsx | 51 ++++++++++++++++++ .../basic_table/default_item_action.test.tsx | 53 +++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/packages/eui/src/components/basic_table/collapsed_item_actions.test.tsx b/packages/eui/src/components/basic_table/collapsed_item_actions.test.tsx index b1b93636e39..7d95c6d6b4c 100644 --- a/packages/eui/src/components/basic_table/collapsed_item_actions.test.tsx +++ b/packages/eui/src/components/basic_table/collapsed_item_actions.test.tsx @@ -13,6 +13,7 @@ import { waitForEuiPopoverOpen, waitForEuiPopoverClose, waitForEuiToolTipVisible, + waitForEuiToolTipHidden, } from '../../test/rtl'; import { CollapsedItemActions } from './collapsed_item_actions'; @@ -156,4 +157,54 @@ describe('CollapsedItemActions', () => { fireEvent.click(getByTestSubject('customAction')); await waitForEuiPopoverClose(); }); + + // If `name` and `description` are exactly the same + // we don't want screen readers announcing the same text twice + test('tooltip screen-reader output', async () => { + const props = { + actions: [ + { + name: 'same', + description: 'different', + onClick: () => {}, + 'data-test-subj': 'different', + }, + { + name: 'same', + description: 'same', + onClick: () => {}, + 'data-test-subj': 'same', + }, + ], + itemId: 'id', + item: { id: 'abc' }, + actionsDisabled: false, + displayedRowIndex: 0, + }; + + const { getByTestSubject, getByRole } = render( + + ); + fireEvent.click(getByTestSubject('euiCollapsedItemActionsButton')); + await waitForEuiPopoverOpen(); + + const actionDifferent = getByTestSubject('different'); + fireEvent.mouseOver(actionDifferent); + await waitForEuiToolTipVisible(); + const tooltipDifferent = getByRole('tooltip'); + expect(tooltipDifferent).toHaveTextContent('different'); + expect(actionDifferent).toHaveAttribute('aria-describedby'); + expect(actionDifferent.getAttribute('aria-describedby')).toEqual( + tooltipDifferent.id + ); + fireEvent.mouseOut(actionDifferent); + await waitForEuiToolTipHidden(); + + const actionSame = getByTestSubject('same'); + fireEvent.mouseOver(actionSame); + await waitForEuiToolTipVisible(); + const tooltipSame = getByRole('tooltip'); + expect(tooltipSame).toHaveTextContent('same'); + expect(actionSame).not.toHaveAttribute('aria-describedby'); + }); }); diff --git a/packages/eui/src/components/basic_table/default_item_action.test.tsx b/packages/eui/src/components/basic_table/default_item_action.test.tsx index 6514004e1b3..9ad93e49ef3 100644 --- a/packages/eui/src/components/basic_table/default_item_action.test.tsx +++ b/packages/eui/src/components/basic_table/default_item_action.test.tsx @@ -123,6 +123,59 @@ describe('DefaultItemAction', () => { expect(getByText('goodbye tooltip')).toBeInTheDocument(); }); + it('is described by the tooltip via aria-describedby', async () => { + const actionWithDifferentNameAndDescription: EmptyButtonAction = { + name: 'same', + description: 'different', + 'data-test-subj': 'different', + type: 'button', + onClick: () => {}, + }; + const { getByTestSubject, getByRole } = render( + + ); + + const action = getByTestSubject('different'); + fireEvent.mouseOver(action); + await waitForEuiToolTipVisible(); + const tooltip = getByRole('tooltip'); + expect(tooltip).toHaveTextContent('different'); + expect(tooltip).toBeInTheDocument(); + expect(action).toHaveAttribute('aria-describedby'); + expect(action.getAttribute('aria-describedby')).toEqual(tooltip.id); + }); + + // If `name` and `description` are exactly the same + // we don't want screen readers announcing the same text twice + it('has visual-only tooltip when `name` equals `description`', async () => { + const actionWithEqualNameAndDescription: EmptyButtonAction = { + name: 'same', + description: 'same', + 'data-test-subj': 'same', + type: 'button', + onClick: () => {}, + }; + const { getByTestSubject, getByRole } = render( + + ); + + const action = getByTestSubject('same'); + fireEvent.mouseOver(action); + await waitForEuiToolTipVisible(); + const tooltip = getByRole('tooltip'); + expect(tooltip).toBeInTheDocument(); + expect(tooltip).toHaveTextContent('same'); + expect(action).not.toHaveAttribute('aria-describedby'); + }); + it('passes back the original click event as well as the row item to onClick', () => { const onClick = jest.fn((item, event) => { event.preventDefault(); From 802f1c9105ab9e626dcc94b380e7b0f24cc6833d Mon Sep 17 00:00:00 2001 From: Arturo Castillo Delgado Date: Thu, 23 Oct 2025 12:26:00 +0200 Subject: [PATCH 2/5] [EuiBasicTable][EuiInMemoryTable] Improve tooltip screen-reader output in action button --- .../basic_table/collapsed_item_actions.tsx | 8 +++++++- .../components/basic_table/default_item_action.tsx | 13 +++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/eui/src/components/basic_table/collapsed_item_actions.tsx b/packages/eui/src/components/basic_table/collapsed_item_actions.tsx index 1a86031b680..9dbc6f8ff2b 100644 --- a/packages/eui/src/components/basic_table/collapsed_item_actions.tsx +++ b/packages/eui/src/components/basic_table/collapsed_item_actions.tsx @@ -110,7 +110,13 @@ export const CollapsedItemActions = ({ if (!event.isPropagationStopped()) closePopover(); }} toolTipContent={toolTipContent} - toolTipProps={{ delay: 'long' }} + toolTipProps={{ + delay: 'long', + // Avoid screen-readers announcing the same text twice + disableScreenReaderOutput: + typeof buttonContent === 'string' && + buttonContent === toolTipContent, + }} > {buttonContent} diff --git a/packages/eui/src/components/basic_table/default_item_action.tsx b/packages/eui/src/components/basic_table/default_item_action.tsx index e026adf6ceb..4b032dcef6e 100644 --- a/packages/eui/src/components/basic_table/default_item_action.tsx +++ b/packages/eui/src/components/basic_table/default_item_action.tsx @@ -14,7 +14,7 @@ import { EuiButtonEmptyProps, EuiButtonIconProps, } from '../button'; -import { EuiToolTip } from '../tool_tip'; +import { EuiToolTip, EuiToolTipProps } from '../tool_tip'; import { useGeneratedHtmlId } from '../../services/accessibility'; import { EuiScreenReaderOnly } from '../accessibility'; @@ -58,6 +58,13 @@ export const DefaultItemAction = ({ : undefined; const actionContent = callWithItemIfFunction(item)(action.name); const tooltipContent = callWithItemIfFunction(item)(action.description); + const tooltipProps: Omit = { + content: tooltipContent, + delay: 'long', + // Avoid screen-readers announcing the same text twice + disableScreenReaderOutput: + typeof actionContent === 'string' && actionContent === tooltipContent, + }; const href = callWithItemIfFunction(item)(action.href); const dataTestSubj = callWithItemIfFunction(item)(action['data-test-subj']); @@ -114,9 +121,7 @@ export const DefaultItemAction = ({ return enabled ? ( <> - - {button} - + {button} {/* SR text has to be rendered outside the tooltip, otherwise EuiToolTip's own aria-labelledby won't properly clone */} {ariaLabelledBy} From f99f1ee7316d3705c00a6fd4dbf74c5ad77465ec Mon Sep 17 00:00:00 2001 From: Arturo Castillo Delgado Date: Thu, 23 Oct 2025 12:39:42 +0200 Subject: [PATCH 3/5] Changelog --- packages/eui/changelogs/upcoming/9140.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/eui/changelogs/upcoming/9140.md diff --git a/packages/eui/changelogs/upcoming/9140.md b/packages/eui/changelogs/upcoming/9140.md new file mode 100644 index 00000000000..bbfbec90259 --- /dev/null +++ b/packages/eui/changelogs/upcoming/9140.md @@ -0,0 +1,4 @@ +**Accessibility** + +- Improved the accessibility experience of `EuiBasicTable` and `EuiInMemoryTable` by ensuring tooltips displayed for action buttons are visual-only whenever the `action.name` and `action.description` are exactly the same + From 667eac029095fb10965b640f4790e7ff9f0b9ab1 Mon Sep 17 00:00:00 2001 From: Arturo Castillo Delgado Date: Wed, 29 Oct 2025 17:08:19 +0100 Subject: [PATCH 4/5] [Docs][Tables] Add accessibility recommendation regarding actions names and descriptions --- .../website/docs/components/tables/basic.mdx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/website/docs/components/tables/basic.mdx b/packages/website/docs/components/tables/basic.mdx index db71030430b..6b2e3fd8e5a 100644 --- a/packages/website/docs/components/tables/basic.mdx +++ b/packages/website/docs/components/tables/basic.mdx @@ -170,6 +170,23 @@ Actions follow these UI/UX rules: - Actions become semi-transparent when you hover over the row. With more than 2 actions, only the ellipsis button stays visible at all times. - When one or more rows are selected, all individual actions are disabled. Users should use bulk actions outside the table instead. +:::accessibility Accessibility recommendation + +Always try to be intentional about the `action.description`. Setting it to something descriptive and different from the `action.name` will result in a better user experience. + +```jsx +const action = { + name: 'User profile', + description: + ({ firstName, lastName }) => `Visit ${firstName} ${lastName}'s profile`, + // ... +}; +``` + +If a meaningful description for an action is not possible, setting the exact same value for `action.name` and `action.description` as a `string` will ensure the button's tooltip is visual-only and the text is not announced to screen-readers twice. + +::: + ```tsx interactive import React, { useState, useMemo } from 'react'; import { From 7433825f689c9bfcad3bf91b59012640a0227926 Mon Sep 17 00:00:00 2001 From: Arturo Castillo Delgado Date: Fri, 31 Oct 2025 12:36:28 +0100 Subject: [PATCH 5/5] [Feedback] Restructure docs content --- packages/website/docs/components/tables/basic.mdx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/website/docs/components/tables/basic.mdx b/packages/website/docs/components/tables/basic.mdx index 6b2e3fd8e5a..adc4737c137 100644 --- a/packages/website/docs/components/tables/basic.mdx +++ b/packages/website/docs/components/tables/basic.mdx @@ -170,9 +170,9 @@ Actions follow these UI/UX rules: - Actions become semi-transparent when you hover over the row. With more than 2 actions, only the ellipsis button stays visible at all times. - When one or more rows are selected, all individual actions are disabled. Users should use bulk actions outside the table instead. -:::accessibility Accessibility recommendation +### Accessibility -Always try to be intentional about the `action.description`. Setting it to something descriptive and different from the `action.name` will result in a better user experience. +`action.name` and `action.description` should be different. The name is a short identifier, while the description should explain the purpose of the action or provide hints. ```jsx const action = { @@ -185,8 +185,6 @@ const action = { If a meaningful description for an action is not possible, setting the exact same value for `action.name` and `action.description` as a `string` will ensure the button's tooltip is visual-only and the text is not announced to screen-readers twice. -::: - ```tsx interactive import React, { useState, useMemo } from 'react'; import {