Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelogs/upcoming/7675.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Accessibility**

- Added `aria-valuetext` attributes to `EuiRange`s with tick labels for improved screen reader UX
8 changes: 8 additions & 0 deletions src-docs/src/views/range/range_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,14 @@ export const RangeControlExample = {
<EuiCode>label</EuiCode>. The value must be included in the range of
values (min-max), though the label may be anything you choose.
</p>
<p>
The <EuiCode>EuiRangeTick</EuiCode> interface now includes an
optional <EuiCode>accessibleLabel</EuiCode>. This property is
combined with the current <EuiCode>value</EuiCode> to render an{' '}
<EuiCode>aria-valuetext</EuiCode> attribute. A{' '}
<EuiCode>label</EuiCode> of type string will be combined with the
current value when no accessible label is passed.
</p>
Comment thread
1Copenut marked this conversation as resolved.
<EuiCallOut
color="warning"
title="Minimum of 5px width per tick allowed"
Expand Down
16 changes: 8 additions & 8 deletions src-docs/src/views/range/ticks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,15 @@ export default () => {
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' },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

😍 Now this is an example that makes sense to me! Huzzah!

]}
aria-label="An example of EuiDualRange with no linear intervals"
aria-label="An example of EuiRange with no linear intervals"
/>
</>
);
Expand Down
51 changes: 50 additions & 1 deletion src/components/form/range/range.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import React from 'react';
import { fireEvent } from '@testing-library/react';
import { fireEvent, getByRole } from '@testing-library/react';
Comment thread
cee-chen marked this conversation as resolved.
Outdated
import { shouldRenderCustomStyles } from '../../../test/internal';
import { requiredProps } from '../../../test/required_props';
import { render } from '../../../test/rtl';
Expand Down Expand Up @@ -231,4 +231,53 @@ describe('EuiRange', () => {
expect(container.firstChild).toMatchSnapshot();
});
});

describe('input aria-valuetext', () => {
const ticksWithLabels = [
{
label: '20kb',
value: 20,
accessibleLabel: 'twenty kilobytes',
},
{
label: '100kb',
value: 100,
accessibleLabel: 'one-hundred kilobytes',
},
];

it('should exist when the current value has an accessible label', () => {
const { container } = render(
<EuiRange {...props} showTicks ticks={ticksWithLabels} value={20} />
);
const input = getByRole(container, 'slider');
expect(input.getAttribute('aria-valuetext')).toEqual(
'20, (twenty kilobytes)'
);
});
Comment thread
cee-chen marked this conversation as resolved.
Outdated

it('should exist when the current value has a label with typeof string', () => {
Comment thread
cee-chen marked this conversation as resolved.
Outdated
const { container } = render(
<EuiRange
{...props}
showTicks
ticks={[
{ label: '20kb', value: 20 },
{ label: '100kb', value: 100 },
]}
value={20}
/>
);
const input = getByRole(container, 'slider');
expect(input.getAttribute('aria-valuetext')).toEqual('20, (20kb)');
Comment thread
cee-chen marked this conversation as resolved.
Outdated
});

it('should not exist when the current value does not have a matching label', () => {
const { container } = render(
<EuiRange {...props} showTicks ticks={ticksWithLabels} />
Comment thread
cee-chen marked this conversation as resolved.
Outdated
);
const input = getByRole(container, 'slider');
expect(input.getAttribute('aria-valuetext')).toBeNull();
Comment thread
cee-chen marked this conversation as resolved.
Outdated
});
});
});
23 changes: 22 additions & 1 deletion src/components/form/range/range.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -116,6 +116,24 @@ 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})`
// Fall back to the label if it's a usable string
: typeof target.label === 'string'
Comment thread
1Copenut marked this conversation as resolved.
Outdated
? `${target.value}, (${target.label})`
: undefined;
}
};

render() {
const { defaultFullWidth } = this.context as FormContextValue;
const {
Expand Down Expand Up @@ -220,6 +238,9 @@ export class EuiRangeClass extends Component<
showRange={showRange}
>
<EuiRangeSlider
ariaValueText={
ticks ? this.handleAriaValueText(ticks, value) : undefined
}
id={showInput ? undefined : id} // Attach id only to the input if there is one
name={name}
min={min}
Expand Down
3 changes: 3 additions & 0 deletions src/components/form/range/range_slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface EuiRangeSliderProps
onChange?: ChangeEventHandler<HTMLInputElement>;
thumbColor?: EuiRangeLevel['color'];
onResize: EuiResizeObserverProps['onResize'];
ariaValueText?: string;
}

export const EuiRangeSlider: FunctionComponent<EuiRangeSliderProps> = ({
Expand All @@ -69,6 +70,7 @@ export const EuiRangeSlider: FunctionComponent<EuiRangeSliderProps> = ({
showRange,
thumbColor,
onResize,
ariaValueText,
...rest
}) => {
const classes = classNames('euiRangeSlider', className);
Expand All @@ -94,6 +96,7 @@ export const EuiRangeSlider: FunctionComponent<EuiRangeSliderProps> = ({
<EuiResizeObserver onResize={onResize}>
{(resizeRef) => (
<input
aria-valuetext={ariaValueText}
ref={resizeRef}
type="range"
id={id}
Expand Down
1 change: 1 addition & 0 deletions src/components/form/range/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export interface EuiDualRangeProps
export interface EuiRangeTick {
value: number;
label: ReactNode;
accessibleLabel?: string;
}

export interface EuiRangeLevel
Expand Down