This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch chore/enable-jsx-a11y-rules in repository https://gitbox.apache.org/repos/asf/superset.git
commit 64058e0b124070ac270f27d7897238968d95fac4 Author: Evan Rusackas <[email protected]> AuthorDate: Mon Jul 13 10:33:37 2026 -0700 chore(a11y): enable 9 more jsx-a11y oxlint rules as errors Turns on the next batch of jsx-a11y rules in oxlint.json (set to "error") and fixes the violations they surface: - interactive-supports-focus: add tabIndex to role-bearing clickable elements across 13 files - mouse-events-have-key-events: mirror onMouseOver/onMouseOut with onFocus/onBlur (Modal drag handle, two popover titles) - control-has-associated-label: add aria-label to the collapsed-sidebar toggle and a test mock button - no-noninteractive-element-to-interactive-role: enabled; the pivot table intentionally keeps role="grid" (asserted by tableRenders.test), so it gets a scoped override - anchor-ambiguous-text, autocomplete-valid, no-aria-hidden-on-focusable, no-interactive-element-to-noninteractive-role, no-noninteractive-element-interactions: already clean, enabled to guard against regressions Also drops the stale "not implemented" comment for the four interactive rules oxlint 1.72 now supports. no-static-element-interactions is left off with a TODO: its remaining violations are event-boundary wrappers and test scaffolding where adding a role is semantically wrong. Co-Authored-By: Claude Opus 4.8 <[email protected]> --- superset-frontend/oxlint.json | 28 ++++++++++++++++++---- .../src/components/FaveStar/index.tsx | 1 + .../src/components/Modal/Modal.tsx | 2 ++ .../src/components/RefreshLabel/index.tsx | 1 + .../src/DeckGLContainer.test.tsx | 1 + .../TableExploreTree/TreeNodeRenderer.tsx | 2 ++ .../Chart/DrillDetail/DrillDetailTableControls.tsx | 1 + .../ScopingModal/ChartsScopingListPanel.tsx | 1 + .../nativeFilters/FilterBar/Vertical.tsx | 1 + .../FiltersConfigModal/FilterTitleContainer.tsx | 1 + .../FiltersConfigForm/DependencyList.tsx | 2 +- .../FiltersConfigModal/ItemTitleContainer.tsx | 1 + .../components/DataTableControls.tsx | 1 + .../components/ExploreViewContainer/index.tsx | 1 + .../DndColumnSelectPopoverTitle.tsx | 1 + .../controls/DndColumnSelectControl/Option.tsx | 1 + .../MetricControl/AdhocMetricEditPopoverTitle.tsx | 1 + .../components/controls/OptionControls/index.tsx | 1 + .../src/features/queries/QueryPreviewModal.tsx | 2 ++ 19 files changed, 44 insertions(+), 6 deletions(-) diff --git a/superset-frontend/oxlint.json b/superset-frontend/oxlint.json index 27563d81c5e..1516d96689c 100644 --- a/superset-frontend/oxlint.json +++ b/superset-frontend/oxlint.json @@ -52,10 +52,6 @@ // require-default-props, sort-comp, static-property-placement // (prefer-stateless-function / function-component-definition // are represented by react/prefer-function-component below) - // jsx-a11y: interactive-supports-focus, - // no-interactive-element-to-noninteractive-role, - // no-noninteractive-element-interactions, - // no-noninteractive-element-to-interactive-role // typescript: naming-convention // unicorn: prevent-abbreviations @@ -196,6 +192,7 @@ // === JSX-a11y rules === "jsx-a11y/alt-text": "error", + "jsx-a11y/anchor-ambiguous-text": "error", "jsx-a11y/anchor-has-content": "error", "jsx-a11y/anchor-is-valid": "error", "jsx-a11y/aria-activedescendant-has-tabindex": "error", @@ -203,20 +200,31 @@ "jsx-a11y/aria-proptypes": "error", "jsx-a11y/aria-role": ["error", { "ignoreNonDOM": false }], "jsx-a11y/aria-unsupported-elements": "error", + "jsx-a11y/autocomplete-valid": "error", "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/control-has-associated-label": "error", "jsx-a11y/heading-has-content": "error", "jsx-a11y/html-has-lang": "error", "jsx-a11y/iframe-has-title": "error", "jsx-a11y/img-redundant-alt": "error", + "jsx-a11y/interactive-supports-focus": "error", "jsx-a11y/label-has-associated-control": "error", "jsx-a11y/lang": "error", "jsx-a11y/media-has-caption": "error", - "jsx-a11y/mouse-events-have-key-events": "off", + "jsx-a11y/mouse-events-have-key-events": "error", "jsx-a11y/no-access-key": "error", + "jsx-a11y/no-aria-hidden-on-focusable": "error", "jsx-a11y/no-autofocus": ["error", { "ignoreNonDOM": true }], "jsx-a11y/no-distracting-elements": "error", + "jsx-a11y/no-interactive-element-to-noninteractive-role": "error", + "jsx-a11y/no-noninteractive-element-interactions": "error", + "jsx-a11y/no-noninteractive-element-to-interactive-role": "error", "jsx-a11y/no-noninteractive-tabindex": "error", "jsx-a11y/no-redundant-roles": "error", + // TODO: Graduate to "error" — remaining violations are event-boundary + // wrappers (divs/spans whose handlers only stopPropagation) and test + // scaffolding where adding a role is semantically wrong. Needs a + // dedicated pass; see PR enabling the other jsx-a11y interactive rules. "jsx-a11y/no-static-element-interactions": "off", "jsx-a11y/role-has-required-aria-props": "error", "jsx-a11y/role-supports-aria-props": "error", @@ -289,6 +297,16 @@ "rules": { "jsx-a11y/no-redundant-roles": "off" } + }, + { + // The pivot table intentionally exposes `role="grid"` on its <table> + // for interactive grid semantics (asserted by tableRenders.test.tsx). + "files": [ + "plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx" + ], + "rules": { + "jsx-a11y/no-noninteractive-element-to-interactive-role": "off" + } } ], "ignorePatterns": [ diff --git a/superset-frontend/packages/superset-ui-core/src/components/FaveStar/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/FaveStar/index.tsx index 308e44e4630..fbfb9c5e4a1 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/FaveStar/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/FaveStar/index.tsx @@ -60,6 +60,7 @@ export const FaveStar = ({ className="fave-unfave-icon" data-test="fave-unfave-icon" role="button" + tabIndex={0} > {isStarred ? ( <Icons.StarFilled diff --git a/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.tsx b/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.tsx index f8768f29c35..6c00d8884a5 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.tsx @@ -344,6 +344,8 @@ const CustomModal = ({ className="draggable-trigger" onMouseOver={() => dragDisabled && setDragDisabled(false)} onMouseOut={() => !dragDisabled && setDragDisabled(true)} + onFocus={() => dragDisabled && setDragDisabled(false)} + onBlur={() => !dragDisabled && setDragDisabled(true)} > {title} </div> diff --git a/superset-frontend/packages/superset-ui-core/src/components/RefreshLabel/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/RefreshLabel/index.tsx index 1c01ca63acd..c5c162a5098 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/RefreshLabel/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/RefreshLabel/index.tsx @@ -36,6 +36,7 @@ const RefreshLabel = ({ <Icons.SyncOutlined iconSize="l" role="button" + tabIndex={disabled ? -1 : 0} onClick={disabled ? undefined : onClick} css={(theme: SupersetTheme) => ({ cursor: 'pointer', diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/DeckGLContainer.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/DeckGLContainer.test.tsx index 21c30ef8a37..b035d2ee87d 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/DeckGLContainer.test.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/DeckGLContainer.test.tsx @@ -42,6 +42,7 @@ jest.mock('react-map-gl/maplibre', () => ({ <div data-test="maplibre-map" data-map-style={JSON.stringify(mapStyle)}> <button type="button" + aria-label="move map" data-test="maplibre-move" onClick={() => onMove({ viewState: { longitude: 1, latitude: 2, zoom: 3 } }) diff --git a/superset-frontend/src/SqlLab/components/TableExploreTree/TreeNodeRenderer.tsx b/superset-frontend/src/SqlLab/components/TableExploreTree/TreeNodeRenderer.tsx index 5e0eb6c60d2..3cb934d1b80 100644 --- a/superset-frontend/src/SqlLab/components/TableExploreTree/TreeNodeRenderer.tsx +++ b/superset-frontend/src/SqlLab/components/TableExploreTree/TreeNodeRenderer.tsx @@ -239,6 +239,7 @@ const TreeNodeRenderer: React.FC<TreeNodeRendererProps> = ({ <div className="side-action-container" role="menu" + tabIndex={0} onClick={e => e.stopPropagation()} > {pinnedSchemas.has(schema) && ( @@ -307,6 +308,7 @@ const TreeNodeRenderer: React.FC<TreeNodeRendererProps> = ({ <div className="side-action-container" role="menu" + tabIndex={0} onClick={e => e.stopPropagation()} > {isPinned && ( diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx index 17de19f8b79..929f863c640 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx @@ -163,6 +163,7 @@ export default function TableControls({ iconSize="l" aria-label={t('Reload')} role="button" + tabIndex={0} onClick={onReload} /> </Tooltip> diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ChartsScopingListPanel.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ChartsScopingListPanel.tsx index 9775de8022e..05a34f8a0ee 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ChartsScopingListPanel.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ChartsScopingListPanel.tsx @@ -142,6 +142,7 @@ export const ChartsScopingListPanel = ({ </AddButtonContainer> <FilterTitle role="button" + tabIndex={0} onClick={() => setCurrentChartId(undefined)} className={activeChartId === undefined ? 'active' : ''} > diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx index f133db24a25..17d457f81d5 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -266,6 +266,7 @@ const VerticalFilterBar: FC<VerticalBarProps> = ({ className={cx({ open: !filtersOpen })} onClick={openFiltersBar} role="button" + tabIndex={0} offset={offset} > <Icons.VerticalAlignTopOutlined diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx index 19d7427917b..e5420ad1b63 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx @@ -166,6 +166,7 @@ const FilterTitleContainer = forwardRef<HTMLDivElement, Props>( <FilterTitle role="tab" key={`filter-title-tab-${id}`} + tabIndex={0} onClick={() => onChange(id)} className={classNames.join(' ')} aria-selected={isActive} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DependencyList.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DependencyList.tsx index 072ba9ba87f..52c6952e062 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DependencyList.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DependencyList.tsx @@ -175,7 +175,7 @@ const List = ({ /> ))} {availableFilters.length > rows.length && ( - <AddFilter role="button" onClick={onAdd}> + <AddFilter role="button" tabIndex={0} onClick={onAdd}> <Icons.PlusOutlined iconSize="xs" /> {t('Add filter')} </AddFilter> diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/ItemTitleContainer.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/ItemTitleContainer.tsx index 78b9186dca6..388421b5ac1 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/ItemTitleContainer.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/ItemTitleContainer.tsx @@ -139,6 +139,7 @@ const ItemTitleContainer = forwardRef<HTMLDivElement, Props>( <ItemTitle role="tab" key={`item-title-tab-${id}`} + tabIndex={0} onClick={() => onChange(id)} className={classNames.join(' ')} aria-selected={isActive} diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx index 8ca46c6f136..abae9473796 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx @@ -140,6 +140,7 @@ export const TableControls = ({ iconSize="l" aria-label={t('Reload')} role="button" + tabIndex={0} onClick={onReload} /> </Tooltip> diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx index faaacd102f5..aebaa64da17 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx @@ -1026,6 +1026,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) { data-test="open-datasource-tab" role="button" tabIndex={0} + aria-label={t('Open Datasource tab')} > <span role="button" tabIndex={0} className="action-button"> <Tooltip title={t('Open Datasource tab')}> diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx index dcc01245432..05c3cd3744c 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx @@ -94,6 +94,7 @@ export const DndColumnSelectPopoverTitle = ({ data-test="AdhocMetricEditTitle#trigger" onMouseOver={onMouseOver} onMouseOut={onMouseOut} + onFocus={onMouseOver} onClick={onClick} onBlur={onBlur} role="button" diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx index 6ab8ef322d4..2f8b733960e 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx @@ -58,6 +58,7 @@ export default function Option({ text-align: center; `} role="button" + tabIndex={0} data-test="remove-control-button" onClick={onClickClose} > diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx index 2e52b23ebee..4483feb5a3a 100644 --- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx +++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx @@ -115,6 +115,7 @@ const AdhocMetricEditPopoverTitle: FC<AdhocMetricEditPopoverTitleProps> = ({ data-test="AdhocMetricEditTitle#trigger" onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} + onFocus={handleMouseOver} onClick={handleClick} onBlur={handleBlur} role="button" diff --git a/superset-frontend/src/explore/components/controls/OptionControls/index.tsx b/superset-frontend/src/explore/components/controls/OptionControls/index.tsx index 97c414c37bd..a8f65bd0ddb 100644 --- a/superset-frontend/src/explore/components/controls/OptionControls/index.tsx +++ b/superset-frontend/src/explore/components/controls/OptionControls/index.tsx @@ -350,6 +350,7 @@ export const OptionControlLabel = ({ role="button" data-test="remove-control-button" onClick={onRemove} + tabIndex={0} > <Icons.CloseOutlined iconSize="m" diff --git a/superset-frontend/src/features/queries/QueryPreviewModal.tsx b/superset-frontend/src/features/queries/QueryPreviewModal.tsx index 823aae806c5..1001478ae57 100644 --- a/superset-frontend/src/features/queries/QueryPreviewModal.tsx +++ b/superset-frontend/src/features/queries/QueryPreviewModal.tsx @@ -148,6 +148,7 @@ function QueryPreviewModal({ <QueryViewToggle> <TabButton role="button" + tabIndex={0} data-test="toggle-user-sql" className={cx({ active: currentTab === 'user' })} onClick={() => setCurrentTab('user')} @@ -156,6 +157,7 @@ function QueryPreviewModal({ </TabButton> <TabButton role="button" + tabIndex={0} data-test="toggle-executed-sql" className={cx({ active: currentTab === 'executed' })} onClick={() => setCurrentTab('executed')}
