fix(a11y): add aria-pressed and aria-label to Browse panel buttons (WCAG 2.1)
Screen readers had no way to determine which domain, category, subcategory,
or sort button was selected — the active CSS class is invisible to assistive
technology.
- aria-pressed on all toggle buttons (domain, category, subcategory, sort)
- aria-label="Previous page" / "Next page" on pagination buttons
- aria-live="polite" on results count span — announces filter result changes
- Equipment chip-remove: "Remove" → "Remove equipment: {item}"
Addresses WCAG 2.1 AA criteria 4.1.2 (Name, Role, Value) and 1.3.1
(Info and Relationships). Part of kiwi UX audit (2026-05-11).
This commit is contained in:
parent
8c765b7da2
commit
0ef57618bf
2 changed files with 18 additions and 3 deletions
|
|
@ -6,6 +6,7 @@
|
||||||
v-for="domain in domains"
|
v-for="domain in domains"
|
||||||
:key="domain.id"
|
:key="domain.id"
|
||||||
:class="['btn', activeDomain === domain.id ? 'btn-primary' : 'btn-secondary']"
|
:class="['btn', activeDomain === domain.id ? 'btn-primary' : 'btn-secondary']"
|
||||||
|
:aria-pressed="activeDomain === domain.id"
|
||||||
@click="selectDomain(domain.id)"
|
@click="selectDomain(domain.id)"
|
||||||
>
|
>
|
||||||
{{ domain.label }}
|
{{ domain.label }}
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
<div v-else class="category-list mb-sm flex flex-wrap gap-xs">
|
<div v-else class="category-list mb-sm flex flex-wrap gap-xs">
|
||||||
<button
|
<button
|
||||||
:class="['btn', 'btn-secondary', 'cat-btn', { active: activeCategory === '_all' }]"
|
:class="['btn', 'btn-secondary', 'cat-btn', { active: activeCategory === '_all' }]"
|
||||||
|
:aria-pressed="activeCategory === '_all'"
|
||||||
@click="selectCategory('_all')"
|
@click="selectCategory('_all')"
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
|
|
@ -32,6 +34,7 @@
|
||||||
v-for="cat in categories"
|
v-for="cat in categories"
|
||||||
:key="cat.category"
|
:key="cat.category"
|
||||||
:class="['btn', 'btn-secondary', 'cat-btn', { active: activeCategory === cat.category }]"
|
:class="['btn', 'btn-secondary', 'cat-btn', { active: activeCategory === cat.category }]"
|
||||||
|
:aria-pressed="activeCategory === cat.category"
|
||||||
@click="selectCategory(cat.category)"
|
@click="selectCategory(cat.category)"
|
||||||
>
|
>
|
||||||
{{ cat.category }}
|
{{ cat.category }}
|
||||||
|
|
@ -57,6 +60,7 @@
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button
|
<button
|
||||||
:class="['btn', 'btn-secondary', 'subcat-btn', { active: activeSubcategory === null }]"
|
:class="['btn', 'btn-secondary', 'subcat-btn', { active: activeSubcategory === null }]"
|
||||||
|
:aria-pressed="activeSubcategory === null"
|
||||||
@click="selectSubcategory(null)"
|
@click="selectSubcategory(null)"
|
||||||
>
|
>
|
||||||
All {{ activeCategory }}
|
All {{ activeCategory }}
|
||||||
|
|
@ -65,6 +69,7 @@
|
||||||
v-for="sub in subcategories"
|
v-for="sub in subcategories"
|
||||||
:key="sub.subcategory"
|
:key="sub.subcategory"
|
||||||
:class="['btn', 'btn-secondary', 'subcat-btn', { active: activeSubcategory === sub.subcategory }]"
|
:class="['btn', 'btn-secondary', 'subcat-btn', { active: activeSubcategory === sub.subcategory }]"
|
||||||
|
:aria-pressed="activeSubcategory === sub.subcategory"
|
||||||
@click="selectSubcategory(sub.subcategory)"
|
@click="selectSubcategory(sub.subcategory)"
|
||||||
>
|
>
|
||||||
{{ sub.subcategory }}
|
{{ sub.subcategory }}
|
||||||
|
|
@ -105,21 +110,25 @@
|
||||||
<div class="sort-btns flex gap-xs">
|
<div class="sort-btns flex gap-xs">
|
||||||
<button
|
<button
|
||||||
:class="['btn', 'btn-secondary', 'sort-btn', { active: sortOrder === 'default' }]"
|
:class="['btn', 'btn-secondary', 'sort-btn', { active: sortOrder === 'default' }]"
|
||||||
|
:aria-pressed="sortOrder === 'default'"
|
||||||
@click="setSort('default')"
|
@click="setSort('default')"
|
||||||
title="Corpus order"
|
title="Corpus order"
|
||||||
>Default</button>
|
>Default</button>
|
||||||
<button
|
<button
|
||||||
:class="['btn', 'btn-secondary', 'sort-btn', { active: sortOrder === 'alpha' }]"
|
:class="['btn', 'btn-secondary', 'sort-btn', { active: sortOrder === 'alpha' }]"
|
||||||
|
:aria-pressed="sortOrder === 'alpha'"
|
||||||
@click="setSort('alpha')"
|
@click="setSort('alpha')"
|
||||||
title="Alphabetical A→Z"
|
title="Alphabetical A→Z"
|
||||||
>A→Z</button>
|
>A→Z</button>
|
||||||
<button
|
<button
|
||||||
:class="['btn', 'btn-secondary', 'sort-btn', { active: sortOrder === 'alpha_desc' }]"
|
:class="['btn', 'btn-secondary', 'sort-btn', { active: sortOrder === 'alpha_desc' }]"
|
||||||
|
:aria-pressed="sortOrder === 'alpha_desc'"
|
||||||
@click="setSort('alpha_desc')"
|
@click="setSort('alpha_desc')"
|
||||||
title="Alphabetical Z→A"
|
title="Alphabetical Z→A"
|
||||||
>Z→A</button>
|
>Z→A</button>
|
||||||
<button
|
<button
|
||||||
:class="['btn', 'btn-secondary', 'sort-btn', { active: sortOrder === 'match' }]"
|
:class="['btn', 'btn-secondary', 'sort-btn', { active: sortOrder === 'match' }]"
|
||||||
|
:aria-pressed="sortOrder === 'match'"
|
||||||
:disabled="pantryCount === 0"
|
:disabled="pantryCount === 0"
|
||||||
@click="setSort('match')"
|
@click="setSort('match')"
|
||||||
:title="pantryCount > 0 ? 'Sort by pantry match %' : 'Add items to pantry to sort by match'"
|
:title="pantryCount > 0 ? 'Sort by pantry match %' : 'Add items to pantry to sort by match'"
|
||||||
|
|
@ -128,7 +137,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="results-header flex-between mb-sm">
|
<div class="results-header flex-between mb-sm">
|
||||||
<span class="text-sm text-secondary">
|
<span
|
||||||
|
class="text-sm text-secondary"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="true"
|
||||||
|
>
|
||||||
{{ total }} recipes
|
{{ total }} recipes
|
||||||
<span v-if="pantryCount > 0"> — pantry match shown</span>
|
<span v-if="pantryCount > 0"> — pantry match shown</span>
|
||||||
<span v-if="requiredIngredient.trim()"> — must include "{{ requiredIngredient.trim() }}"</span>
|
<span v-if="requiredIngredient.trim()"> — must include "{{ requiredIngredient.trim() }}"</span>
|
||||||
|
|
@ -137,12 +150,14 @@
|
||||||
<button
|
<button
|
||||||
class="btn btn-secondary btn-xs"
|
class="btn btn-secondary btn-xs"
|
||||||
:disabled="page <= 1"
|
:disabled="page <= 1"
|
||||||
|
aria-label="Previous page"
|
||||||
@click="changePage(page - 1)"
|
@click="changePage(page - 1)"
|
||||||
>‹ Prev</button>
|
>‹ Prev</button>
|
||||||
<span class="text-sm text-secondary page-indicator">{{ page }} / {{ totalPages }}</span>
|
<span class="text-sm text-secondary page-indicator" aria-live="polite">{{ page }} / {{ totalPages }}</span>
|
||||||
<button
|
<button
|
||||||
class="btn btn-secondary btn-xs"
|
class="btn btn-secondary btn-xs"
|
||||||
:disabled="page >= totalPages"
|
:disabled="page >= totalPages"
|
||||||
|
aria-label="Next page"
|
||||||
@click="changePage(page + 1)"
|
@click="changePage(page + 1)"
|
||||||
>Next ›</button>
|
>Next ›</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
class="tag-chip status-badge status-info"
|
class="tag-chip status-badge status-info"
|
||||||
>
|
>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
<button class="chip-remove" @click="removeEquipment(item)" aria-label="Remove">×</button>
|
<button class="chip-remove" @click="removeEquipment(item)" :aria-label="'Remove equipment: ' + item">×</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue