Skip to content
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

[a11y] EuiScreenReaderLive component #5567

Merged
merged 11 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Added referenceable `id` for the generated label in `EuiFormRow` ([#5574](https://github.com/elastic/eui/pull/5574))
- Addeed optional attribute configurations in `EuiPopover` to aid screen reader announcements ([#5574](https://github.com/elastic/eui/pull/5574))
- Added `ref` passthroughs to `EuiIputPopover` subcomponents ([#5574](https://github.com/elastic/eui/pull/5574))
- Added `EuiScreenReaderLive` component for updateable `aria-live` regions ([#5567](https://github.com/elastic/eui/pull/5567))

**Bug fixes**

Expand Down
43 changes: 43 additions & 0 deletions src-docs/src/views/accessibility/accessibility_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import {
EuiCode,
EuiLink,
EuiSkipLink,
EuiScreenReaderLive,
EuiScreenReaderOnly,
EuiSpacer,
} from '../../../../src/components';

import ScreenReaderLive from './screen_reader_live';
import ScreenReaderOnly from './screen_reader';
import ScreenReaderFocus from './screen_reader_focus';
import SkipLink from './skip_link';

const screenReaderLiveSource = require('!!raw-loader!./screen_reader_live');
const screenReaderOnlySource = require('!!raw-loader!./screen_reader');
const screenReaderFocusSource = require('!!raw-loader!./screen_reader_focus');

Expand Down Expand Up @@ -112,6 +115,46 @@ export const AccessibilityExample = {
</EuiScreenReaderOnly>`,
demo: <ScreenReaderFocus />,
},
{
title: 'Screen reader live region',
source: [
{
type: GuideSectionTypes.JS,
code: screenReaderLiveSource,
},
],
text: (
<>
<p>
Using <EuiCode>EuiScreenReaderLive</EuiCode> to announce dynamic
content, such as status changes based on user interaction.
</p>
<p>
The configurable <EuiCode>role</EuiCode> and{' '}
<EuiCode>aria-live</EuiCode> props default to{' '}
<EuiCode>status</EuiCode> and <EuiCode>polite</EuiCode> respectively
for unintrusive but timely update announcements. When not using the
default values, be sure to follow{' '}
<EuiLink
href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions"
external
>
ARIA guidelines
</EuiLink>{' '}
for <EuiCode>role</EuiCode> to <EuiCode>aria-live</EuiCode> mapping.
</p>
<p>
Also consider other live region guidelines, such as that live
regions must be present on initial page load, and should not be in a
conditional JSX wrapper.
</p>
</>
),
props: {
EuiScreenReaderLive,
},
demo: <ScreenReaderLive />,
},
{
title: 'Skip link',
source: [
Expand Down
36 changes: 36 additions & 0 deletions src-docs/src/views/accessibility/screen_reader_live.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useState } from 'react';

import {
EuiCode,
EuiFieldNumber,
EuiFormRow,
EuiScreenReaderLive,
EuiSpacer,
EuiText,
} from '../../../../src/components';

export default () => {
const [value, setValue] = useState(1);
return (
<>
<EuiFormRow label="Current value">
<EuiFieldNumber
placeholder="Current value"
value={value}
onChange={(e) => setValue(Number(e.target.value))}
min={0}
/>
</EuiFormRow>
<EuiSpacer />
<EuiText>
<p>
<em>Content announced by screen reader: </em>
<EuiCode>Current value: {value}</EuiCode>
</p>
<EuiScreenReaderLive>
<p>Current value: {value}</p>
</EuiScreenReaderLive>
</EuiText>
</>
);
};
9 changes: 8 additions & 1 deletion src/components/accessibility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@
* Side Public License, v 1.
*/

export { EuiScreenReaderOnly } from './screen_reader';
export {
EuiScreenReaderLive,
EuiScreenReaderLiveProps,
} from './screen_reader_live';
export {
EuiScreenReaderOnly,
EuiScreenReaderOnlyProps,
} from './screen_reader_only';
export { EuiSkipLink, EuiSkipLinkProps } from './skip_link';
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`EuiScreenReaderLive with a static configuration accepts \`aria-live\` 1`] = `
<div
class="euiScreenReaderOnly"
>
<div
aria-atomic="true"
aria-live="assertive"
role="status"
/>
<div
aria-atomic="true"
aria-live="assertive"
role="status"
>
<p>
This paragraph is not visible to sighted users but will be read by screenreaders.
</p>
</div>
</div>
`;

exports[`EuiScreenReaderLive with a static configuration accepts \`role\` 1`] = `
<div
class="euiScreenReaderOnly"
>
<div
aria-atomic="true"
aria-live="polite"
role="log"
/>
<div
aria-atomic="true"
aria-live="polite"
role="log"
>
<p>
This paragraph is not visible to sighted users but will be read by screenreaders.
</p>
</div>
</div>
`;

exports[`EuiScreenReaderLive with a static configuration does not render screen reader content when inactive 1`] = `
<div
class="euiScreenReaderOnly"
>
<div
aria-atomic="true"
aria-live="polite"
role="status"
/>
<div
aria-atomic="true"
aria-live="polite"
role="status"
/>
</div>
`;

exports[`EuiScreenReaderLive with a static configuration renders screen reader content when active 1`] = `
<div
class="euiScreenReaderOnly"
>
<div
aria-atomic="true"
aria-live="polite"
role="status"
/>
<div
aria-atomic="true"
aria-live="polite"
role="status"
>
<p>
This paragraph is not visible to sighted users but will be read by screenreaders.
</p>
</div>
</div>
`;

exports[`EuiScreenReaderLive with dynamic properties alternates rendering screen reader content into the second live region when changed/toggled 1`] = `
<Component>
<div>
<button
data-test-subj="increment"
onClick={[Function]}
>
Increment
</button>
<EuiScreenReaderLive>
<EuiScreenReaderOnly>
<div
className="euiScreenReaderOnly"
>
<div
aria-atomic="true"
aria-live="polite"
role="status"
/>
<div
aria-atomic="true"
aria-live="polite"
role="status"
>
<p>
Number of active options:
1
</p>
</div>
</div>
</EuiScreenReaderOnly>
</EuiScreenReaderLive>
</div>
</Component>
`;

exports[`EuiScreenReaderLive with dynamic properties initially renders screen reader content in the first live region 1`] = `
<Component>
<div>
<button
data-test-subj="increment"
onClick={[Function]}
>
Increment
</button>
<EuiScreenReaderLive>
<EuiScreenReaderOnly>
<div
className="euiScreenReaderOnly"
>
<div
aria-atomic="true"
aria-live="polite"
role="status"
>
<p>
Number of active options:
0
</p>
</div>
<div
aria-atomic="true"
aria-live="polite"
role="status"
/>
</div>
</EuiScreenReaderOnly>
</EuiScreenReaderLive>
</div>
</Component>
`;
12 changes: 12 additions & 0 deletions src/components/accessibility/screen_reader_live/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export {
EuiScreenReaderLive,
EuiScreenReaderLiveProps,
} from './screen_reader_live';
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { useState } from 'react';
import { mount, render } from 'enzyme';

import { findTestSubject } from '../../../test';

import { EuiScreenReaderLive } from './screen_reader_live';

describe('EuiScreenReaderLive', () => {
describe('with a static configuration', () => {
const content = (
<p>
This paragraph is not visible to sighted users but will be read by
screenreaders.
</p>
);

it('renders screen reader content when active', () => {
const component = render(
<EuiScreenReaderLive isActive={true}>{content}</EuiScreenReaderLive>
);

expect(component).toMatchSnapshot();
});

it('does not render screen reader content when inactive', () => {
const component = render(
<EuiScreenReaderLive isActive={false}>{content}</EuiScreenReaderLive>
);

expect(component).toMatchSnapshot();
});

it('accepts `role`', () => {
const component = render(
<EuiScreenReaderLive role="log">{content}</EuiScreenReaderLive>
);

expect(component).toMatchSnapshot();
});

it('accepts `aria-live`', () => {
const component = render(
<EuiScreenReaderLive aria-live="assertive">
{content}
</EuiScreenReaderLive>
);

expect(component).toMatchSnapshot();
});
});

describe('with dynamic properties', () => {
const Component = () => {
const [activeOptions, setActiveOptions] = useState(0);

return (
<div>
<button
data-test-subj="increment"
onClick={() => setActiveOptions(1)}
>
Increment
</button>
<EuiScreenReaderLive>
<p>Number of active options: {activeOptions}</p>
</EuiScreenReaderLive>
</div>
);
};

it('initially renders screen reader content in the first live region', () => {
const component = mount(<Component />);

expect(component).toMatchSnapshot();
});

it('alternates rendering screen reader content into the second live region when changed/toggled', () => {
const component = mount(<Component />);

findTestSubject(component, 'increment').simulate('click');

expect(component).toMatchSnapshot();
});
});
});
Loading