fix(a11y): WCAG 2.4.6 — add descriptive headings and section labels#39241
fix(a11y): WCAG 2.4.6 — add descriptive headings and section labels#39241Aitema-gmbh wants to merge 3 commits intoapache:masterfrom
Conversation
- Add visually-hidden h1 with dashboard name to Dashboard Header - Add visually-hidden h1 with chart name to Explore/Chart Header - Add visually-hidden h1 to Home page and ChartCreation page - Fix ChartCreation heading hierarchy (h3 → h2) - Add sr-only h2 section headings for Filters and Dashboard Content areas - All headings use clip/sr-only pattern for screen readers only Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review Agent Run #effbd8Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
| {renderDraggableContent} | ||
| </Droppable> | ||
| </StyledHeader> | ||
| <SrOnlyH2>{t('Dashboard content')}</SrOnlyH2> |
There was a problem hiding this comment.
Suggestion: The new filters heading is only rendered in the vertical-filter branch, so dashboards using the horizontal filter bar still expose an unlabeled filter section. Add a screen-reader heading for the horizontal orientation as well so both filter-bar modes have equivalent section labeling. [logic error]
Severity Level: Major ⚠️
- ⚠️ Horizontal filter bar lacks accessible section heading.
- ⚠️ WCAG 2.4.6 compliance inconsistent across filter orientations.
- ⚠️ Screen reader users lose clear filters region label.| <SrOnlyH2>{t('Dashboard content')}</SrOnlyH2> | |
| {showFilterBar && | |
| filterBarOrientation === FilterBarOrientation.Horizontal && ( | |
| <SrOnlyH2>{t('Filters')}</SrOnlyH2> | |
| )} |
Steps of Reproduction ✅
1. Open any dashboard route that renders `DashboardPage` at
`superset-frontend/src/dashboard/containers/DashboardPage.tsx:30-55`, which lazily loads
and renders `<DashboardBuilder />` inside `<DashboardContainer>`.
2. Configure or select a dashboard whose metadata has `filter_bar_orientation` set to
horizontal so that `hydrate.ts` maps it to `FilterBarOrientation.Horizontal` (see
`superset-frontend/src/dashboard/actions/hydrate.ts:22-24`, where `filterBarOrientation`
is populated from `metadata.filter_bar_orientation`).
3. Ensure native filters are enabled and the dashboard is in view mode (not edit mode), so
in `DashboardBuilder` the `showFilterBar` flag at
`superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx:159-171`
is `true` and `filterBarOrientation === FilterBarOrientation.Horizontal`, causing the
horizontal `<FilterBar>` to render in `headerContent` at lines 219-229.
4. Inspect the accessible heading structure with a tool like HeadingsMap or a screen
reader: for horizontal orientation there is no `<SrOnlyH2>Filters</SrOnlyH2>` heading near
the horizontal `<FilterBar>` (the only sr-only heading in this area is `Dashboard content`
at line 667), while the vertical filter bar path at lines 64-85 renders
`<SrOnlyH2>{t('Filters')}</SrOnlyH2>` before the vertical `ResizableSidebar`, resulting in
the filters section being unlabeled only in the horizontal-mode dashboards.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
**Line:** 667:667
**Comment:**
*Logic Error: The new filters heading is only rendered in the vertical-filter branch, so dashboards using the horizontal filter bar still expose an unlabeled filter section. Add a screen-reader heading for the horizontal orientation as well so both filter-bar modes have equivalent section labeling.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.| {isVerticalFilterBarVisible && ( | ||
| <> | ||
| <SrOnlyH2>{t('Filters')}</SrOnlyH2> | ||
| <ResizableSidebar |
There was a problem hiding this comment.
🟠 Architect Review — HIGH
The new sr-only "Filters" section heading is only rendered when the filter bar orientation is vertical; when dashboards use the horizontal filter bar, the filter area has no equivalent heading, so the WCAG 2.4.6 heading/label improvement is applied inconsistently.
Suggestion: Add an equivalent semantic heading for the horizontal filter bar path (e.g., render an sr-only
before the when FilterBarOrientation.Horizontal is active) so both orientations expose a labeled filters section to assistive technologies.
…r bar orientations
Code Review Agent Run #df80d9Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
There was a problem hiding this comment.
Pull request overview
Implements WCAG 2.4.6 (Headings and Labels) improvements in Superset’s main UI surfaces by introducing semantic (mostly screen-reader-only) page/section headings to improve navigability for assistive technologies.
Changes:
- Add screen-reader-only
<h1>headings to key pages (Home, Explore, Dashboard, Chart Creation). - Add screen-reader-only
<h2>section headings for Dashboard areas (Filters, Dashboard content). - Adjust Chart Creation heading markup to support the new heading structure.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| superset-frontend/src/pages/Home/index.tsx | Adds SR-only <h1> “Home” heading. |
| superset-frontend/src/pages/ChartCreation/index.tsx | Adds SR-only <h1> and introduces a visible heading element for the page title. |
| superset-frontend/src/explore/components/ExploreChartHeader/index.tsx | Adds SR-only <h1> derived from slice name / fallback label. |
| superset-frontend/src/dashboard/components/Header/index.tsx | Adds SR-only <h1> for the dashboard title. |
| superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx | Adds SR-only <h2> headings for Filters and Dashboard content areas. |
| const SrOnlyH1 = styled.h1` | ||
| position: absolute; | ||
| width: 1px; | ||
| height: 1px; | ||
| padding: 0; | ||
| margin: -1px; |
There was a problem hiding this comment.
The SrOnlyH1 visually-hidden CSS block is duplicated across several pages/components in this PR. Consider extracting a shared VisuallyHidden/SrOnly component or CSS helper to keep the hiding behavior consistent everywhere.
| const SrOnlyH1 = styled.h1` | ||
| position: absolute; | ||
| width: 1px; | ||
| height: 1px; | ||
| padding: 0; |
There was a problem hiding this comment.
The SrOnlyH1 visually-hidden CSS block is duplicated across several pages/components in this PR. Consider extracting a shared VisuallyHidden/SrOnly component or CSS helper so this pattern is implemented consistently and maintained in one place.
| hidden={isReport} | ||
| /> | ||
| <> | ||
| <SrOnlyH2>{t('Filters')}</SrOnlyH2> |
There was a problem hiding this comment.
SrOnlyH2 is rendered even when the horizontal FilterBar is hidden (hidden={isReport}). This leaves a “Filters” heading in the accessibility tree for report views where the filters UI is not shown. Consider only rendering the heading when the filter bar is actually visible (e.g., gate it on !isReport or the same hidden condition).
| <SrOnlyH2>{t('Filters')}</SrOnlyH2> | |
| {!isReport && <SrOnlyH2>{t('Filters')}</SrOnlyH2>} |
| {isVerticalFilterBarVisible && ( | ||
| <> | ||
| <SrOnlyH2>{t('Filters')}</SrOnlyH2> | ||
| <ResizableSidebar |
There was a problem hiding this comment.
In vertical filter-bar mode this adds an extra “Filters” heading outside the sidebar, while renderChild already renders its own SrOnlyH2. This creates duplicate “Filters” headings, and when isReport hides the actual filter panel the outer heading still remains, which is misleading for screen readers. Suggest removing the outer heading (or gating it with the same hidden condition) and keeping a single heading tied to the visible panel.
| const SrOnlyH2 = styled.h2` | ||
| position: absolute; | ||
| width: 1px; | ||
| height: 1px; | ||
| padding: 0; | ||
| margin: -1px; |
There was a problem hiding this comment.
The SrOnlyH2 visually-hidden CSS block is duplicated across several pages/components in this PR. To avoid future drift (and make future a11y fixes easier), consider extracting a shared VisuallyHidden/SrOnly component or CSS helper and reusing it here instead of redefining the same styles inline.
| const SrOnlyH1 = styled.h1` | ||
| position: absolute; | ||
| width: 1px; | ||
| height: 1px; | ||
| padding: 0; |
There was a problem hiding this comment.
The SrOnlyH1 visually-hidden CSS block is duplicated across several pages/components in this PR. Consider extracting a shared VisuallyHidden/SrOnly component or CSS helper to prevent divergence and reduce repetitive CSS definitions.
| const SrOnlyH1 = styled.h1` | ||
| position: absolute; | ||
| width: 1px; | ||
| height: 1px; | ||
| padding: 0; |
There was a problem hiding this comment.
The SrOnlyH1 visually-hidden CSS block is duplicated across several pages/components in this PR. Consider extracting a shared VisuallyHidden/SrOnly component or CSS helper instead of redefining the same CSS here.
| <SrOnlyH1>{t('Create a new chart')}</SrOnlyH1> | ||
| <h2 aria-hidden="true">{t('Create a new chart')}</h2> |
There was a problem hiding this comment.
This renders the same title twice (a screen-reader-only h1 plus a visible h2 that is aria-hidden). If the intent is a single semantic page heading without altering visuals, consider rendering just one heading element (e.g., make the visible title the h1 and style it to match) to avoid duplicated DOM content and reliance on aria-hidden for visible text.
| <SrOnlyH1>{t('Create a new chart')}</SrOnlyH1> | |
| <h2 aria-hidden="true">{t('Create a new chart')}</h2> | |
| <h1>{t('Create a new chart')}</h1> |
|
Bugs/concerns:
|
…and SkipLink tests Addresses three review polish points on PR apache#39240: - Add `<main>` landmark via role="main" on Layout.Content so the skip target has semantic meaning beyond just an id anchor. - Scope `outline: none` to `[tabindex='-1']:focus` on the same element so the programmatic-focus moment doesn't render a stray browser outline. The rule is intentionally tight: only fires when SkipLink has dynamically attached tabindex='-1', does not affect any other element. - Add `VisuallyHidden` reusable component (with `as` prop for h1, h2, etc.) in the Accessibility folder. This is the shared sr-only primitive that WCAG 2.4.6 (PR apache#39241) needs to deduplicate across pages. Tests added for both SkipLink and VisuallyHidden covering default behavior, custom props, focus management, and tabindex cleanup on blur.
…isuallyHidden Addresses six review concerns on PR apache#39241: - Bug: vertical filter bar rendered "Filters" h2 twice (once outside the ResizableSidebar wrapper, once inside renderChild before StickyPanel). Screen readers announced the heading twice. Removed the outer one and kept the inner one — that's the semantically correct location since it scopes to the FiltersPanel container. - DRY: replaced the SrOnlyH1 / SrOnlyH2 styled blocks copy-pasted into five files with the shared VisuallyHidden component (with `as` prop). Component lives in src/components/Accessibility/ alongside SkipLink. - ChartCreation no longer renders the title twice (sr-only h1 plus visible aria-hidden h2). Now uses a single visible h1 styled to match the previous h2 size, with the StyledContainer CSS rule updated from h2 to h1. - StyledContainer CSS audit: no other selectors target ChartCreation h3 anywhere in src — the previous h3 → h2 → h1 chain is contained. - Indentation in DashboardWrapper return cleaned up (no fragment wrapper needed after removing the duplicate h2). - Tests added for VisuallyHidden covering default rendering, custom `as`, prop forwarding, and the sr-only style invariant. This PR depends on apache#39240 introducing the same Accessibility folder; once that merges first, the duplicate component definition will rebase away.
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Code Review Agent Run #ae7f96Actionable Suggestions - 0Additional Suggestions - 1
Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
SUMMARY
Implements WCAG 2.1 criterion 2.4.6 (Headings and Labels, Level AA).
<h1>headings to all main pages (Home, Dashboards, Charts, SQL Lab, Datasets, Chart Creation)<h2>section headings for Dashboard content areas and filter panels<h2>elementsTESTING INSTRUCTIONS
ADDITIONAL INFORMATION
Part of a series of 16 individual WCAG 2.1 accessibility PRs. See also fix(a11y): Accessibility improvements for WCAG 2.1 Level A compliance #38342.