diff --git a/src-docs/src/views/skeleton/skeleton_example.js b/src-docs/src/views/skeleton/skeleton_example.js index be4253f7eae..eb6ea409273 100644 --- a/src-docs/src/views/skeleton/skeleton_example.js +++ b/src-docs/src/views/skeleton/skeleton_example.js @@ -1,4 +1,5 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import { GuideSectionTypes } from '../../components'; import { @@ -7,6 +8,7 @@ import { EuiSkeletonRectangle, EuiSkeletonCircle, EuiSkeletonLoading, + EuiScreenReaderLive, EuiText, EuiSpacer, EuiCallOut, @@ -70,6 +72,15 @@ const skeletonLoadingSnippet = ``; +import SkeletonLiveProps from './skeleton_live_props'; +const skeletonLivePropsSource = require('!!raw-loader!./skeleton_live_props'); +const skeletonLivePropsSnippet = ``; + export const SkeletonExample = { title: 'Skeleton', intro: ( @@ -222,5 +233,42 @@ export const SkeletonExample = { snippet: skeletonLoadingSnippet, demo: , }, + { + title: 'Customizing screen reader announcements', + source: [ + { + type: GuideSectionTypes.JS, + code: skeletonLivePropsSource, + }, + ], + text: ( + +

+ EuiSkeleton components assume that the page starts + as loading and transitions to loaded, and the default screen reader + experience is set up accordingly ( + announceLoadedStatus={'{true}'}). +

+

+ In scenarios where that is not the case (i.e., transitioning to + loading), you can customize what statuses are announced to screen + readers by setting announceLoadingStatus to true, + or announceLoadedStatus to false. Submitting the + below example announces a loading status, but not a loaded status. +

+

+ As an optional escape hatch, ariaLiveProps is + also available and accepts any{' '} + + EuiScreenReaderLive + {' '} + props. +

+
+ ), + props: { EuiSkeletonLoading, EuiScreenReaderLive }, + snippet: skeletonLivePropsSnippet, + demo: , + }, ], }; diff --git a/src-docs/src/views/skeleton/skeleton_live_props.tsx b/src-docs/src/views/skeleton/skeleton_live_props.tsx new file mode 100644 index 00000000000..22922224ff4 --- /dev/null +++ b/src-docs/src/views/skeleton/skeleton_live_props.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; + +import { + EuiSkeletonLoading, + EuiSkeletonRectangle, + EuiFieldText, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + useEuiTheme, +} from '../../../../src'; + +export default () => { + const [isLoading, setIsLoading] = useState(false); + + const simulateLoading = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, 3000); + }; + + const { euiTheme } = useEuiTheme(); + + return ( + <> + + } + loadedContent={ +
+ + + + + + + Submit + + + + Cancel + + +
+ } + /> + + ); +}; diff --git a/src/components/skeleton/__snapshots__/skeleton_loading.test.tsx.snap b/src/components/skeleton/__snapshots__/skeleton_loading.test.tsx.snap index 64eebfe087a..717965124f5 100644 --- a/src/components/skeleton/__snapshots__/skeleton_loading.test.tsx.snap +++ b/src/components/skeleton/__snapshots__/skeleton_loading.test.tsx.snap @@ -1,5 +1,74 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`EuiSkeletonLoading aria-live behavior announceLoadedStatus - allows turning off live announcements 1`] = ` +
+ +
+`; + +exports[`EuiSkeletonLoading aria-live behavior announceLoadingStatus - allows enabling live announcements 1`] = ` +
+
+
+ Loading Sample user data +
+ + +
+`; + +exports[`EuiSkeletonLoading aria-live behavior ariaLiveProps 1`] = ` +
+
+
+ Loaded Sample user data +
+ + +
+`; + exports[`EuiSkeletonLoading renders \`loadedContent\` when \`isLoading\` is false 1`] = `
= ({ size = 'm', className, contentAriaLabel, + announceLoadingStatus, + announceLoadedStatus, + ariaLiveProps, ariaWrapperProps, children, ...rest @@ -49,6 +52,9 @@ export const EuiSkeletonCircle: FunctionComponent = ({ } loadedContent={children || ''} contentAriaLabel={contentAriaLabel} + announceLoadingStatus={announceLoadingStatus} + announceLoadedStatus={announceLoadedStatus} + ariaLiveProps={ariaLiveProps} {...ariaWrapperProps} /> ); diff --git a/src/components/skeleton/skeleton_loading.test.tsx b/src/components/skeleton/skeleton_loading.test.tsx index 02d3e0121d2..6a2a774f627 100644 --- a/src/components/skeleton/skeleton_loading.test.tsx +++ b/src/components/skeleton/skeleton_loading.test.tsx @@ -43,4 +43,41 @@ describe('EuiSkeletonLoading', () => { expect(queryByTestSubject('loaded')).toBeTruthy(); expect(container.firstChild).toMatchSnapshot(); }); + + describe('aria-live behavior', () => { + test('announceLoadingStatus - allows enabling live announcements', () => { + const { container, getByText } = render( + + ); + + expect(getByText('Loading Sample user data')).toBeTruthy(); + expect(container.firstChild).toMatchSnapshot(); + }); + + test('announceLoadedStatus - allows turning off live announcements', () => { + const { container, queryByText } = render( + + ); + + expect(queryByText('Loaded Sample user data')).toBeFalsy(); + expect(container.firstChild).toMatchSnapshot(); + }); + + test('ariaLiveProps', () => { + const { container, getByRole } = render( + + ); + + expect(getByRole('alert')).toBeTruthy(); + expect(container.firstChild).toMatchSnapshot(); + }); + }); }); diff --git a/src/components/skeleton/skeleton_loading.tsx b/src/components/skeleton/skeleton_loading.tsx index 400e8e8a4a9..23711fabab2 100644 --- a/src/components/skeleton/skeleton_loading.tsx +++ b/src/components/skeleton/skeleton_loading.tsx @@ -9,7 +9,10 @@ import React, { FunctionComponent, HTMLAttributes, ReactElement } from 'react'; import { CommonProps } from '../common'; -import { EuiScreenReaderLive } from '../accessibility/screen_reader_live'; +import { + EuiScreenReaderLive, + EuiScreenReaderLiveProps, +} from '../accessibility/screen_reader_live'; import { useEuiI18n } from '../i18n'; export type _EuiSkeletonAriaProps = { @@ -24,14 +27,29 @@ export type _EuiSkeletonAriaProps = { */ contentAriaLabel?: string; /** - * Any optional props to pass to the `aria-busy` wrapper around the skeleton content + * Makes a live screen reader announcement when `isLoading` is true + * @default false + */ + announceLoadingStatus?: boolean; + /** + * Makes a live screen reader announcement when `isLoading` is false + * @default true + */ + announceLoadedStatus?: boolean; + /** + * Optional props to pass to the `aria-live` region that announces the loading status to screen readers. + * Accepts any `EuiScreenReaderLive` props. + */ + ariaLiveProps?: Partial; + /** + * Optional props to pass to the `aria-busy` wrapper around the skeleton content */ ariaWrapperProps?: HTMLAttributes; }; export type EuiSkeletonLoadingProps = CommonProps & _EuiSkeletonAriaProps['ariaWrapperProps'] & - Pick<_EuiSkeletonAriaProps, 'isLoading' | 'contentAriaLabel'> & { + Omit<_EuiSkeletonAriaProps, 'ariaWrapperProps'> & { /** * Content to display when loading */ @@ -45,24 +63,29 @@ export type EuiSkeletonLoadingProps = CommonProps & export const EuiSkeletonLoading: FunctionComponent = ({ isLoading = true, contentAriaLabel, - loadingContent, + loadingContent: _loadingContent, loadedContent, + announceLoadingStatus = false, + announceLoadedStatus = true, + ariaLiveProps, ...rest }) => { - const loadingAriaLabel = useEuiI18n( - 'euiSkeletonLoading.loadingAriaText', - 'Loading {contentAriaLabel}', - { contentAriaLabel } - ); const loadedAriaLive = useEuiI18n( 'euiSkeletonLoading.loadedAriaText', 'Loaded {contentAriaLabel}', { contentAriaLabel } ); + + const loadingAriaLabel = useEuiI18n( + 'euiSkeletonLoading.loadingAriaText', + 'Loading {contentAriaLabel}', + { contentAriaLabel } + ); const loadingProps = { 'aria-label': loadingAriaLabel, role: 'progressbar', }; + const loadingContent = React.cloneElement(_loadingContent, loadingProps); return (
= ({ {...rest} > {isLoading ? ( - React.cloneElement(loadingContent, loadingProps) + <> + {announceLoadingStatus && ( + + {loadingAriaLabel} + + )} + {loadingContent} + ) : ( <> - {loadedAriaLive} + {announceLoadedStatus && ( + + {loadedAriaLive} + + )} {loadedContent} )} diff --git a/src/components/skeleton/skeleton_rectangle.tsx b/src/components/skeleton/skeleton_rectangle.tsx index b0b6d6a0284..154eb1a9a48 100644 --- a/src/components/skeleton/skeleton_rectangle.tsx +++ b/src/components/skeleton/skeleton_rectangle.tsx @@ -35,6 +35,9 @@ export const EuiSkeletonRectangle: FunctionComponent style, className, contentAriaLabel, + announceLoadingStatus, + announceLoadedStatus, + ariaLiveProps, ariaWrapperProps, children, ...rest @@ -56,6 +59,9 @@ export const EuiSkeletonRectangle: FunctionComponent } loadedContent={children || ''} contentAriaLabel={contentAriaLabel} + announceLoadingStatus={announceLoadingStatus} + announceLoadedStatus={announceLoadedStatus} + ariaLiveProps={ariaLiveProps} {...ariaWrapperProps} /> ); diff --git a/src/components/skeleton/skeleton_text.tsx b/src/components/skeleton/skeleton_text.tsx index ac29cbeb8ee..437cccaab08 100644 --- a/src/components/skeleton/skeleton_text.tsx +++ b/src/components/skeleton/skeleton_text.tsx @@ -38,6 +38,9 @@ export const EuiSkeletonText: FunctionComponent = ({ size = 'm', className, contentAriaLabel, + announceLoadingStatus, + announceLoadedStatus, + ariaLiveProps, ariaWrapperProps, children, ...rest @@ -61,6 +64,9 @@ export const EuiSkeletonText: FunctionComponent = ({ } loadedContent={children || ''} contentAriaLabel={contentAriaLabel} + announceLoadingStatus={announceLoadingStatus} + announceLoadedStatus={announceLoadedStatus} + ariaLiveProps={ariaLiveProps} {...ariaWrapperProps} /> ); diff --git a/src/components/skeleton/skeleton_title.tsx b/src/components/skeleton/skeleton_title.tsx index 4d70210af41..7186bd3ed8e 100644 --- a/src/components/skeleton/skeleton_title.tsx +++ b/src/components/skeleton/skeleton_title.tsx @@ -30,6 +30,9 @@ export const EuiSkeletonTitle: FunctionComponent = ({ size = 'm', className, contentAriaLabel, + announceLoadingStatus, + announceLoadedStatus, + ariaLiveProps, ariaWrapperProps, children, ...rest @@ -50,6 +53,9 @@ export const EuiSkeletonTitle: FunctionComponent = ({ } loadedContent={children || ''} contentAriaLabel={contentAriaLabel} + announceLoadingStatus={announceLoadingStatus} + announceLoadedStatus={announceLoadedStatus} + ariaLiveProps={ariaLiveProps} {...ariaWrapperProps} /> ); diff --git a/upcoming_changelogs/6752.md b/upcoming_changelogs/6752.md new file mode 100644 index 00000000000..8e902048067 --- /dev/null +++ b/upcoming_changelogs/6752.md @@ -0,0 +1 @@ +- Updated all `EuiSkeleton` components with new props that allow for more control over screen reader live announcements: `announceLoadingStatus`, `announceLoadedStatus`, and `ariaLiveProps`