11<script setup lang="ts">
2+ import { debounce } from ' perfect-debounce'
23const {
34 isFacetSelected,
45 toggleFacet,
@@ -22,61 +23,94 @@ function isCategoryNoneSelected(category: string): boolean {
2223 const selectableFacets = facets .filter (f => ! f .comingSoon )
2324 return selectableFacets .length > 0 && selectableFacets .every (f => ! isFacetSelected (f .id ))
2425}
26+
27+ const liveRegionText = ref (' ' )
28+ const clearLiveRegion = debounce (() => {
29+ liveRegionText .value = ' '
30+ }, 250 )
31+ const updateLiveRegion = debounce ((message : string ) => {
32+ liveRegionText .value = message
33+ clearLiveRegion ()
34+ }, 250 )
35+
36+ function selectAllFacet(category : string ) {
37+ if (! isCategoryAllSelected (category )) {
38+ updateLiveRegion ($t (' compare.facets.selected_all_category_facets' , { category }))
39+ selectCategory (category )
40+ }
41+ }
42+
43+ function deselectAllFacet(category : string ) {
44+ if (! isCategoryNoneSelected (category )) {
45+ updateLiveRegion ($t (' compare.facets.deselected_all_category_facets' , { category }))
46+ deselectCategory (category )
47+ }
48+ }
2549 </script >
2650
2751<template >
28- <div class =" space-y-3" role =" group" :aria-label =" $t('compare.facets.group_label')" >
52+ <div role =" status" aria-live =" polite" class =" sr-only" >{{ liveRegionText }}</div >
53+ <div class =" space-y-3" >
2954 <div v-for =" category in categoryOrder" :key =" category" >
30- <!-- Category header with all/none buttons -->
3155 <div class =" flex items-center gap-2 mb-2" >
32- <span class =" text-3xs text-fg-subtle uppercase tracking-wider" >
56+ <span
57+ :id =" `facet-category-label-${category}`"
58+ class =" text-3xs text-fg-subtle uppercase tracking-wider"
59+ >
3360 {{ getCategoryLabel(category) }}
3461 </span >
35- <!-- TODO: These should be radios, since they are mutually exclusive, and currently this behavior is faked with buttons -->
62+
3663 <ButtonBase
64+ size =" sm"
65+ data-facet-category-action =" all"
66+ :data-facet-category =" category"
3767 :aria-label ="
38- $t('compare.facets.select_category', { category: getCategoryLabel(category) })
68+ $t('compare.facets.select_all_category_facets', {
69+ category: getCategoryLabel(category),
70+ })
3971 "
40- :aria-pressed =" isCategoryAllSelected(category)"
41- :disabled =" isCategoryAllSelected(category)"
42- @click =" selectCategory(category)"
43- size =" sm"
72+ :aria-disabled =" isCategoryAllSelected(category)"
73+ class =" aria-disabled:(opacity-40 border-transparent)"
74+ @click =" selectAllFacet(category)"
4475 >
4576 {{ $t('compare.facets.all') }}
4677 </ButtonBase >
47- <span class =" text-2xs text-fg-muted/40" >/</span >
78+
79+ <span class =" text-2xs text-fg-muted/40" aria-hidden =" true" >/</span >
80+
4881 <ButtonBase
82+ size =" sm"
83+ data-facet-category-action =" none"
84+ :data-facet-category =" category"
4985 :aria-label ="
50- $t('compare.facets.deselect_category', { category: getCategoryLabel(category) })
86+ $t('compare.facets.deselect_all_category_facets', {
87+ category: getCategoryLabel(category),
88+ })
5189 "
52- :aria-pressed =" isCategoryNoneSelected(category)"
53- :disabled =" isCategoryNoneSelected(category)"
54- @click =" deselectCategory(category)"
55- size =" sm"
90+ :aria-disabled =" isCategoryNoneSelected(category)"
91+ class =" aria-disabled:(opacity-40 border-transparent)"
92+ @click =" deselectAllFacet(category)"
5693 >
5794 {{ $t('compare.facets.none') }}
5895 </ButtonBase >
5996 </div >
6097
61- <!-- Facet buttons -->
62- <div class =" flex items-center gap-1.5 flex-wrap" role =" group" >
63- <!-- TODO: These should be checkboxes -->
98+ <div
99+ class =" flex items-center gap-1.5 flex-wrap"
100+ role =" group"
101+ :aria-labelledby =" `facet-category-label-${category}`"
102+ data-facet-category-facets
103+ >
64104 <ButtonBase
65105 v-for =" facet in facetsByCategory[category]"
66106 :key =" facet.id"
67107 size =" sm"
108+ role =" checkbox"
68109 :title =" facet.comingSoon ? $t('compare.facets.coming_soon') : facet.description"
69110 :disabled =" facet.comingSoon"
70- :aria-pressed =" isFacetSelected(facet.id)"
111+ :aria-checked =" isFacetSelected(facet.id)"
71112 :aria-label =" facet.label"
72- class =" gap-1 px-1.5 rounded transition-colors focus-visible:outline-accent/70"
73- :class ="
74- facet.comingSoon
75- ? 'text-fg-subtle/50 bg-bg-subtle border-border-subtle cursor-not-allowed'
76- : isFacetSelected(facet.id)
77- ? 'text-fg-muted bg-bg-muted'
78- : 'text-fg-subtle bg-bg-subtle border-border-subtle hover:text-fg-muted hover:border-border'
79- "
113+ class =" gap-1 px-1.5 rounded transition-colors text-fg-subtle bg-bg-subtle border-border-subtle enabled:hover:(text-fg-muted border-border) aria-checked:(text-fg-muted bg-fg/10 border-fg/20 hover:enabled:(bg-fg/20 text-fg/50)) focus-visible:outline-accent/70 disabled:(text-fg-subtle/50 bg-bg-subtle border-border-subtle)"
80114 @click =" !facet.comingSoon && toggleFacet(facet.id)"
81115 :classicon ="
82116 facet.comingSoon
0 commit comments