Skip to content

Commit

Permalink
Update EuiScreenReaderLive and use it in EUI Docs to announce page …
Browse files Browse the repository at this point in the history
…loads to screen readers (#5995)

* Adding focus control to EuiScreenReaderLive

* Updating screen_reader_live to accept focus.
* Added two tests for isFocusable behavior.
* Adding documentation for isFocusable prop.

* Updating skip link tests.

* Adding CHANGELOG entry.

* Splitting useEffects, making skip link color dynamic.

* Fix toggle announcing too much on sub section link clicks

* Improve subsection links screen reader experience

* Updating one test after in-page link improvement.

* Renaming a prop in unit tests.

* misc tweaks/fixes to previous constance commits

- actually revert guide_page_chrome to original code

- fix scroll_to_hash comments, syntax

* [PR feedback] EuiSkipLink `color` prop

- DRY out ButtonColor typing
- remove unnecessary `= 'primary'` fallback - EuiButton already has this by default, so passing undefined is fine

* [PR feedback] Revert EuiSkipLink changes

- the component already extends `EuiButtonProps` and already accepts `color` with all the necessary typing, which I only just realized 🤦

* Update src-docs/src/views/accessibility/accessibility_example.js

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* Update src/components/accessibility/screen_reader_live/screen_reader_live.tsx

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* Add documentation section for `focusRegionOnTextChange` + example

* Messed up a snippet

* Fix documentation example on small screens

Co-authored-by: Constance Chen <constance.chen.3@gmail.com>
Co-authored-by: Constance <constancecchen@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 28, 2022
1 parent 48db5bb commit 6e72317
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 10 deletions.
13 changes: 13 additions & 0 deletions src-docs/src/components/scroll_to_hash.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useState, FunctionComponent } from 'react';
import { useLocation } from 'react-router-dom';
import { isTabbable } from 'tabbable';

