-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(Slider): forward aria attributes to the thumb #6848
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,7 +44,7 @@ export interface SliderEmits { | |
| </script> | ||
|
|
||
| <script setup lang="ts" generic="T extends number | number[]"> | ||
| import { computed } from 'vue' | ||
| import { computed, useAttrs } from 'vue' | ||
| import { SliderRoot, SliderRange, SliderTrack, SliderThumb } from 'reka-ui' | ||
| import { useForwardProps } from '../composables/useForwardProps' | ||
| import { reactivePick } from '@vueuse/core' | ||
|
|
@@ -62,6 +62,8 @@ const _props = withDefaults(defineProps<SliderProps>(), { | |
| }) | ||
| const emits = defineEmits<SliderEmits>() | ||
|
|
||
| defineOptions({ inheritAttrs: false }) | ||
|
|
||
| const props = useComponentProps<SliderProps>('slider', _props) | ||
|
|
||
| const modelValue = defineModel<T>() | ||
|
|
@@ -100,6 +102,30 @@ const sliderValue = computed({ | |
|
|
||
| const thumbs = computed(() => sliderValue.value?.length ?? 1) | ||
|
|
||
| const attrs = useAttrs() | ||
|
|
||
| const thumbAttrs = computed(() => { | ||
| if (thumbs.value > 1) return {} | ||
|
|
||
| const ariaAttrs = Object.fromEntries(Object.entries(attrs).filter(([key]) => key.startsWith('aria-'))) | ||
|
Comment on lines
+108
to
+110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π― Functional Correctness | π Major | ποΈ Heavy lift π§© Analysis chainπ Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target file outline ---'
ast-grep outline src/runtime/components/Slider.vue --match Slider --view expanded || true
printf '%s\n' '--- target implementation ---'
sed -n '1,220p' src/runtime/components/Slider.vue
printf '%s\n' '--- related symbols and tests ---'
rg -n --glob '!node_modules' --glob '!dist' \
'thumbAttrs|rootAttrs|aria-valuetext|SliderRoot|SliderThumb|useAttrs\(' \
src test tests 2>/dev/null || true
printf '%s\n' '--- candidate slider files ---'
fd -i 'slider' . --type fRepository: nuxt/ui Length of output: 50364 π Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Slider.vue ---'
cat -n src/runtime/components/Slider.vue
printf '%s\n' '--- slider-related tracked files ---'
git ls-files | rg -i '(^|/)(slider|.*slider.*)\.(vue|ts|tsx|js|jsx)$|slider.*\.(spec|test)\.'
printf '%s\n' '--- focused source references ---'
rg -n --glob '!**/__snapshots__/**' --glob '!**/*.snap' \
'thumbAttrs|rootAttrs|aria-valuetext|SliderRoot|SliderThumb|useAttrs\(' \
src test | head -n 300 || true
printf '%s\n' '--- package and primitive references ---'
rg -n --glob 'package.json' --glob 'pnpm-lock.yaml' --glob 'yarn.lock' --glob 'package-lock.json' \
'reka-ui|slider' . | head -n 120 || trueRepository: nuxt/ui Length of output: 10598 π Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Slider.spec.ts ---'
cat -n test/components/Slider.spec.ts
printf '%s\n' '--- nearby slider snapshot references ---'
rg -n -C 3 --glob 'test/components/__snapshots__/Slider*.snap' \
'aria-valuetext|role="group"|role="slider"|data-slot="thumb"' \
test/components/__snapshots__ | head -n 240 || true
printf '%s\n' '--- all non-snapshot slider assertions ---'
rg -n -C 5 --glob '!**/__snapshots__/**' \
'multi|multiple|aria-|role|thumbs|re-render|renderThumbs' \
test/components/Slider.spec.tsRepository: nuxt/ui Length of output: 50363 π Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from dataclasses import dataclass
`@dataclass`
class Result:
thumbs: int
thumb_attrs: dict
root_attrs: dict
def component_partition(attrs, thumbs):
thumb_attrs = {}
if thumbs <= 1:
thumb_attrs = {k: v for k, v in attrs.items() if k.startswith("aria-")}
if "aria-label" not in thumb_attrs and "aria-labelledby" not in thumb_attrs:
thumb_attrs["aria-label"] = "Thumb"
root_attrs = {k: v for k, v in attrs.items() if k not in thumb_attrs}
if root_attrs.get("aria-label") or root_attrs.get("aria-labelledby"):
root_attrs["role"] = "group"
return Result(thumbs, thumb_attrs, root_attrs)
cases = [
(1, {"aria-valuetext": "10 milliseconds"}),
(2, {"aria-label": "Price range", "aria-valuetext": "10β90"}),
(2, {"aria-labelledby": "price-label", "aria-describedby": "price-help", "aria-valuetext": "10β90"}),
]
for thumbs, attrs in cases:
result = component_partition(attrs, thumbs)
print(f"thumbs={result.thumbs}")
print(f" thumbAttrs={result.thumb_attrs}")
print(f" rootAttrs={result.root_attrs}")
PYRepository: nuxt/ui Length of output: 504 π Web query:
π‘ Result: The WAI-ARIA Authoring Practices Guide (APG) defines the multi-thumb slider pattern as an extension of the standard slider pattern, designed to handle multiple independent or related values within a single range [1]. Key accessibility requirements for the multi-thumb slider pattern include: Role and Focus: Each thumb must be an independent, focusable element with role="slider" [1][2]. To make these elements focusable, the tabindex="0" attribute is required [3][4]. Focus should be placed directly on the visual thumb that represents the slider value [5][4]. Required Attributes: Each slider thumb must implement the following ARIA attributes: - aria-valuenow: The current value of the thumb [1][6]. - aria-valuemin: The minimum allowed value [1][6]. - aria-valuemax: The maximum allowed value [1][6]. - Labeling: Each thumb requires an accessible name, typically provided via aria-label or aria-labelledby [1][3][2]. Optional Attributes (aria-valuetext): When the numeric value (aria-valuenow) is not sufficiently descriptive or intuitiveβsuch as when representing non-numeric data or complex unitsβaria-valuetext should be used to provide a human-readable string [4][6]. This is particularly useful for improving the experience for assistive technology users by conveying context (e.g., "7 out of 10" or currency formatting) [4][7][8]. Interaction and Behavior: - Keyboard Support: Users interact with each thumb using standard slider keys: Left/Down Arrow (decrease), Right/Up Arrow (increase), Home (minimum), and End (maximum) [3][9][5]. - Multi-Thumb Coordination: In scenarios where thumbs define a range (e.g., minimum and maximum price), they often have mutual constraints. When one thumb's value limits the other, the aria-valuemin or aria-valuemax attributes of the dependent slider must be updated dynamically as the controlling thumb moves [1][2]. - Tab Order: The tab order should remain constant regardless of the visual position or value of the thumbs [1]. For multi-thumb sliders, it is also recommended to group the thumbs within a container element using role="group" with a clear, shared label to communicate the collective purpose of the sliders [2][7]. Citations:
Expose per-thumb When π€ Prompt for AI Agents |
||
|
|
||
| if (!ariaAttrs['aria-label'] && !ariaAttrs['aria-labelledby']) { | ||
| ariaAttrs['aria-label'] = 'Thumb' | ||
| } | ||
|
|
||
| return ariaAttrs | ||
| }) | ||
|
|
||
| const rootAttrs = computed(() => { | ||
| const rest = Object.fromEntries(Object.entries(attrs).filter(([key]) => !(key in thumbAttrs.value))) | ||
|
|
||
| if (rest['aria-label'] || rest['aria-labelledby']) { | ||
| rest.role = 'group' | ||
| } | ||
|
|
||
| return rest | ||
| }) | ||
|
|
||
| // eslint-disable-next-line vue/no-dupe-keys | ||
| const ui = computed(() => tv({ extend: theme, ...(appConfig.ui?.slider || {}) })({ | ||
| disabled: disabled.value, | ||
|
|
@@ -118,12 +144,12 @@ function onChange(value: any) { | |
|
|
||
| <template> | ||
| <SliderRoot | ||
| v-bind="rootProps" | ||
| :id="id" | ||
| v-model="sliderValue" | ||
| data-slot="root" | ||
| v-bind="{ ...rootProps, ...rootAttrs }" | ||
| :name="name" | ||
| :disabled="disabled" | ||
| data-slot="root" | ||
| :class="ui.root({ class: [props.ui?.root, props.class] })" | ||
| :default-value="defaultSliderValue" | ||
| @update:model-value="emitFormInput()" | ||
|
|
@@ -140,9 +166,9 @@ function onChange(value: any) { | |
| disable-closing-trigger | ||
| v-bind="(typeof props.tooltip === 'object' ? props.tooltip : {})" | ||
| > | ||
| <SliderThumb data-slot="thumb" :class="ui.thumb({ class: props.ui?.thumb })" :aria-label="thumbs === 1 ? 'Thumb' : `Thumb ${thumb} of ${thumbs}`" v-bind="ariaAttrs" /> | ||
| <SliderThumb data-slot="thumb" :class="ui.thumb({ class: props.ui?.thumb })" v-bind="{ ...thumbAttrs, ...ariaAttrs }" /> | ||
| </UTooltip> | ||
| <SliderThumb v-else data-slot="thumb" :class="ui.thumb({ class: props.ui?.thumb })" :aria-label="thumbs === 1 ? 'Thumb' : `Thumb ${thumb} of ${thumbs}`" v-bind="ariaAttrs" /> | ||
| <SliderThumb v-else data-slot="thumb" :class="ui.thumb({ class: props.ui?.thumb })" v-bind="{ ...thumbAttrs, ...ariaAttrs }" /> | ||
| </template> | ||
| </SliderRoot> | ||
| </template> | ||
Uh oh!
There was an error while loading. Please reload this page.