From b6085ce59764dc76460aa9b809762961a725c36f Mon Sep 17 00:00:00 2001 From: 1Copenut Date: Wed, 10 Apr 2024 16:29:58 -0500 Subject: [PATCH 01/20] Added aria-valuetext logic to EuiRangeSlider. --- src/components/form/range/range.tsx | 22 +++++++++++++++++++++- src/components/form/range/range_slider.tsx | 3 +++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/form/range/range.tsx b/src/components/form/range/range.tsx index 9ef374c178a..e46c735636f 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 => { + let ariaValueText; + let target = ticks.find( + (tick) => tick.value.toString() === currentVal.toString() + ); + + if (target) { + ariaValueText = target.value.toString(); + return ariaValueText; + } else { + return ariaValueText; // 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) => ( Date: Wed, 10 Apr 2024 16:45:07 -0500 Subject: [PATCH 02/20] Added accessibleLabel type to EuiRangeTick, logic to build aria-valuetext if it exists. --- src/components/form/range/range.tsx | 11 ++++++++--- src/components/form/range/types.ts | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/form/range/range.tsx b/src/components/form/range/range.tsx index e46c735636f..1ffac92e2c7 100644 --- a/src/components/form/range/range.tsx +++ b/src/components/form/range/range.tsx @@ -125,12 +125,17 @@ export class EuiRangeClass extends Component< (tick) => tick.value.toString() === currentVal.toString() ); - if (target) { + if (target && !target.accessibleLabel) { ariaValueText = target.value.toString(); return ariaValueText; - } else { - return ariaValueText; // undefined } + + if (target && target.accessibleLabel) { + ariaValueText = `${target.value.toString()}, (${target.accessibleLabel})`; + return ariaValueText; + } + + return ariaValueText; // undefined }; render() { diff --git a/src/components/form/range/types.ts b/src/components/form/range/types.ts index c717b7f3a98..bd4368fbc18 100644 --- a/src/components/form/range/types.ts +++ b/src/components/form/range/types.ts @@ -239,6 +239,7 @@ export interface EuiDualRangeProps export interface EuiRangeTick { value: number; label: ReactNode; + accessibleLabel?: string; } export interface EuiRangeLevel From a8517c8df8736b86883f6095ea3ce1e99a615c36 Mon Sep 17 00:00:00 2001 From: 1Copenut Date: Thu, 11 Apr 2024 09:52:01 -0500 Subject: [PATCH 03/20] Adding aria-valuenow logic for EuiRange. --- src/components/form/range/range.tsx | 15 +++++++++++++++ src/components/form/range/range_slider.tsx | 3 +++ 2 files changed, 18 insertions(+) diff --git a/src/components/form/range/range.tsx b/src/components/form/range/range.tsx index 1ffac92e2c7..74e255a590c 100644 --- a/src/components/form/range/range.tsx +++ b/src/components/form/range/range.tsx @@ -116,6 +116,20 @@ export class EuiRangeClass extends Component< }); }; + handleAriaValueNow = (currentVal: string | number): number | undefined => { + if (!currentVal) return; + + let ariaValueNow; + let target = Number(currentVal); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/Number#return_value + + if (!Number.isNaN(target)) { + ariaValueNow = target; + return ariaValueNow; + } + + return ariaValueNow; // undefined + }; + handleAriaValueText = ( ticks: EuiRangeTick[], currentVal: string | number @@ -245,6 +259,7 @@ export class EuiRangeClass extends Component< ariaValueText={ ticks ? this.handleAriaValueText(ticks, value) : undefined } + ariaValueNow={this.handleAriaValueNow(value)} id={showInput ? undefined : id} // Attach id only to the input if there is one name={name} min={min} diff --git a/src/components/form/range/range_slider.tsx b/src/components/form/range/range_slider.tsx index b05033a167d..b369a24c896 100644 --- a/src/components/form/range/range_slider.tsx +++ b/src/components/form/range/range_slider.tsx @@ -52,6 +52,7 @@ export interface EuiRangeSliderProps thumbColor?: EuiRangeLevel['color']; onResize: EuiResizeObserverProps['onResize']; ariaValueText?: string; + ariaValueNow?: number; } export const EuiRangeSlider: FunctionComponent = ({ @@ -71,6 +72,7 @@ export const EuiRangeSlider: FunctionComponent = ({ thumbColor, onResize, ariaValueText, + ariaValueNow, ...rest }) => { const classes = classNames('euiRangeSlider', className); @@ -97,6 +99,7 @@ export const EuiRangeSlider: FunctionComponent = ({ {(resizeRef) => ( Date: Thu, 11 Apr 2024 10:34:26 -0500 Subject: [PATCH 04/20] Adding unit test logic for aria-valuenow and aria-valuetext. --- .../range/__snapshots__/range.test.tsx.snap | 119 ++++++++++++++++++ src/components/form/range/range.test.tsx | 63 +++++++++- 2 files changed, 181 insertions(+), 1 deletion(-) diff --git a/src/components/form/range/__snapshots__/range.test.tsx.snap b/src/components/form/range/__snapshots__/range.test.tsx.snap index b096d5f964f..8313ba46fa3 100644 --- a/src/components/form/range/__snapshots__/range.test.tsx.snap +++ b/src/components/form/range/__snapshots__/range.test.tsx.snap @@ -10,6 +10,7 @@ exports[`EuiRange allows value prop to accept a number 1`] = ` > 1`] = ` > `; +exports[`EuiRange props input should include aria-valuetext when value equals tick[value] 1`] = ` +
+
+
+ + +
+ +
+
+`; + +exports[`EuiRange props input should not include aria-valuetext when values are different 1`] = ` +
+
+
+ + +
+ +
+
+`; + exports[`EuiRange props input should render 1`] = ` `; -exports[`EuiRange props input should include aria-valuetext when value equals tick[value] 1`] = ` -
-
-
- - -
- -
-
-`; - -exports[`EuiRange props input should not include aria-valuetext when values are different 1`] = ` -
-
-
- - -
- -
-
-`; - exports[`EuiRange props input should render 1`] = `
{ expect(container.firstChild).toMatchSnapshot(); }); - test('input should include aria-valuetext when value equals tick[value]', () => { - const { container } = render( - - ); - - const input = getByRole(container, 'slider'); - - expect(input).toBeInTheDocument(); - expect(input.getAttribute('aria-valuetext')).toEqual( - '20, (twenty kilobytes)' - ); - - expect(container.firstChild).toMatchSnapshot(); - }); - - test('input should not include aria-valuetext when values are different', () => { - const { container } = render( - - ); - - const input = getByRole(container, 'slider'); - - expect(input).toBeInTheDocument(); - expect(input.getAttribute('aria-valuetext')).toBeFalsy(); - - expect(container.firstChild).toMatchSnapshot(); - }); - test('slider should display in popover', () => { const { container, baseElement, getByTestSubject } = render( { 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( + + ); + const input = getByRole(container, 'slider'); + expect(input.getAttribute('aria-valuetext')).toEqual( + '20, (twenty kilobytes)' + ); + }); + + it('should exist when the current value has a label with typeof string', () => { + const { container } = render( + + ); + const input = getByRole(container, 'slider'); + expect(input.getAttribute('aria-valuetext')).toEqual('20, (20kb)'); + }); + + it('should not exist when the current value does not have a matching label', () => { + const { container } = render( + + ); + const input = getByRole(container, 'slider'); + expect(input.getAttribute('aria-valuetext')).toBeNull(); + }); + }); }); From b8439df002894571af52122a2f0b5896b29c7c13 Mon Sep 17 00:00:00 2001 From: 1Copenut Date: Mon, 15 Apr 2024 11:18:21 -0500 Subject: [PATCH 14/20] Updated docs to illustrate aria-valuetext with label typeof string. --- src-docs/src/views/range/levels.tsx | 10 +++++----- src-docs/src/views/range/range_example.js | 8 +++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src-docs/src/views/range/levels.tsx b/src-docs/src/views/range/levels.tsx index 5542462038d..949e4583088 100644 --- a/src-docs/src/views/range/levels.tsx +++ b/src-docs/src/views/range/levels.tsx @@ -31,11 +31,11 @@ export default () => { ]; const customTicks = [ - { label: 'low', value: 0, accessibleLabel: 'low' }, - { label: 'intermediate', value: 15, accessibleLabel: 'intermediate' }, - { label: 'moderate', value: 35, accessibleLabel: 'moderate' }, - { label: 'high', value: 65, accessibleLabel: 'high' }, - { label: 'severe', value: 85, accessibleLabel: 'severe' }, + { label: 'low', value: 0 }, + { label: 'intermediate', value: 15 }, + { label: 'moderate', value: 35 }, + { label: 'high', value: 65 }, + { label: 'severe', value: 85 }, ]; const customColorsLevels = [ diff --git a/src-docs/src/views/range/range_example.js b/src-docs/src/views/range/range_example.js index 0a91e58d95d..dae1615a9ec 100644 --- a/src-docs/src/views/range/range_example.js +++ b/src-docs/src/views/range/range_example.js @@ -242,11 +242,9 @@ export const RangeControlExample = { The EuiRangeTick interface now includes an optional accessibleLabel. This property is combined with the current value to render an{' '} - aria-valuetext attribute. If the accessible label - is not included, aria-valuetext will be the - computed current value. This attribute is announced to screen reader - users and is useful when values are defined differently than tick - labels. + aria-valuetext attribute. A{' '} + label of type string will be combined with the + current value when no accessible label is passed.

Date: Tue, 16 Apr 2024 10:38:59 -0500 Subject: [PATCH 15/20] Update changelogs/upcoming/7675.md Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com> --- changelogs/upcoming/7675.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/upcoming/7675.md b/changelogs/upcoming/7675.md index bed64683a1b..f3baab655fd 100644 --- a/changelogs/upcoming/7675.md +++ b/changelogs/upcoming/7675.md @@ -1,3 +1,3 @@ **Accessibility** -- Added `aria-valuenow` and `aria-valuetext` attributes to `EuiRange` for improved screen reader UX +- Added `aria-valuetext` attributes to `EuiRange`s with tick labels for improved screen reader UX From 08c8aa314eac16856f53649b1abcb797917b2f2a Mon Sep 17 00:00:00 2001 From: Trevor Pierce <1Copenut@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:42:35 -0500 Subject: [PATCH 16/20] Update src/components/form/range/range.tsx Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com> --- src/components/form/range/range.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/form/range/range.tsx b/src/components/form/range/range.tsx index 2277d433cae..d64fc15f523 100644 --- a/src/components/form/range/range.tsx +++ b/src/components/form/range/range.tsx @@ -126,9 +126,10 @@ export class EuiRangeClass extends Component< if (target) { return target.accessibleLabel - ? `${target.value.toString()}, (${target.accessibleLabel})` + ? `${target.value}, (${target.accessibleLabel})` + // Fall back to the label if it's a usable string : typeof target.label === 'string' - ? `${target.value.toString()}, (${target.label})` + ? `${target.value}, (${target.label})` : undefined; } }; From 0ccc7a4a392e28c809e8cb51b50c4f08a3f8c07c Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 16 Apr 2024 08:53:11 -0700 Subject: [PATCH 17/20] [PR feedback] tests --- src/components/form/range/range.test.tsx | 53 +++++++++++++----------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/components/form/range/range.test.tsx b/src/components/form/range/range.test.tsx index abe46f4ce01..62425bf4cf4 100644 --- a/src/components/form/range/range.test.tsx +++ b/src/components/form/range/range.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { fireEvent, getByRole } from '@testing-library/react'; +import { fireEvent } from '@testing-library/react'; import { shouldRenderCustomStyles } from '../../../test/internal'; import { requiredProps } from '../../../test/required_props'; import { render } from '../../../test/rtl'; @@ -233,31 +233,34 @@ describe('EuiRange', () => { }); 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( - + const { getByRole } = render( + ); - const input = getByRole(container, 'slider'); - expect(input.getAttribute('aria-valuetext')).toEqual( + expect(getByRole('slider')).toHaveAttribute( + 'aria-valuetext', '20, (twenty kilobytes)' ); }); - it('should exist when the current value has a label with typeof string', () => { - const { container } = render( + it('falls back to string `label`s if `accessibleLabel` does not exist', () => { + const { getByRole } = render( { value={20} /> ); - const input = getByRole(container, 'slider'); - expect(input.getAttribute('aria-valuetext')).toEqual('20, (20kb)'); + + expect(getByRole('slider')).toHaveAttribute('aria-valuetext', '20, (20kb)'); }); it('should not exist when the current value does not have a matching label', () => { - const { container } = render( + const { getByRole } = render( ); - const input = getByRole(container, 'slider'); - expect(input.getAttribute('aria-valuetext')).toBeNull(); + + expect(getByRole('slider')).not.toHaveAttribute('aria-valuetext'); }); }); }); From fed091fff0feef9a2054386996c7848cb9d4f424 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 16 Apr 2024 08:56:15 -0700 Subject: [PATCH 18/20] sorry Trevor! --- src/components/form/range/range.test.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/form/range/range.test.tsx b/src/components/form/range/range.test.tsx index 62425bf4cf4..6fce5140241 100644 --- a/src/components/form/range/range.test.tsx +++ b/src/components/form/range/range.test.tsx @@ -272,12 +272,22 @@ describe('EuiRange', () => { /> ); - expect(getByRole('slider')).toHaveAttribute('aria-valuetext', '20, (20kb)'); + 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'); From f3b701e5490479bec5aeb23568b81f5bd0d8d325 Mon Sep 17 00:00:00 2001 From: Trevor Pierce <1Copenut@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:55:31 -0500 Subject: [PATCH 19/20] Update src-docs/src/views/range/range_example.js This is more clear and succinct. Using it exactly as is. Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com> --- src-docs/src/views/range/range_example.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src-docs/src/views/range/range_example.js b/src-docs/src/views/range/range_example.js index dae1615a9ec..c6cd7244a83 100644 --- a/src-docs/src/views/range/range_example.js +++ b/src-docs/src/views/range/range_example.js @@ -239,12 +239,11 @@ export const RangeControlExample = { values (min-max), though the label may be anything you choose.

- The EuiRangeTick interface now includes an - optional accessibleLabel. This property is - combined with the current value to render an{' '} - aria-valuetext attribute. A{' '} - label of type string will be combined with the - current value when no accessible label is passed. + 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.

Date: Tue, 16 Apr 2024 11:56:09 -0500 Subject: [PATCH 20/20] Update src/components/form/range/range.tsx Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com> --- src/components/form/range/range.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/form/range/range.tsx b/src/components/form/range/range.tsx index d64fc15f523..cecc02ee955 100644 --- a/src/components/form/range/range.tsx +++ b/src/components/form/range/range.tsx @@ -127,8 +127,7 @@ export class EuiRangeClass extends Component< if (target) { return target.accessibleLabel ? `${target.value}, (${target.accessibleLabel})` - // Fall back to the label if it's a usable string - : typeof target.label === 'string' + : typeof target.label === 'string' // Fall back to the label if it's a usable string ? `${target.value}, (${target.label})` : undefined; }