const ScrollToHash: FunctionComponent = () => {
const location = useLocation();
Expand All @@ -18,11 +19,23 @@ const ScrollToHash: FunctionComponent = () => {
const element = document.getElementById(hash);
const headerOffset = 48;
if (element) {
// Focus element for keyboard and screen reader users
if (!isTabbable(element)) {
element.tabIndex = -1;
element.addEventListener(
'blur',
() => element.removeAttribute('tabindex'),
{ once: true }
);
element.focus();
}
// Scroll to element
window.scrollTo({
top: element.offsetTop - headerOffset,
behavior: 'smooth',
});
} else {
// Scroll back to top of page
window.scrollTo({
behavior: 'auto',
top: 0,
Expand Down
44 changes: 44 additions & 0 deletions src-docs/src/views/accessibility/accessibility_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
} from '../../../../src';

import ScreenReaderLive from './screen_reader_live';
import ScreenReaderLiveFocus from './screen_reader_live_focus';
import ScreenReaderOnly from './screen_reader';
import ScreenReaderFocus from './screen_reader_focus';
import SkipLink from './skip_link';
import StylesHelpers from './styles_helpers';

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

Expand Down Expand Up @@ -154,8 +156,50 @@ export const AccessibilityExample = {
props: {
EuiScreenReaderLive,
},
snippet: `<EuiScreenReaderLive>
<!-- visually hidden announced content -->
</EuiScreenReaderLive>`,
demo: <ScreenReaderLive />,
},
{
text: (
<>
<h3>Auto-focusing the live region on text change</h3>
<p>
The <EuiCode>focusRegionOnTextChange</EuiCode> prop will
automatically focus the <EuiCode>EuiScreenReaderLive</EuiCode>{' '}
region (causing screen readers to read out the text content)
whenever <EuiCode>children</EuiCode> changes.
</p>
<p>
This is primarily useful for announcing navigation or page changes,
when programmatically resetting focus location back to a certain
part of the page (where the <EuiCode>EuiScreenReaderLive</EuiCode>{' '}
is placed) is desired.
</p>
<p>
<em>
Using a screen reader, click the following navigation links and
notice that when the new page is announced, focus is also set to
the top of the body content.
</em>
</p>
</>
),
props: {
EuiScreenReaderLive,
},
snippet: `<EuiScreenReaderLive focusRegionOnTextChange>
<!-- visually hidden content, focused and announced on text change -->
</EuiScreenReaderLive>`,
demo: <ScreenReaderLiveFocus />,
source: [
{
type: GuideSectionTypes.TSX,
code: screenReaderLiveFocusSource,
},
],
},
{
title: 'Skip link',
source: [
Expand Down
63 changes: 63 additions & 0 deletions src-docs/src/views/accessibility/screen_reader_live_focus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState } from 'react';

import {
EuiScreenReaderLive,
EuiPageTemplate,
EuiSideNav,
EuiButton,
} from '../../../../src/components';
import { htmlIdGenerator } from '../../../../src/services';

export default () => {
const idGenerator = htmlIdGenerator('focusRegionOnTextChange');

const [pageTitle, setPageTitle] = useState('Home');

const sideNav = [
{
name: 'Example side nav',
id: idGenerator(),
items: [
{
name: 'Home',
id: idGenerator(),
onClick: () => setPageTitle('Home'),
},
{
name: 'About',
id: idGenerator(),
onClick: () => setPageTitle('About'),
},
{
name: 'Docs',
id: idGenerator(),
onClick: () => setPageTitle('Docs'),
},
{
name: 'Contact',
id: idGenerator(),
onClick: () => setPageTitle('Contact'),
},
],
},
];

return (
<>
<EuiPageTemplate
pageSideBar={<EuiSideNav items={sideNav} isOpenOnMobile />}
pageHeader={{
iconType: 'logoElastic',
pageTitle: pageTitle,
}}
>
<EuiScreenReaderLive focusRegionOnTextChange>
{pageTitle}
</EuiScreenReaderLive>
<EuiButton>
Clicking a nav link and then pressing tab should focus this button
</EuiButton>
</EuiPageTemplate>
</>
);
};
5 changes: 5 additions & 0 deletions src-docs/src/views/app_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
EuiPage,
EuiPageBody,
EuiSkipLink,
EuiScreenReaderLive,
} from '../../../src/components';

import { keys } from '../../../src/services';
Expand Down Expand Up @@ -69,7 +70,11 @@ export const AppView = ({ children, currentRoute }) => {

return (
<LinkWrapper>
<EuiScreenReaderLive focusRegionOnTextChange>
{`${currentRoute.name} - Elastic UI Framework`}
</EuiScreenReaderLive>
<EuiSkipLink
color="ghost"
destinationId="start-of-content"
position="fixed"
overrideLinkBehavior
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports[`EuiScreenReaderLive with a static configuration accepts \`aria-live\` 1
>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="assertive"
role="status"
/>
Expand All @@ -21,12 +22,36 @@ exports[`EuiScreenReaderLive with a static configuration accepts \`aria-live\` 1
</div>
`;

exports[`EuiScreenReaderLive with a static configuration accepts \`focusRegionOnTextChange\` 1`] = `
<div
class="emotion-euiScreenReaderOnly"
tabindex="-1"
>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="off"
role="status"
/>
<div
aria-atomic="true"
aria-live="off"
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="emotion-euiScreenReaderOnly"
>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="polite"
role="log"
/>
Expand All @@ -48,6 +73,7 @@ exports[`EuiScreenReaderLive with a static configuration does not render screen
>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="polite"
role="status"
/>
Expand All @@ -65,6 +91,7 @@ exports[`EuiScreenReaderLive with a static configuration renders screen reader c
>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="polite"
role="status"
/>
Expand Down Expand Up @@ -92,6 +119,7 @@ exports[`EuiScreenReaderLive with dynamic properties alternates rendering screen
>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="polite"
role="status"
/>
Expand Down Expand Up @@ -120,6 +148,7 @@ exports[`EuiScreenReaderLive with dynamic properties initially renders screen re
>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="polite"
role="status"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import { findTestSubject } from '../../../test';

import { EuiScreenReaderLive } from './screen_reader_live';

const content = (
<p>
This paragraph is not visible to sighted users but will be read by
screenreaders.
</p>
);

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>
Expand Down Expand Up @@ -55,6 +55,16 @@ describe('EuiScreenReaderLive', () => {

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

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

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

describe('with dynamic properties', () => {
Expand Down Expand Up @@ -90,4 +100,18 @@ describe('EuiScreenReaderLive', () => {
expect(component.render()).toMatchSnapshot();
});
});

describe('with focus behavior', () => {
it('sets focus correctly', () => {
const component = mount(
<EuiScreenReaderLive focusRegionOnTextChange={true}>
{content}
</EuiScreenReaderLive>
);

const focusableDiv = component.find('div').at(0);

expect(focusableDiv.is(':focus')).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React, {
FunctionComponent,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';

Expand All @@ -36,20 +37,35 @@ export interface EuiScreenReaderLiveProps {
* `aria-live` attribute for both live regions
*/
'aria-live'?: AriaAttributes['aria-live'];
/**
* On `children`/text change, the region will auto-focus itself, causing screen readers
* to automatically read out the text content. This prop should primarily be used for
* navigation or page changes, where programmatically resetting focus location back to
* a certain part of the page is desired.
*/
focusRegionOnTextChange?: boolean;
}

export const EuiScreenReaderLive: FunctionComponent<EuiScreenReaderLiveProps> = ({
children,
isActive = true,
role = 'status',
'aria-live': ariaLive = 'polite',
focusRegionOnTextChange = false,
}) => {
const [toggle, setToggle] = useState(false);
const focusRef = useRef<HTMLDivElement>(null);

useEffect(() => {
setToggle((toggle) => !toggle);
}, [children]);

useEffect(() => {
if (focusRef.current !== null && focusRegionOnTextChange) {
focusRef.current.focus();
}
}, [toggle, focusRegionOnTextChange]);

return (
/**
* Intentionally uses two persistent live regions with oscillating content updates.
Expand All @@ -62,11 +78,23 @@ export const EuiScreenReaderLive: FunctionComponent<EuiScreenReaderLiveProps> =
* for more examples of the double region approach.
*/
<EuiScreenReaderOnly>
<div>
<div role={role} aria-atomic="true" aria-live={ariaLive}>
<div ref={focusRef} tabIndex={focusRegionOnTextChange ? -1 : undefined}>
<div
role={role}
aria-atomic="true"
// Setting `aria-hidden` and setting `aria-live` to "off" prevents
// double announcements from VO when `focusRegionOnTextChange` is true
aria-hidden={toggle ? undefined : 'true'}
aria-live={focusRegionOnTextChange ? 'off' : ariaLive}
>
{isActive && toggle ? children : ''}
</div>
<div role={role} aria-atomic="true" aria-live={ariaLive}>
<div
role={role}
aria-atomic="true"
aria-hidden={!toggle ? undefined : 'true'}
aria-live={focusRegionOnTextChange ? 'off' : ariaLive}
>
{isActive && !toggle ? children : ''}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ exports[`EuiSelectable search value supports inheriting initialSearchValue from
>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="polite"
role="status"
/>
Expand Down
1 change: 1 addition & 0 deletions upcoming_changelogs/5995.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added `focusRegionOnTextChange` prop to `EuiScreenReaderLive`

0 comments on commit 6e72317

Please sign in to comment.