diff --git a/changelogs/upcoming/7675.md b/changelogs/upcoming/7675.md new file mode 100644 index 00000000000..f3baab655fd --- /dev/null +++ b/changelogs/upcoming/7675.md @@ -0,0 +1,3 @@ +**Accessibility** + +- Added `aria-valuetext` attributes to `EuiRange`s with tick labels for improved screen reader UX diff --git a/src-docs/src/views/range/range_example.js b/src-docs/src/views/range/range_example.js index 7a845151721..c6cd7244a83 100644 --- a/src-docs/src/views/range/range_example.js +++ b/src-docs/src/views/range/range_example.js @@ -238,6 +238,13 @@ export const RangeControlExample = { label. The value must be included in the range of values (min-max), though the label may be anything you choose.

+

+ Tick labels can improve the accessibility of your range. If your + label is a simple string, it will be read out to screen readers + alongside the value. You can also use the{' '} + accessibleLabel property to provide more explicit + screen reader text. +

{ min={0} max={84} ticks={[ - { label: '1 GB', value: 0 }, - { label: '2GB', value: 14 }, - { label: '4GB', value: 28 }, - { label: '8GB', value: 42 }, - { label: '16GB', value: 56 }, - { label: '32GB', value: 70 }, - { label: '64GB', value: 84 }, + { label: '1 GB', value: 0, accessibleLabel: 'one gigabyte' }, + { label: '2GB', value: 14, accessibleLabel: 'two gigabytes' }, + { label: '4GB', value: 28, accessibleLabel: 'four gigabytes' }, + { label: '8GB', value: 42, accessibleLabel: 'eight gigabytes' }, + { label: '16GB', value: 56, accessibleLabel: 'sixteen gigabytes' }, + { label: '32GB', value: 70, accessibleLabel: 'thirty-two gigabytes' }, + { label: '64GB', value: 84, accessibleLabel: 'sixty-four gigabytes' }, ]} - aria-label="An example of EuiDualRange with no linear intervals" + aria-label="An example of EuiRange with no linear intervals" /> ); diff --git a/src/components/form/range/range.test.tsx b/src/components/form/range/range.test.tsx index 1597204c515..6fce5140241 100644 --- a/src/components/form/range/range.test.tsx +++ b/src/components/form/range/range.test.tsx @@ -231,4 +231,66 @@ describe('EuiRange', () => { expect(container.firstChild).toMatchSnapshot(); }); }); + + describe('input aria-valuetext', () => { + it('should exist when the current value has an accessible label', () => { + const { getByRole } = render( + + ); + expect(getByRole('slider')).toHaveAttribute( + 'aria-valuetext', + '20, (twenty kilobytes)' + ); + }); + + it('falls back to string `label`s if `accessibleLabel` does not exist', () => { + const { getByRole } = render( + + ); + + expect(getByRole('slider')).toHaveAttribute( + 'aria-valuetext', + '20, (20kb)' + ); + }); + + it('should not exist when the current value does not have a matching label', () => { + const { getByRole } = render( + + ); + + expect(getByRole('slider')).not.toHaveAttribute('aria-valuetext'); + }); + }); }); diff --git a/src/components/form/range/range.tsx b/src/components/form/range/range.tsx index 9ef374c178a..cecc02ee955 100644 --- a/src/components/form/range/range.tsx +++ b/src/components/form/range/range.tsx @@ -27,7 +27,7 @@ import { EuiRangeTooltip } from './range_tooltip'; import { EuiRangeTrack } from './range_track'; import { EuiRangeWrapper } from './range_wrapper'; -import type { EuiRangeProps } from './types'; +import type { EuiRangeProps, EuiRangeTick } from './types'; import { euiRangeStyles } from './range.styles'; import { EuiI18n } from '../../i18n'; @@ -116,6 +116,23 @@ export class EuiRangeClass extends Component< }); }; + handleAriaValueText = ( + ticks: EuiRangeTick[], + currentVal: string | number + ): string | undefined => { + const target = ticks.find( + (tick) => tick.value.toString() === currentVal.toString() + ); + + if (target) { + return target.accessibleLabel + ? `${target.value}, (${target.accessibleLabel})` + : typeof target.label === 'string' // Fall back to the label if it's a usable string + ? `${target.value}, (${target.label})` + : undefined; + } + }; + render() { const { defaultFullWidth } = this.context as FormContextValue; const { @@ -220,6 +237,9 @@ export class EuiRangeClass extends Component< showRange={showRange} > ; thumbColor?: EuiRangeLevel['color']; onResize: EuiResizeObserverProps['onResize']; + ariaValueText?: string; } export const EuiRangeSlider: FunctionComponent = ({ @@ -69,6 +70,7 @@ export const EuiRangeSlider: FunctionComponent = ({ showRange, thumbColor, onResize, + ariaValueText, ...rest }) => { const classes = classNames('euiRangeSlider', className); @@ -94,6 +96,7 @@ export const EuiRangeSlider: FunctionComponent = ({ {(resizeRef) => (