This is an automated email from the ASF dual-hosted git repository.
rusackas 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 c5131bff47e chore(a11y): enable 9 more jsx-a11y oxlint rules as errors
(#42006)
c5131bff47e is described below
commit c5131bff47eefdee0b92016ffe4afa944cee0e95
Author: Evan Rusackas <[email protected]>
AuthorDate: Mon Jul 13 11:39:13 2026 -0700
chore(a11y): enable 9 more jsx-a11y oxlint rules as errors (#42006)
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 | 6 ++++-
.../components/controls/OptionControls/index.tsx | 1 +
.../src/features/queries/QueryPreviewModal.tsx | 2 ++
19 files changed, 46 insertions(+), 9 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..dc3e83eaefe 100644
---
a/superset-frontend/src/SqlLab/components/TableExploreTree/TreeNodeRenderer.tsx
+++
b/superset-frontend/src/SqlLab/components/TableExploreTree/TreeNodeRenderer.tsx
@@ -238,7 +238,6 @@ const TreeNodeRenderer: React.FC<TreeNodeRendererProps> = ({
{identifier === 'schema' && (
<div
className="side-action-container"
- role="menu"
onClick={e => e.stopPropagation()}
>
{pinnedSchemas.has(schema) && (
@@ -306,7 +305,6 @@ const TreeNodeRenderer: React.FC<TreeNodeRendererProps> = ({
return (
<div
className="side-action-container"
- role="menu"
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..57d5ed8e3c2 100644
---
a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx
+++
b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx
@@ -64,7 +64,10 @@ const AdhocMetricEditPopoverTitle:
FC<AdhocMetricEditPopoverTitleProps> = ({
const handleMouseOver = useCallback(() => setIsHovered(true), []);
const handleMouseOut = useCallback(() => setIsHovered(false), []);
const handleClick = useCallback(() => setIsEditMode(true), []);
- const handleBlur = useCallback(() => setIsEditMode(false), []);
+ const handleBlur = useCallback(() => {
+ setIsHovered(false);
+ setIsEditMode(false);
+ }, []);
const handleKeyPress = useCallback(
(ev: KeyboardEvent<HTMLInputElement>) => {
@@ -115,6 +118,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')}