Skip to content

fix(#1231) fix(#1070) A11y: add minimum/maximum labels, use output for displayValue #1148

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

Closed
wants to merge 9 commits into from
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/ar-AE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "الحد الأقصى",
"minimum": "الحد الأدنى"
}
Comment on lines +1 to +4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume these translations were vetted by the localization team or came from a prior source that was vetted?

4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/bg-BG.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Максимум",
"minimum": "Минимум"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/cs-CZ.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maximum",
"minimum": "Minimální"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/da-DK.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maksimum",
"minimum": "Minimum"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/de-DE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maximal",
"minimum": "Minimum"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/el-GR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Το μέγιστο",
"minimum": "Ελάχιστο"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"minimum": "Minimum",
"maximum": "Maximum"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/es-ES.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Máximo",
"minimum": "Mínimo"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/et-EE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maksimaalne",
"minimum": "Minimaalne"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/fi-FI.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maksimi",
"minimum": "Minimi"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/fr-FR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maximum",
"minimum": "Minimum"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/he-IL.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "מַקסִימוּם",
"minimum": "מִינִימוּם"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/hu-HU.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maximális",
"minimum": "Minimális"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/it-IT.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Massimo",
"minimum": "Minimo"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/ja-JP.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "最大",
"minimum": "最小"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/ko-KR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "최대",
"minimum": "최소"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/lt-LT.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maksimalus",
"minimum": "Minimumas"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/lv-LV.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maksimums",
"minimum": "Minimālais"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/nb-NO.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maksimum",
"minimum": "Minimum"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/nl-NL.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maximaal",
"minimum": "Minimum"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/pl-PL.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maksimum",
"minimum": "Minimum"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/pt-BR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Máximo",
"minimum": "Mínimo"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/ro-RO.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maxim",
"minimum": "Minim"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/ru-RU.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Максимум",
"minimum": "Минимум"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/sk-SK.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maximum",
"minimum": "Minimálne"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/sl-SI.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Največ",
"minimum": "Najmanj"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/sr-SP.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Максимум",
"minimum": "Минимум"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/sv-SE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maximal",
"minimum": "Minimum"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/tr-TR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Maksimum",
"minimum": "Minimum"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/uk-UA.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "Максимум",
"minimum": "Мінімум"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/zh-CN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "最大",
"minimum": "最小"
}
4 changes: 4 additions & 0 deletions packages/@react-aria/slider/intl/zh-TW.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"maximum": "最大值",
"minimum": "最小值"
}
18 changes: 16 additions & 2 deletions packages/@react-aria/slider/src/useSliderThumb.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {clamp, focusWithoutScrolling, mergeProps, useGlobalListeners} from '@react-aria/utils';
// @ts-ignore
import intlMessages from '../intl/*.json';
import React, {ChangeEvent, HTMLAttributes, useCallback, useEffect, useRef} from 'react';
import {sliderIds} from './utils';
import {SliderState} from '@react-stately/slider';
import {SliderThumbProps} from '@react-types/slider';
import {useFocusable} from '@react-aria/focus';
import {useLabel} from '@react-aria/label';
import {useLocale} from '@react-aria/i18n';
import {useLocale, useMessageFormatter} from '@react-aria/i18n';
import {useMove} from '@react-aria/interactions';

