mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(www): group the filter controls and announce the result count (#49410)
Closes FE-4251 ## Problem The filter sidebars are unstructured `div` nesting. Measured on a preview: * 9 checkboxes on `/features` and 13 on `/partners/catalog`, none inside a `fieldset`, a `role="group"`, or any region. * The `/features` "Filter by tags:" `h2` has no `id`, so nothing can reference it as a group name. * The only landmarks on `/features` are two `nav` elements, so the filter panel is unreachable by landmark navigation. A screen reader user meets a checkbox announced as "authentication, checkbox" with nothing conveying that it filters features by tag. Separately, both result counts update on every filter change with no live region, so the outcome of toggling a filter is never announced. ## Solution * Wrap each checkbox set in a labelled `role="group"`. * Wrap each filter panel in an `aside` labelled "Filters". * Add `aria-live="polite"` to both result counts. The two pages name their group differently on purpose. `/features` uses `aria-labelledby` pointing at the existing `h2`, so the visible heading is the accessible name. `/partners/catalog` uses `aria-label`, because `filtersPanel` renders into both the desktop sidebar and the mobile sheet, so an `id` would appear twice in the DOM. That file already calls out the duplicate-id hazard at line 152 as its reason for using wrapping labels. Chose `role="group"` over `fieldset` and `legend` to avoid resetting UA styling in a styled sidebar. The single self-hosted checkbox keeps its own label and needs no group. ## Manual testing **/features** 1. Open [/features](https://zone-www-dot-com-git-www-filter-groups-and-live-count-supabase.vercel.app/features) with Screenreader. 2. Confirm the tag checkboxes report as a group named "Filter by tags:". 3. Confirm a "Filters" landmark appears in landmark navigation. 4. Tick a tag filter. The result count changes and is announced. **/partners/catalog** 5. Open [/partners/catalog](https://zone-www-dot-com-git-www-filter-groups-and-live-count-supabase.vercel.app/partners/catalog) at a desktop width. The filter panel is `hidden md:block`, so the landmark only exists at md and above. 6. Confirm the category checkboxes report as a group named "Categories". 7. Open the mobile filter sheet at a narrow width and confirm the group is still named, with no duplicate ids. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Accessibility Improvements** * Improved screen reader navigation for partner catalog and feature filters. * Added accessible labels and landmarks for filter sections. * Updated result counts to be announced when selections change. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
78d6d57faa
commit
60f7903b52
2 files changed
+20
-10
No files matched your search
@@ -154,7 +154,7 @@ export default function IntegrationsContent({
|
||||
// Uses wrapping <label> elements (no id/htmlFor) to avoid duplicate HTML IDs in the DOM.
|
||||
const filtersPanel = (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div role="group" aria-label="Categories" className="flex flex-col gap-2.5">
|
||||
<h2 className="text-xs font-mono uppercase text-foreground-lighter">Categories</h2>
|
||||
{allCategories.map((category) => {
|
||||
const Icon = getCategoryIcon(category.slug)
|
||||
@@ -212,7 +212,7 @@ export default function IntegrationsContent({
|
||||
<div className="relative grid md:grid-cols-4 md:gap-4">
|
||||
{/* Left sidebar — sticky search + filters */}
|
||||
<div className="relative w-full h-full">
|
||||
<div className="mb-4 flex flex-col gap-4 sticky top-20">
|
||||
<aside aria-label="Filters" className="mb-4 flex flex-col gap-4 sticky top-20">
|
||||
{/* Search bar + mobile filter trigger */}
|
||||
<div className="flex gap-2">
|
||||
<InputGroup className="flex-1">
|
||||
@@ -259,7 +259,7 @@ export default function IntegrationsContent({
|
||||
|
||||
{/* Desktop-only filter panel */}
|
||||
<div className="hidden md:block">{filtersPanel}</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Right: content */}
|
||||
@@ -308,7 +308,7 @@ export default function IntegrationsContent({
|
||||
|
||||
{/* Toolbar: count + view toggle */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-foreground-muted text-xs">
|
||||
<span aria-live="polite" className="text-foreground-muted text-xs">
|
||||
{listPartners.length} partner{listPartners.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<ToggleGroup
|
||||
|
||||
@@ -202,7 +202,7 @@ function FeaturesPage() {
|
||||
</SectionContainer>
|
||||
<SectionContainer className="relative grid md:grid-cols-4 md:gap-4 pt-0!">
|
||||
<div className="relative w-full h-full">
|
||||
<div className="mb-4 flex flex-col gap-4 sticky top-20">
|
||||
<aside aria-label="Filters" className="mb-4 flex flex-col gap-4 sticky top-20">
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon>
|
||||
<Search />
|
||||
@@ -229,8 +229,14 @@ function FeaturesPage() {
|
||||
</label>
|
||||
</div>
|
||||
<div className="hidden md:flex flex-col gap-4">
|
||||
<h2 className="text-sm text-foreground-lighter">Filter by tags:</h2>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<h2 id="feature-tag-filters" className="text-sm text-foreground-lighter">
|
||||
Filter by tags:
|
||||
</h2>
|
||||
<div
|
||||
role="group"
|
||||
aria-labelledby="feature-tag-filters"
|
||||
className="flex flex-col gap-2.5"
|
||||
>
|
||||
{products
|
||||
.sort((a, b) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1))
|
||||
.map((product) => (
|
||||
@@ -249,8 +255,12 @@ function FeaturesPage() {
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-foreground-muted text-xs">
|
||||
Features selected: {filteredFeatures.length}
|
||||
<div
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="text-foreground-muted text-xs"
|
||||
>
|
||||
{`${filteredFeatures.length} features selected`}
|
||||
</div>
|
||||
</div>
|
||||
{HAS_ACTIVE_FILTERS && (
|
||||
@@ -267,7 +277,7 @@ function FeaturesPage() {
|
||||
Clear all filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<div className="md:col-span-3 min-w-0 flex flex-col gap-4 md:gap-6">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
|
||||
Reference in new issue
Block a user