From de9e5eb05c1d9f3cd5282e09a67e9d2d8fea71e3 Mon Sep 17 00:00:00 2001 From: 1Copenut Date: Thu, 23 Jun 2022 13:15:44 -0500 Subject: [PATCH 01/16] Adding focus control to EuiScreenReaderLive * Updating screen_reader_live to accept focus. * Added two tests for isFocusable behavior. * Adding documentation for isFocusable prop. --- .../guide_page/guide_page_chrome.js | 10 +++++- .../accessibility/accessibility_example.js | 5 +++ src-docs/src/views/app_view.js | 4 +++ .../screen_reader_live.test.tsx.snap | 22 ++++++++++++ .../screen_reader_live.test.tsx | 36 +++++++++++++++---- .../screen_reader_live/screen_reader_live.tsx | 28 +++++++++++++-- .../accessibility/skip_link/skip_link.tsx | 1 + 7 files changed, 95 insertions(+), 11 deletions(-) diff --git a/src-docs/src/components/guide_page/guide_page_chrome.js b/src-docs/src/components/guide_page/guide_page_chrome.js index a295ca89270..aa10e02d514 100644 --- a/src-docs/src/components/guide_page/guide_page_chrome.js +++ b/src-docs/src/components/guide_page/guide_page_chrome.js @@ -7,6 +7,7 @@ import { EuiFlexItem, EuiSideNav, EuiPageSideBar, + EuiScreenReaderOnly, EuiText, } from '../../../../src/components'; @@ -127,7 +128,14 @@ export class GuidePageChrome extends Component { return { id: sectionHref, - name: isCurrentlyOpenSubSection ? {name} : name, + name: isCurrentlyOpenSubSection + ? {name} + : <> + {name} + + - same page + + , href: sectionHref, className: isCurrentlyOpenSubSection ? 'guideSideNav__item--openSubTitle' diff --git a/src-docs/src/views/accessibility/accessibility_example.js b/src-docs/src/views/accessibility/accessibility_example.js index fde94031ff8..ae4a0726a64 100644 --- a/src-docs/src/views/accessibility/accessibility_example.js +++ b/src-docs/src/views/accessibility/accessibility_example.js @@ -144,6 +144,11 @@ export const AccessibilityExample = { {' '} for role to aria-live mapping.

+

+ If you need to manage focus, set the isFocusable prop to true. + This will set focus on the containing element on page load and when the children{' '} + prop updates. Screen readers will announce the text inside the live region as normal. +

Also consider other live region guidelines, such as that live regions must be present on initial page load, and should not be in a diff --git a/src-docs/src/views/app_view.js b/src-docs/src/views/app_view.js index 132653906b8..11b2fbea092 100644 --- a/src-docs/src/views/app_view.js +++ b/src-docs/src/views/app_view.js @@ -10,6 +10,7 @@ import { EuiPage, EuiPageBody, EuiSkipLink, + EuiScreenReaderLive, } from '../../../src/components'; import { keys } from '../../../src/services'; @@ -69,6 +70,9 @@ export const AppView = ({ children, currentRoute }) => { return ( + + {`${currentRoute.name} - Elastic UI Framework`} + `; +exports[`EuiScreenReaderLive with a static configuration accepts \`isFocusable\` 1`] = ` +

+
+
+

+ This paragraph is not visible to sighted users but will be read by screenreaders. +

+
+
+`; + exports[`EuiScreenReaderLive with a static configuration accepts \`role\` 1`] = `
+ This paragraph is not visible to sighted users but will be read by + screenreaders. +

+); + describe('EuiScreenReaderLive', () => { describe('with a static configuration', () => { - const content = ( -

- This paragraph is not visible to sighted users but will be read by - screenreaders. -

- ); - it('renders screen reader content when active', () => { const component = render( {content} @@ -55,6 +55,16 @@ describe('EuiScreenReaderLive', () => { expect(component).toMatchSnapshot(); }); + + it('accepts `isFocusable`', () => { + const component = render( + + {content} + + ); + + expect(component).toMatchSnapshot(); + }); }); describe('with dynamic properties', () => { @@ -90,4 +100,16 @@ describe('EuiScreenReaderLive', () => { expect(component).toMatchSnapshot(); }); }); + + describe('with focus behavior', () => { + it('sets focus correctly', () => { + const component = mount( + {content} + ); + + const focusableDiv = component.find('div').at(0); + + expect(focusableDiv.is(':focus')).toBe(true); + }); + }); }); diff --git a/src/components/accessibility/screen_reader_live/screen_reader_live.tsx b/src/components/accessibility/screen_reader_live/screen_reader_live.tsx index 88d4a256497..19a8cde35e3 100644 --- a/src/components/accessibility/screen_reader_live/screen_reader_live.tsx +++ b/src/components/accessibility/screen_reader_live/screen_reader_live.tsx @@ -12,6 +12,7 @@ import React, { FunctionComponent, ReactNode, useEffect, + useRef, useState, } from 'react'; @@ -36,6 +37,11 @@ export interface EuiScreenReaderLiveProps { * `aria-live` attribute for both live regions */ 'aria-live'?: AriaAttributes['aria-live']; + /** + * Adds a tabindex={-1} to containing div. Focus is set on first + * component render and updates to `children` prop. + */ + isFocusable?: boolean; } export const EuiScreenReaderLive: FunctionComponent = ({ @@ -43,11 +49,17 @@ export const EuiScreenReaderLive: FunctionComponent = isActive = true, role = 'status', 'aria-live': ariaLive = 'polite', + isFocusable = false, }) => { const [toggle, setToggle] = useState(false); + const focusRef = useRef(null); useEffect(() => { setToggle((toggle) => !toggle); + + if (focusRef.current !== null && isFocusable) { + focusRef.current.focus(); + } }, [children]); return ( @@ -62,11 +74,21 @@ export const EuiScreenReaderLive: FunctionComponent = * for more examples of the double region approach. */ -
-
+
+
{isActive && toggle ? children : ''}
-
+
{isActive && !toggle ? children : ''}
diff --git a/src/components/accessibility/skip_link/skip_link.tsx b/src/components/accessibility/skip_link/skip_link.tsx index d88e76be080..a55ecb84288 100644 --- a/src/components/accessibility/skip_link/skip_link.tsx +++ b/src/components/accessibility/skip_link/skip_link.tsx @@ -103,6 +103,7 @@ export const EuiSkipLink: FunctionComponent = ({ Date: Thu, 23 Jun 2022 15:10:32 -0500 Subject: [PATCH 02/16] Updating skip link tests. --- .../components/guide_page/guide_page_chrome.js | 18 ++++++++++-------- .../accessibility/accessibility_example.js | 8 +++++--- .../screen_reader_live.test.tsx | 4 +--- .../screen_reader_live/screen_reader_live.tsx | 2 +- .../__snapshots__/skip_link.test.tsx.snap | 12 ++++++------ 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src-docs/src/components/guide_page/guide_page_chrome.js b/src-docs/src/components/guide_page/guide_page_chrome.js index aa10e02d514..1924e848742 100644 --- a/src-docs/src/components/guide_page/guide_page_chrome.js +++ b/src-docs/src/components/guide_page/guide_page_chrome.js @@ -128,14 +128,16 @@ export class GuidePageChrome extends Component { return { id: sectionHref, - name: isCurrentlyOpenSubSection - ? {name} - : <> - {name} - - - same page - - , + name: isCurrentlyOpenSubSection ? ( + {name} + ) : ( + <> + {name} + + - same page + + + ), href: sectionHref, className: isCurrentlyOpenSubSection ? 'guideSideNav__item--openSubTitle' diff --git a/src-docs/src/views/accessibility/accessibility_example.js b/src-docs/src/views/accessibility/accessibility_example.js index ae4a0726a64..cb336e800ce 100644 --- a/src-docs/src/views/accessibility/accessibility_example.js +++ b/src-docs/src/views/accessibility/accessibility_example.js @@ -145,9 +145,11 @@ export const AccessibilityExample = { for role to aria-live mapping.

- If you need to manage focus, set the isFocusable prop to true. - This will set focus on the containing element on page load and when the children{' '} - prop updates. Screen readers will announce the text inside the live region as normal. + If you need to manage focus, set the isFocusable{' '} + prop to true. This will set focus on the + containing element on page load and when the{' '} + children prop updates. Screen readers will + announce the text inside the live region as normal.

Also consider other live region guidelines, such as that live diff --git a/src/components/accessibility/screen_reader_live/screen_reader_live.test.tsx b/src/components/accessibility/screen_reader_live/screen_reader_live.test.tsx index eddcac3ab39..5bbff41c896 100644 --- a/src/components/accessibility/screen_reader_live/screen_reader_live.test.tsx +++ b/src/components/accessibility/screen_reader_live/screen_reader_live.test.tsx @@ -58,9 +58,7 @@ describe('EuiScreenReaderLive', () => { it('accepts `isFocusable`', () => { const component = render( - - {content} - + {content} ); expect(component).toMatchSnapshot(); diff --git a/src/components/accessibility/screen_reader_live/screen_reader_live.tsx b/src/components/accessibility/screen_reader_live/screen_reader_live.tsx index 19a8cde35e3..71ecd8cd409 100644 --- a/src/components/accessibility/screen_reader_live/screen_reader_live.tsx +++ b/src/components/accessibility/screen_reader_live/screen_reader_live.tsx @@ -60,7 +60,7 @@ export const EuiScreenReaderLive: FunctionComponent = if (focusRef.current !== null && isFocusable) { focusRef.current.focus(); } - }, [children]); + }, [children, isFocusable]); return ( /** diff --git a/src/components/accessibility/skip_link/__snapshots__/skip_link.test.tsx.snap b/src/components/accessibility/skip_link/__snapshots__/skip_link.test.tsx.snap index 54a9ea2440a..12bfdb61b43 100644 --- a/src/components/accessibility/skip_link/__snapshots__/skip_link.test.tsx.snap +++ b/src/components/accessibility/skip_link/__snapshots__/skip_link.test.tsx.snap @@ -3,7 +3,7 @@ exports[`EuiSkipLink is rendered 1`] = ` @@ -38,7 +38,7 @@ exports[`EuiSkipLink props onClick is rendered 1`] = ` exports[`EuiSkipLink props position absolute is rendered 1`] = ` @@ -54,7 +54,7 @@ exports[`EuiSkipLink props position absolute is rendered 1`] = ` exports[`EuiSkipLink props position fixed is rendered 1`] = ` @@ -87,7 +87,7 @@ exports[`EuiSkipLink props position static is rendered 1`] = ` exports[`EuiSkipLink props tabIndex is rendered 1`] = ` Date: Thu, 23 Jun 2022 15:20:14 -0500 Subject: [PATCH 03/16] Adding CHANGELOG entry. --- upcoming_changelogs/5995.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 upcoming_changelogs/5995.md diff --git a/upcoming_changelogs/5995.md b/upcoming_changelogs/5995.md new file mode 100644 index 00000000000..c04b3b7b746 --- /dev/null +++ b/upcoming_changelogs/5995.md @@ -0,0 +1 @@ +- Added `isFocusable` prop to `EuiScreenReaderLive` From 29f3e4f9e3cb5f0041fd919d9da82db384db6e05 Mon Sep 17 00:00:00 2001 From: 1Copenut Date: Fri, 24 Jun 2022 11:24:40 -0500 Subject: [PATCH 04/16] Splitting useEffects, making skip link color dynamic. --- src-docs/src/views/app_view.js | 3 ++- .../screen_reader_live.test.tsx | 8 +++++-- .../screen_reader_live/screen_reader_live.tsx | 24 +++++++++++-------- .../__snapshots__/skip_link.test.tsx.snap | 12 +++++----- .../accessibility/skip_link/skip_link.tsx | 14 ++++++++++- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src-docs/src/views/app_view.js b/src-docs/src/views/app_view.js index 11b2fbea092..afa0bd36a2b 100644 --- a/src-docs/src/views/app_view.js +++ b/src-docs/src/views/app_view.js @@ -70,10 +70,11 @@ export const AppView = ({ children, currentRoute }) => { return ( - + {`${currentRoute.name} - Elastic UI Framework`} { it('accepts `isFocusable`', () => { const component = render( - {content} + + {content} + ); expect(component).toMatchSnapshot(); @@ -102,7 +104,9 @@ describe('EuiScreenReaderLive', () => { describe('with focus behavior', () => { it('sets focus correctly', () => { const component = mount( - {content} + + {content} + ); const focusableDiv = component.find('div').at(0); diff --git a/src/components/accessibility/screen_reader_live/screen_reader_live.tsx b/src/components/accessibility/screen_reader_live/screen_reader_live.tsx index 71ecd8cd409..c42ea6a0756 100644 --- a/src/components/accessibility/screen_reader_live/screen_reader_live.tsx +++ b/src/components/accessibility/screen_reader_live/screen_reader_live.tsx @@ -38,10 +38,12 @@ export interface EuiScreenReaderLiveProps { */ 'aria-live'?: AriaAttributes['aria-live']; /** - * Adds a tabindex={-1} to containing div. Focus is set on first - * component render and updates to `children` prop. + * 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. */ - isFocusable?: boolean; + focusRegionOnTextChange?: boolean; } export const EuiScreenReaderLive: FunctionComponent = ({ @@ -49,18 +51,20 @@ export const EuiScreenReaderLive: FunctionComponent = isActive = true, role = 'status', 'aria-live': ariaLive = 'polite', - isFocusable = false, + focusRegionOnTextChange = false, }) => { const [toggle, setToggle] = useState(false); const focusRef = useRef(null); useEffect(() => { setToggle((toggle) => !toggle); + }, [children]); - if (focusRef.current !== null && isFocusable) { + useEffect(() => { + if (focusRef.current !== null && focusRegionOnTextChange) { focusRef.current.focus(); } - }, [children, isFocusable]); + }, [focusRegionOnTextChange]); return ( /** @@ -74,20 +78,20 @@ export const EuiScreenReaderLive: FunctionComponent = * for more examples of the double region approach. */ -

+
{isActive && toggle ? children : ''}
{isActive && !toggle ? children : ''}
diff --git a/src/components/accessibility/skip_link/__snapshots__/skip_link.test.tsx.snap b/src/components/accessibility/skip_link/__snapshots__/skip_link.test.tsx.snap index 12bfdb61b43..54a9ea2440a 100644 --- a/src/components/accessibility/skip_link/__snapshots__/skip_link.test.tsx.snap +++ b/src/components/accessibility/skip_link/__snapshots__/skip_link.test.tsx.snap @@ -3,7 +3,7 @@ exports[`EuiSkipLink is rendered 1`] = `
@@ -38,7 +38,7 @@ exports[`EuiSkipLink props onClick is rendered 1`] = ` exports[`EuiSkipLink props position absolute is rendered 1`] = ` @@ -54,7 +54,7 @@ exports[`EuiSkipLink props position absolute is rendered 1`] = ` exports[`EuiSkipLink props position fixed is rendered 1`] = ` @@ -87,7 +87,7 @@ exports[`EuiSkipLink props position static is rendered 1`] = ` exports[`EuiSkipLink props tabIndex is rendered 1`] = ` = ({ position = 'static', children, className, + color = 'primary', ...rest }) => { const euiTheme = useEuiTheme(); @@ -103,7 +115,7 @@ export const EuiSkipLink: FunctionComponent = ({ Date: Fri, 24 Jun 2022 11:49:10 -0700 Subject: [PATCH 05/16] Fix toggle announcing too much on sub section link clicks --- src-docs/src/views/app_view.js | 2 +- .../accessibility/screen_reader_live/screen_reader_live.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src-docs/src/views/app_view.js b/src-docs/src/views/app_view.js index afa0bd36a2b..21e61b29828 100644 --- a/src-docs/src/views/app_view.js +++ b/src-docs/src/views/app_view.js @@ -71,7 +71,7 @@ export const AppView = ({ children, currentRoute }) => { return ( - {`${currentRoute.name} - Elastic UI Framework`} + {`${currentRoute.name} - Elastic UI Framework`} = if (focusRef.current !== null && focusRegionOnTextChange) { focusRef.current.focus(); } - }, [focusRegionOnTextChange]); + }, [toggle, focusRegionOnTextChange]); return ( /** From 65dc9e155b58782e23fa7da945bf6e1db3b0f1ef Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Fri, 24 Jun 2022 12:01:08 -0700 Subject: [PATCH 06/16] Improve subsection links screen reader experience --- .../components/guide_page/guide_page_chrome.js | 12 +----------- src-docs/src/components/scroll_to_hash.tsx | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src-docs/src/components/guide_page/guide_page_chrome.js b/src-docs/src/components/guide_page/guide_page_chrome.js index 1924e848742..1e5064c0bf0 100644 --- a/src-docs/src/components/guide_page/guide_page_chrome.js +++ b/src-docs/src/components/guide_page/guide_page_chrome.js @@ -7,7 +7,6 @@ import { EuiFlexItem, EuiSideNav, EuiPageSideBar, - EuiScreenReaderOnly, EuiText, } from '../../../../src/components'; @@ -128,16 +127,7 @@ export class GuidePageChrome extends Component { return { id: sectionHref, - name: isCurrentlyOpenSubSection ? ( - {name} - ) : ( - <> - {name} - - - same page - - - ), + name: isCurrentlyOpenSubSection ? {name} : <>{name}, href: sectionHref, className: isCurrentlyOpenSubSection ? 'guideSideNav__item--openSubTitle' diff --git a/src-docs/src/components/scroll_to_hash.tsx b/src-docs/src/components/scroll_to_hash.tsx index 23a6bb59b27..dcd667a4856 100644 --- a/src-docs/src/components/scroll_to_hash.tsx +++ b/src-docs/src/components/scroll_to_hash.tsx @@ -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(); @@ -18,11 +19,25 @@ const ScrollToHash: FunctionComponent = () => { const element = document.getElementById(hash); const headerOffset = 48; if (element) { + // Focus header for keyboard and screen reader users + if (!isTabbable(element)) { + element.tabIndex = -1; + element.addEventListener( + 'blur', + () => { + element.removeAttribute('tabindex'); + }, + { once: true } + ); + element.focus(); + } + // Scroll to header window.scrollTo({ top: element.offsetTop - headerOffset, behavior: 'smooth', }); } else { + // Scroll back to top of page window.scrollTo({ behavior: 'auto', top: 0, From 7288e1e58d12324b4767de614a65cbea91ade89c Mon Sep 17 00:00:00 2001 From: 1Copenut Date: Fri, 24 Jun 2022 14:55:19 -0500 Subject: [PATCH 07/16] Updating one test after in-page link improvement. --- .../__snapshots__/screen_reader_live.test.tsx.snap | 7 +++++++ .../screen_reader_live/screen_reader_live.tsx | 4 ++++ .../selectable/__snapshots__/selectable.test.tsx.snap | 1 + upcoming_changelogs/5995.md | 2 +- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/accessibility/screen_reader_live/__snapshots__/screen_reader_live.test.tsx.snap b/src/components/accessibility/screen_reader_live/__snapshots__/screen_reader_live.test.tsx.snap index 645fe81f09c..5ded7ea0b5d 100644 --- a/src/components/accessibility/screen_reader_live/__snapshots__/screen_reader_live.test.tsx.snap +++ b/src/components/accessibility/screen_reader_live/__snapshots__/screen_reader_live.test.tsx.snap @@ -6,6 +6,7 @@ exports[`EuiScreenReaderLive with a static configuration accepts \`aria-live\` 1 >