From 764a604cb67f65e124ea7d79d319a774277e7301 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 3 Nov 2025 14:01:16 +0200 Subject: [PATCH 1/6] [@elastic/eslint-plugin-eui] added new `require-table-caption` rule. --- packages/eslint-plugin/README.md | 4 + .../eslint-plugin/changelogs/upcoming/_.md | 1 + packages/eslint-plugin/src/index.ts | 3 + .../rules/a11y/require_table_caption.test.ts | 80 +++++++++++++++++++ .../src/rules/a11y/require_table_caption.ts | 64 +++++++++++++++ 5 files changed, 152 insertions(+) create mode 100644 packages/eslint-plugin/changelogs/upcoming/_.md create mode 100644 packages/eslint-plugin/src/rules/a11y/require_table_caption.test.ts create mode 100644 packages/eslint-plugin/src/rules/a11y/require_table_caption.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 608991c2173..336aa04ccd6 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -162,6 +162,10 @@ Ensure `EuiTooltip` components are anchored to elements that can receive keyboar ### `@elastic/eui/accessible-interactive-element` Ensure interactive EUI components (like e.g. `EuiLink`, `EuiButton`, `EuiRadio`) remain accessible by prohibiting `tabIndex={-1}`, which removes them from keyboard navigation. +### `@elastic/eui/require-table-caption` +Ensure `EuiInMemoryTable`, `EuiBasicTable` have a `tableCaption` property for accessibility. + + ## Testing ### Running unit tests diff --git a/packages/eslint-plugin/changelogs/upcoming/_.md b/packages/eslint-plugin/changelogs/upcoming/_.md new file mode 100644 index 00000000000..ea476b2c67f --- /dev/null +++ b/packages/eslint-plugin/changelogs/upcoming/_.md @@ -0,0 +1 @@ +- Added new `require-table-caption` rule. diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts index 34ecddb5451..168704d760d 100644 --- a/packages/eslint-plugin/src/index.ts +++ b/packages/eslint-plugin/src/index.ts @@ -20,6 +20,7 @@ import { NoUnnamedInteractiveElement } from './rules/a11y/no_unnamed_interactive import { TooltipFocusableAnchor } from './rules/a11y/tooltip_focusable_anchor'; import { CallOutAnnounceOnMount } from './rules/a11y/callout_announce_on_mount'; import { AccessibleInteractiveElements } from './rules/a11y/accessible_interactive_element'; +import { RequireTableCaption } from './rules/a11y/require_table_caption'; const config = { rules: { @@ -35,6 +36,7 @@ const config = { 'no-unnamed-interactive-element': NoUnnamedInteractiveElement, 'tooltip-focusable-anchor': TooltipFocusableAnchor, 'accessible-interactive-element': AccessibleInteractiveElements, + 'require-table-caption': RequireTableCaption, }, configs: { recommended: { @@ -52,6 +54,7 @@ const config = { '@elastic/eui/no-unnamed-interactive-element': 'warn', '@elastic/eui/tooltip-focusable-anchor': 'warn', '@elastic/eui/accessible-interactive-element': 'warn', + '@elastic/eui/require-table-caption': 'warn', }, }, }, diff --git a/packages/eslint-plugin/src/rules/a11y/require_table_caption.test.ts b/packages/eslint-plugin/src/rules/a11y/require_table_caption.test.ts new file mode 100644 index 00000000000..7c3236f71d5 --- /dev/null +++ b/packages/eslint-plugin/src/rules/a11y/require_table_caption.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import dedent from 'dedent'; +import { RuleTester } from '@typescript-eslint/rule-tester'; +import { RequireTableCaption } from './require_table_caption'; + +const languageOptions = { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, +}; + +const ruleTester = new RuleTester(); + +ruleTester.run('require-table-caption', RequireTableCaption, { + valid: [ + { + code: dedent` + const MyComponent = () => ( + + ) + `, + languageOptions, + }, + { + code: dedent` + const MyComponent = () => ( + + ) + `, + languageOptions, + }, + { + code: dedent` + const MyComponent = () => ( + Should not affect other components
+ ) + `, + languageOptions, + }, + ], + invalid: [ + { + code: dedent` + const MyComponent = () => ( + + ); + `, + languageOptions, + errors: [ + { + messageId: 'missingTableCaption', + data: { component: 'EuiBasicTable' }, + }, + ], + }, + { + code: dedent` + const MyComponent = () => ( + + ) + `, + languageOptions, + errors: [ + { + messageId: 'missingTableCaption', + data: { component: 'EuiInMemoryTable' }, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts b/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts new file mode 100644 index 00000000000..0f38ab8e553 --- /dev/null +++ b/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils'; + +const TABLE_COMPONENTS = ['EuiInMemoryTable', 'EuiBasicTable']; +const TABLE_CAPTION_PROP = 'tableCaption'; + +export const RequireTableCaption = ESLintUtils.RuleCreator.withoutDocs({ + create(context) { + return { + JSXOpeningElement(node: TSESTree.JSXOpeningElement) { + if (node.name.type !== 'JSXIdentifier') { + return; + } + + const component = node.name.name; + + if (!TABLE_COMPONENTS.includes(component)) { + return; + } + + const hasTableCaption = node.attributes.some( + (attr) => + attr.type === 'JSXAttribute' && + attr.name.type === 'JSXIdentifier' && + attr.name.name === TABLE_CAPTION_PROP + ); + + if (!hasTableCaption) { + context.report({ + node, + messageId: 'missingTableCaption', + data: { component } + }); + } + }, + }; + }, + defaultOptions: [], + meta: { + type: 'problem', + docs: { + description: `Ensure ${TABLE_COMPONENTS.join(' and ')} have a \`${TABLE_CAPTION_PROP}\` prop for accessibility.`, + }, + schema: [], + messages: { + missingTableCaption: [ + '{{component}} must include a `tableCaption` prop for accessibility.', + '', + 'Example:', + ' <{{component}} tableCaption="Descriptive caption for the table" ... />', + ].join('\n'), + }, + }, +}); + + +console.log(`Ensure ${TABLE_COMPONENTS.join(' and ')} have a \`${TABLE_CAPTION_PROP}\` prop for accessibility.`) From 528f88a88b5c8506d130b3c0464f9be96cd681b8 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 3 Nov 2025 14:08:52 +0200 Subject: [PATCH 2/6] changelog --- packages/eslint-plugin/changelogs/upcoming/{_.md => 9168.md} | 0 packages/eslint-plugin/src/rules/a11y/require_table_caption.ts | 3 --- 2 files changed, 3 deletions(-) rename packages/eslint-plugin/changelogs/upcoming/{_.md => 9168.md} (100%) diff --git a/packages/eslint-plugin/changelogs/upcoming/_.md b/packages/eslint-plugin/changelogs/upcoming/9168.md similarity index 100% rename from packages/eslint-plugin/changelogs/upcoming/_.md rename to packages/eslint-plugin/changelogs/upcoming/9168.md diff --git a/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts b/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts index 0f38ab8e553..dbcdd7fa4e9 100644 --- a/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts +++ b/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts @@ -59,6 +59,3 @@ export const RequireTableCaption = ESLintUtils.RuleCreator.withoutDocs({ }, }, }); - - -console.log(`Ensure ${TABLE_COMPONENTS.join(' and ')} have a \`${TABLE_CAPTION_PROP}\` prop for accessibility.`) From b6a25d42a1fa7ed5b1a9ea71ba0249a02898462c Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 3 Nov 2025 14:14:07 +0200 Subject: [PATCH 3/6] cleanup --- packages/eslint-plugin/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 336aa04ccd6..dbdd667a0e1 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -165,7 +165,6 @@ Ensure interactive EUI components (like e.g. `EuiLink`, `EuiButton`, `EuiRadio`) ### `@elastic/eui/require-table-caption` Ensure `EuiInMemoryTable`, `EuiBasicTable` have a `tableCaption` property for accessibility. - ## Testing ### Running unit tests From 53c81e76b65b87b6baa210d8948aad1598bda033 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 3 Nov 2025 14:15:55 +0200 Subject: [PATCH 4/6] Update require_table_caption.ts --- packages/eslint-plugin/src/rules/a11y/require_table_caption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts b/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts index dbcdd7fa4e9..564153df75c 100644 --- a/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts +++ b/packages/eslint-plugin/src/rules/a11y/require_table_caption.ts @@ -46,7 +46,7 @@ export const RequireTableCaption = ESLintUtils.RuleCreator.withoutDocs({ meta: { type: 'problem', docs: { - description: `Ensure ${TABLE_COMPONENTS.join(' and ')} have a \`${TABLE_CAPTION_PROP}\` prop for accessibility.`, + description: `Ensure ${TABLE_COMPONENTS.join(', ')} have a \`${TABLE_CAPTION_PROP}\` prop for accessibility.`, }, schema: [], messages: { From 3118684bff6e3c51150474095289f7ba55b126f3 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 3 Nov 2025 20:27:09 +0200 Subject: [PATCH 5/6] Update packages/eslint-plugin/README.md Co-authored-by: Weronika Olejniczak <32842468+weronikaolejniczak@users.noreply.github.com> --- packages/eslint-plugin/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index dbdd667a0e1..8f739ef990e 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -160,11 +160,12 @@ Ensure that appropriate aria-attributes are set for `EuiBetaBadge`, `EuiButtonIc Ensure `EuiTooltip` components are anchored to elements that can receive keyboard focus, making them accessible to all users. When using non-interactive elements (like `span`or `EuiText`) as tooltip anchors, they must include `tabIndex={0}` to be keyboard-focusable. For better accessibility, prefer using semantic interactive components (like `EuiButton` or `EuiLink`) which are focusable by default. ### `@elastic/eui/accessible-interactive-element` + Ensure interactive EUI components (like e.g. `EuiLink`, `EuiButton`, `EuiRadio`) remain accessible by prohibiting `tabIndex={-1}`, which removes them from keyboard navigation. ### `@elastic/eui/require-table-caption` -Ensure `EuiInMemoryTable`, `EuiBasicTable` have a `tableCaption` property for accessibility. +Ensure `EuiInMemoryTable`, `EuiBasicTable` have a `tableCaption` property for accessibility. ## Testing ### Running unit tests From 19accd553acdca7dc7b59815aafc9b2229593aee Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 4 Nov 2025 13:53:19 +0200 Subject: [PATCH 6/6] Update packages/eslint-plugin/README.md Co-authored-by: Weronika Olejniczak <32842468+weronikaolejniczak@users.noreply.github.com> --- packages/eslint-plugin/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 8f739ef990e..476155c4931 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -166,6 +166,7 @@ Ensure interactive EUI components (like e.g. `EuiLink`, `EuiButton`, `EuiRadio`) ### `@elastic/eui/require-table-caption` Ensure `EuiInMemoryTable`, `EuiBasicTable` have a `tableCaption` property for accessibility. + ## Testing ### Running unit tests