interface SliderThumbAria {
Expand Down Expand Up @@ -40,9 +42,20 @@ export function useSliderThumb(
isDisabled,
validationState,
trackRef,
inputRef
inputRef,
'aria-label': ariaLabel
} = opts;

// Provide minimum and maximum aria-label when there are more than one slider thumb.
let formatMessage = useMessageFormatter(intlMessages);
if (state.values.length === 2 && !ariaLabel) {
if (index === 0) {
ariaLabel = formatMessage('minimum');
} else if (index === 1) {
ariaLabel = formatMessage('maximum');
}
}
Comment on lines +51 to +57
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps overkill for now, but should this account for cases that have more than 2 slider thumbs? I guess it would just need to check that index === state.value.length - 1 for maximum.

Not sure if it is appropriate though, perhaps these aria messages are for range sliders only

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not sure if we should put this here as it kinda assumes that all two-thumbed sliders are range sliders. IMO the labels should go in Spectrum RangeSlider since we don't have a range specific aria hook.


let isVertical = opts.orientation === 'vertical';

let {direction} = useLocale();
Expand All @@ -51,6 +64,7 @@ export function useSliderThumb(
let labelId = sliderIds.get(state);
const {labelProps, fieldProps} = useLabel({
...opts,
'aria-label': ariaLabel,
'aria-labelledby': `${labelId} ${opts['aria-labelledby'] ?? ''}`.trim()
});

Expand Down
30 changes: 27 additions & 3 deletions packages/@react-aria/slider/stories/StoryMultiSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';
import {SliderProps, SliderThumbProps} from '@react-types/slider';
import {SliderState, useSliderState} from '@react-stately/slider';
import styles from './story-slider.css';
import {usePress} from '@react-aria/interactions';
import {useSlider, useSliderThumb} from '@react-aria/slider';
import {VisuallyHidden} from '@react-aria/visually-hidden';

Expand All @@ -25,11 +26,32 @@ export function StoryMultiSlider(props: StoryMultiSliderProps) {
throw new Error('You must have the same number of StoryThumb as the number of values in `defaultValue` or `value`.');
}

// Pressing the displayValue should focus the corresponding input.
let {pressProps: outputPressProps} = usePress({
onPress: (e) => e.target.ownerDocument && e.target.ownerDocument.getElementById(e.target.getAttribute('for')).focus()
});

return (
<div {...containerProps} className={styles.slider}>
<div className={styles.sliderLabel}>
{props.label && <label {...labelProps} className={styles.label}>{props.label}</label>}
<div className={styles.value}>{JSON.stringify(state.values)}</div>
{props.label && <label {...labelProps} htmlFor={`${containerProps.id}-thumb-0`} className={styles.label}>{props.label}</label>}
<div className={styles.value}>
[
{
state.values.map((value, index) => (
<output
key={`${containerProps.id}-output-${index}`}
{...outputPressProps}
aria-live="off"
aria-labelledby={`${(props.label ? labelProps.id : containerProps.id)} ${containerProps.id}-thumb-${index}`}
htmlFor={`${containerProps.id}-thumb-${index}`}>
{value}
{index < state.values.length - 1 && ','}
</output>
))
}
]
</div>
</div>
{
// We make rail and all thumbs children of the trackRef. That means dragging on the thumb
Expand All @@ -40,6 +62,7 @@ export function StoryMultiSlider(props: StoryMultiSliderProps) {
<div className={styles.rail} />
{React.Children.map(children, ((child, index) =>
React.cloneElement(child as React.ReactElement, {
id: `${containerProps.id}-thumb-${index}`,
__context: {
sliderProps: props,
state,
Expand Down Expand Up @@ -68,12 +91,13 @@ export function StoryThumb(props: StoryThumbProps) {
throw new Error('Cannot use StoryThumb outside of a StoryMultiSlider!');
}

const {label, isDisabled} = props;
const {label, isDisabled, id} = props;
const context = (props as any).__context as SliderStateContext;
const {index, state, sliderProps} = context;
const inputRef = React.useRef<HTMLInputElement>(null);
const {inputProps, thumbProps, labelProps} = useSliderThumb({
index,
id,
...props,
isDisabled: sliderProps.isDisabled || props.isDisabled,
trackRef: context.trackRef,
Expand Down
30 changes: 22 additions & 8 deletions packages/@react-aria/slider/stories/StoryRangeSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,21 @@ import {FocusRing} from '@react-aria/focus';
import React from 'react';
import {SliderProps} from '@react-types/slider';
import styles from './story-slider.css';
import {usePress} from '@react-aria/interactions';
import {useSlider, useSliderThumb} from '@react-aria/slider';
import {useSliderState} from '@react-stately/slider';
import {VisuallyHidden} from '@react-aria/visually-hidden';


interface StoryRangeSliderProps extends SliderProps {
minLabel?: string,
maxLabel?: string,
showTip?: boolean
}

export function StoryRangeSlider(props: StoryRangeSliderProps) {
const {minLabel, maxLabel} = props;
const trackRef = React.useRef<HTMLDivElement>(null);
const minInputRef = React.useRef<HTMLInputElement>(null);
const maxInputRef = React.useRef<HTMLInputElement>(null);
const inputRefs = [minInputRef, maxInputRef];
const state = useSliderState(props);

if (state.values.length !== 2) {
Expand All @@ -30,28 +29,43 @@ export function StoryRangeSlider(props: StoryRangeSliderProps) {

const {thumbProps: minThumbProps, inputProps: minInputProps} = useSliderThumb({
index: 0,
'aria-label': minLabel ?? 'Minimum',
isDisabled: props.isDisabled,
trackRef,
inputRef: minInputRef
}, state);

const {thumbProps: maxThumbProps, inputProps: maxInputProps} = useSliderThumb({
index: 1,
'aria-label': maxLabel ?? 'Maximum',
isDisabled: props.isDisabled,
trackRef,
inputRef: maxInputRef
}, state);

// Pressing the displayValue should focus the corresponding input.
let {pressProps: outputPressProps} = usePress({
onPress: (e) => inputRefs.find(inputRef => inputRef.current.id === e.target.getAttribute('for')).current.focus()
});

return (
<div {...containerProps} className={styles.slider}>
<div className={styles.sliderLabel}>
{props.label && <label {...labelProps} className={styles.label}>{props.label}</label>}
{props.label && <label {...labelProps} htmlFor={minInputProps.id} className={styles.label}>{props.label}</label>}
<div className={styles.value}>
{state.getThumbValueLabel(0)}
<output
{...outputPressProps}
aria-live="off"
aria-labelledby={`${props.label ? labelProps.id : containerProps.id} ${minInputProps.id}`}
htmlFor={minInputProps.id}>
{state.getThumbValueLabel(0)}
</output>
{' to '}
{state.getThumbValueLabel(1)}
<output
{...outputPressProps}
aria-live="off"
aria-labelledby={`${props.label ? labelProps.id : containerProps.id} ${maxInputProps.id}`}
htmlFor={maxInputProps.id}>
{state.getThumbValueLabel(1)}
</output>
</div>
</div>
<div className={styles.trackContainer}>
Expand Down
Loading