Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## [`main`](https://github.com/elastic/eui/tree/main)

- Added `EuiScreenReaderLive` component for updateable `aria-live` regions ([#5567](https://github.com/elastic/eui/pull/5567))

**Bug fixes**

- Fixed EuiDataGrid height issue when in full-screen mode and with scrolling content ([#5557](https://github.com/elastic/eui/pull/5557))
Expand Down
30 changes: 30 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,33 @@ 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.
Comment thread
thompsongl marked this conversation as resolved.
Outdated
</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