Skip to content
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
48 changes: 48 additions & 0 deletions src-docs/src/views/skeleton/skeleton_example.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';

import { GuideSectionTypes } from '../../components';
import {
Expand All @@ -7,6 +8,7 @@ import {
EuiSkeletonRectangle,
EuiSkeletonCircle,
EuiSkeletonLoading,
EuiScreenReaderLive,
EuiText,
EuiSpacer,
EuiCallOut,
Expand Down Expand Up @@ -70,6 +72,15 @@ const skeletonLoadingSnippet = `<EuiSkeletonLoading
}
/>`;

import SkeletonLiveProps from './skeleton_live_props';
const skeletonLivePropsSource = require('!!raw-loader!./skeleton_live_props');
const skeletonLivePropsSnippet = `<EuiSkeletonText
announceLoadingStatus={true}
announceLoadedStatus={false}
ariaLiveProps={ariaLiveProps}
contentAriaLabel="Example text"
/>`;

export const SkeletonExample = {
title: 'Skeleton',
intro: (
Expand Down Expand Up @@ -222,5 +233,42 @@ export const SkeletonExample = {
snippet: skeletonLoadingSnippet,
demo: <SkeletonLoading />,
},
{
title: 'Customizing screen reader announcements',
source: [
{
type: GuideSectionTypes.JS,
code: skeletonLivePropsSource,
},
],
text: (
<EuiText>
<p>
<strong>EuiSkeleton</strong> components assume that the page starts
as loading and transitions to loaded, and the default screen reader
experience is set up accordingly (
<EuiCode>announceLoadedStatus={'{true}'}</EuiCode>).
</p>
<p>
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 <EuiCode>announceLoadingStatus</EuiCode> to true,
or <EuiCode>announceLoadedStatus</EuiCode> to false. Submitting the
below example announces a loading status, but not a loaded status.
</p>
<p>
As an optional escape hatch, <EuiCode>ariaLiveProps</EuiCode> is
also available and accepts any{' '}
<Link to="/utilities/accessibility#screen-reader-live-region">
<strong>EuiScreenReaderLive</strong>
</Link>{' '}
props.
</p>
</EuiText>
),
props: { EuiSkeletonLoading, EuiScreenReaderLive },
snippet: skeletonLivePropsSnippet,
demo: <SkeletonLiveProps />,
},
],
};
60 changes: 60 additions & 0 deletions src-docs/src/views/skeleton/skeleton_live_props.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<EuiSkeletonLoading
isLoading={isLoading}
announceLoadedStatus={false} // Prevents unnecessary announcement on page load
announceLoadingStatus={true} // Announces on submit
ariaLiveProps={{ role: 'alert' }} // Allows customizing the aria-live region
contentAriaLabel="Demo form"
loadingContent={
<EuiSkeletonRectangle width="100%" height={euiTheme.size.xxl} />
}
loadedContent={
<form onSubmit={simulateLoading}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem>
<EuiFieldText
fullWidth
aria-label="Input that transitions to loading"
defaultValue="Replaced with a loading skeleton on submit"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton type="submit" fill>
Submit
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton>Cancel</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</form>
}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,74 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`EuiSkeletonLoading aria-live behavior announceLoadedStatus - allows turning off live announcements 1`] = `
<div
aria-busy="false"
data-test-subj="euiSkeletonLoadingAriaWrapper"
>
<span
data-test-subj="loaded"
/>
</div>
`;

exports[`EuiSkeletonLoading aria-live behavior announceLoadingStatus - allows enabling live announcements 1`] = `
<div
aria-busy="true"
data-test-subj="euiSkeletonLoadingAriaWrapper"
>
<div
class="emotion-euiScreenReaderOnly"
>
<div
aria-atomic="true"
aria-live="polite"
role="status"
>
Loading Sample user data
</div>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="off"
role="status"
/>
</div>
<span
aria-label="Loading Sample user data"
data-test-subj="loading"
role="progressbar"
/>
</div>
`;

exports[`EuiSkeletonLoading aria-live behavior ariaLiveProps 1`] = `
<div
aria-busy="false"
data-test-subj="euiSkeletonLoadingAriaWrapper"
>
<div
class="emotion-euiScreenReaderOnly"
>
<div
aria-atomic="true"
aria-live="polite"
role="alert"
>
Loaded Sample user data
</div>
<div
aria-atomic="true"
aria-hidden="true"
aria-live="off"
role="alert"
/>
</div>
<span
data-test-subj="loaded"
/>
</div>
`;

exports[`EuiSkeletonLoading renders \`loadedContent\` when \`isLoading\` is false 1`] = `
<div
aria-busy="false"
Expand Down
6 changes: 6 additions & 0 deletions src/components/skeleton/skeleton_circle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export const EuiSkeletonCircle: FunctionComponent<EuiSkeletonCircleProps> = ({
size = 'm',
className,
contentAriaLabel,
announceLoadingStatus,
announceLoadedStatus,
ariaLiveProps,
ariaWrapperProps,
children,
...rest
Expand All @@ -49,6 +52,9 @@ export const EuiSkeletonCircle: FunctionComponent<EuiSkeletonCircleProps> = ({
}
loadedContent={children || ''}
contentAriaLabel={contentAriaLabel}
announceLoadingStatus={announceLoadingStatus}
announceLoadedStatus={announceLoadedStatus}
ariaLiveProps={ariaLiveProps}
{...ariaWrapperProps}
/>
);
Expand Down
37 changes: 37 additions & 0 deletions src/components/skeleton/skeleton_loading.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<EuiSkeletonLoading {...contentProps} announceLoadingStatus={true} />
);

expect(getByText('Loading Sample user data')).toBeTruthy();
expect(container.firstChild).toMatchSnapshot();
});

test('announceLoadedStatus - allows turning off live announcements', () => {
const { container, queryByText } = render(
<EuiSkeletonLoading
{...contentProps}
isLoading={false}
announceLoadedStatus={false}
/>
);

expect(queryByText('Loaded Sample user data')).toBeFalsy();
expect(container.firstChild).toMatchSnapshot();
});

test('ariaLiveProps', () => {
const { container, getByRole } = render(
<EuiSkeletonLoading
{...contentProps}
isLoading={false}
ariaLiveProps={{ role: 'alert' }}
/>
);

expect(getByRole('alert')).toBeTruthy();
expect(container.firstChild).toMatchSnapshot();
});
});
});
56 changes: 45 additions & 11 deletions src/components/skeleton/skeleton_loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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<EuiScreenReaderLiveProps>;
/**
* Optional props to pass to the `aria-busy` wrapper around the skeleton content
*/
ariaWrapperProps?: HTMLAttributes<HTMLDivElement>;
};

export type EuiSkeletonLoadingProps = CommonProps &
_EuiSkeletonAriaProps['ariaWrapperProps'] &
Pick<_EuiSkeletonAriaProps, 'isLoading' | 'contentAriaLabel'> & {
Omit<_EuiSkeletonAriaProps, 'ariaWrapperProps'> & {
/**
* Content to display when loading
*/
Expand All @@ -45,24 +63,29 @@ export type EuiSkeletonLoadingProps = CommonProps &
export const EuiSkeletonLoading: FunctionComponent<EuiSkeletonLoadingProps> = ({
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 (
<div
Expand All @@ -71,10 +94,21 @@ export const EuiSkeletonLoading: FunctionComponent<EuiSkeletonLoadingProps> = ({
{...rest}
>
{isLoading ? (
React.cloneElement(loadingContent, loadingProps)
<>
{announceLoadingStatus && (
<EuiScreenReaderLive {...ariaLiveProps}>
{loadingAriaLabel}
</EuiScreenReaderLive>
)}
{loadingContent}
</>
) : (
<>
<EuiScreenReaderLive>{loadedAriaLive}</EuiScreenReaderLive>
{announceLoadedStatus && (
<EuiScreenReaderLive {...ariaLiveProps}>
{loadedAriaLive}
</EuiScreenReaderLive>
)}
{loadedContent}
</>
)}
Expand Down
6 changes: 6 additions & 0 deletions src/components/skeleton/skeleton_rectangle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export const EuiSkeletonRectangle: FunctionComponent<EuiSkeletonRectangleProps>
style,
className,
contentAriaLabel,
announceLoadingStatus,
announceLoadedStatus,
ariaLiveProps,
ariaWrapperProps,
children,
...rest
Expand All @@ -56,6 +59,9 @@ export const EuiSkeletonRectangle: FunctionComponent<EuiSkeletonRectangleProps>
}
loadedContent={children || ''}
contentAriaLabel={contentAriaLabel}
announceLoadingStatus={announceLoadingStatus}
announceLoadedStatus={announceLoadedStatus}
ariaLiveProps={ariaLiveProps}
{...ariaWrapperProps}
/>
);
Expand Down
Loading