Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiSkeletonLoading] Improve loading accessibility and bake in isLoading API handling #6562

Merged
merged 9 commits into from
Jan 31, 2023
40 changes: 24 additions & 16 deletions src-docs/src/views/skeleton/skeleton_circle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,35 +22,43 @@ export default () => {
<EuiSpacer />
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiSkeletonCircle size="s" />
) : (
<EuiSkeletonCircle
size="s"
isLoading={isLoading}
contentAriaLabel="Demo skeleton avatar"
>
<EuiAvatar size="s" name="Raphael" />
)}
</EuiSkeletonCircle>
</EuiFlexItem>

<EuiFlexItem grow={false}>
{isLoading ? (
<EuiSkeletonCircle size="m" />
) : (
<EuiSkeletonCircle
size="m"
isLoading={isLoading}
contentAriaLabel="Demo skeleton avatar"
>
<EuiAvatar size="m" name="Donatello" />
)}
</EuiSkeletonCircle>
</EuiFlexItem>

<EuiFlexItem grow={false}>
{isLoading ? (
<EuiSkeletonCircle size="l" />
) : (
<EuiSkeletonCircle
size="l"
isLoading={isLoading}
contentAriaLabel="Demo skeleton avatar"
>
<EuiAvatar size="l" name="Leonardo" />
)}
</EuiSkeletonCircle>
</EuiFlexItem>

<EuiFlexItem grow={false}>
{isLoading ? (
<EuiSkeletonCircle size="xl" />
) : (
<EuiSkeletonCircle
size="xl"
isLoading={isLoading}
contentAriaLabel="Demo skeleton avatar"
>
<EuiAvatar size="xl" name="Michelangelo" />
)}
</EuiSkeletonCircle>
</EuiFlexItem>
</EuiFlexGroup>
</>
Expand Down
117 changes: 103 additions & 14 deletions src-docs/src/views/skeleton/skeleton_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import React from 'react';

import { GuideSectionTypes } from '../../components';
import {
EuiCode,
EuiText,
EuiSkeletonTitle,
EuiSkeletonText,
EuiSkeletonRectangle,
EuiSkeletonCircle,
EuiSkeletonLoading,
EuiText,
EuiSpacer,
EuiCallOut,
EuiCode,
} from '../../../../src/components';

import {
Expand All @@ -19,32 +22,85 @@ import {

import SkeletonCircle from './skeleton_circle';
const skeletonCircleSource = require('!!raw-loader!./skeleton_circle');
const skeletonCircleSnippet = `<EuiSkeletonCircle size="m" isLoading={isLoading} contentAriaLabel="Avatar">
<EuiAvatar size="s" name="Sally" />
</EuiSkeletonCircle>`;

import SkeletonText from './skeleton_text';
const skeletonTextSource = require('!!raw-loader!./skeleton_text');
const skeletonTextSnippet = `<EuiSkeletonText lines={3} size="m" isLoading={isLoading} contentAriaLabel="Example text">
<EuiText size="m"><p>Example text</p></EuiText>
</EuiSkeletonText>`;

import SkeletonTitle from './skeleton_title';
const skeletonTitleSource = require('!!raw-loader!./skeleton_title');
const skeletonTitleSnippet = `<EuiSkeletonTitle size="l" isLoading={isLoading} contentAriaLabel="Example title">
<EuiTitle><h1>Example title</h1></EuiTitle>
</EuiSkeletonTitle>`;

import SkeletonRectangle from './skeleton_rectangle';
const skeletonRectangleSource = require('!!raw-loader!./skeleton_rectangle');
const skeletonRectangleSnippet = `<EuiSkeletonRectangle
width="100%"
height={500}
borderRadius="m"
isLoading={isLoading}
contentAriaLabel="Example description"
>
<EuiPanel />
</EuiSkeletonRectangle>`;

const skeletonCircleSnippet = '<EuiSkeletonCircle size="m" />';
const skeletonTextSnippet = '<EuiSkeletonText lines={3} size="m" />';
const skeletonTitleSnippet = '<EuiSkeletonTitle size="l" />';
const skeletonRectangleSnippet =
'<EuiSkeletonRectangle width="200px" height="20px" borderRadius="m" />';
import SkeletonLoading from './skeleton_loading';
const skeletonLoadingSource = require('!!raw-loader!./skeleton_loading');
const skeletonLoadingSnippet = `<EuiSkeletonLoading
isLoading={isLoading}
contentAriaLabel="User data"
loadingContent={
<>
<EuiSkeletonTitle />
<EuiSkeletonText />
<EuiSkeletonCircle />
<EuiSkeletonRectangle />
</>
}
loadedContent={
<>
{/* Equivalent loaded content */}
</>
}
/>`;

export const SkeletonExample = {
title: 'Skeleton',
intro: (
<EuiText>
<p>
The <strong>EuiSkeleton</strong> components are placeholder components
for content which haven&apos;t yet loaded. They provide a meaningful
preview and avoid layout content shifts.
</p>
</EuiText>
<>
<EuiText>
<p>
The <strong>EuiSkeleton</strong> components are placeholder components
for content which has yet to load. They provide meaningful previews,
avoid layout content shifts, and add accessible announcements to
screen readers when content is done loading.
</p>
</EuiText>
<EuiSpacer />
<EuiCallOut
iconType="accessibility"
title={
<>
Using the <EuiCode>contentAriaLabel</EuiCode> prop
</>
}
>
<p>
The <EuiCode>contentAriaLabel</EuiCode> prop should be used to help
describe the type of content that is loading to screen reader users.
If you do not provide a descriptive label and have have multiple
loading skeletons on the page, screen reader users may hear a
multitude of &ldquo;Loaded&rdquo; messages in a row, with no
meaningful indication of what actually loaded.
</p>
</EuiCallOut>
</>
),
sections: [
{
Expand Down Expand Up @@ -133,5 +189,38 @@ export const SkeletonExample = {
demo: <SkeletonRectangle />,
playground: skeletonRectangleConfig,
},
{
title: 'Combining multiple skeletons',
source: [
{
type: GuideSectionTypes.JS,
code: skeletonLoadingSource,
},
],
text: (
<EuiText>
<p>
<strong>EuiSkeletonLoading</strong> is a light wrapper around the{' '}
<strong>EuiSkeleton</strong> components that handles loading
accessibility and flipping between skeleton and loaded content.
</p>
<p>
As you may have noticed in the previous demos, toggling multiple
skeletons to their loaded state all at once triggers multiple queued
screen reader announcements, which can be annoying to SR users.
</p>
<p>
To circumvent this, use <strong>EuiSkeletonLoading</strong> to
handle a single parent-level <EuiCode>isLoading</EuiCode> state.{' '}
<strong>EuiSkeleton</strong> children passed to the{' '}
<EuiCode>loadingContent</EuiCode> should not have their own{' '}
<EuiCode>isLoading</EuiCode> props or children.
</p>
</EuiText>
),
props: { EuiSkeletonLoading },
snippet: skeletonLoadingSnippet,
demo: <SkeletonLoading />,
},
],
};
93 changes: 93 additions & 0 deletions src-docs/src/views/skeleton/skeleton_loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { useState } from 'react';

import {
EuiSkeletonLoading,
EuiSkeletonTitle,
EuiTitle,
EuiSkeletonText,
EuiText,
EuiSkeletonRectangle,
EuiCard,
EuiSkeletonCircle,
EuiAvatar,
EuiSwitch,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiCode,
} from '../../../../src/components';

export default () => {
const [isLoading, setIsLoading] = useState(true);

return (
<>
<EuiSwitch
label="Toggle loaded state"
checked={isLoading}
onChange={() => setIsLoading(!isLoading)}
/>
<EuiSpacer />
<EuiSkeletonLoading
isLoading={isLoading}
contentAriaLabel="Demo loading section"
loadingContent={
<section>
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiSkeletonCircle size="s" />
</EuiFlexItem>
<EuiFlexItem>
<EuiSkeletonTitle size="l" />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiSkeletonText lines={5} />
</EuiFlexItem>
<EuiFlexItem>
<EuiSkeletonRectangle width="100%" height={148} />
</EuiFlexItem>
</EuiFlexGroup>
</section>
}
loadedContent={
<section>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiAvatar size="s" name="Avatar" />
<EuiTitle size="l">
<span>Example section title</span>
</EuiTitle>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiText>
<p>
This demo groups multiple skeleton types into a single
loading section by using{' '}
<EuiCode>EuiSkeletonLoading</EuiCode>.
</p>
<p>
This is a significant usability improvement for screen
readers as only one loaded message is announced, as opposed
to four.
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type="logoElastic" />}
title="Elastic Cloud"
description="Example card description."
/>
</EuiFlexItem>
</EuiFlexGroup>
</section>
}
/>
</>
);
};
58 changes: 34 additions & 24 deletions src-docs/src/views/skeleton/skeleton_rectangle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
EuiSpacer,
EuiImage,
EuiBadge,
EuiCard,
EuiIcon,
} from '../../../../src/components';

Expand All @@ -24,40 +25,49 @@ export default () => {
<EuiSpacer />
<EuiFlexGroup responsive={false} wrap>
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiSkeletonRectangle width="16px" height="16px" borderRadius="s" />
) : (
<EuiIcon type="cheer" />
)}
</EuiFlexItem>

<EuiFlexItem grow={false}>
{isLoading ? (
<EuiSkeletonRectangle
width="54.16px"
height="20px"
borderRadius="m"
/>
) : (
<EuiSkeletonRectangle
isLoading={isLoading}
contentAriaLabel="Demo skeleton badge"
width="54.16px"
height="20px"
borderRadius="s"
>
<EuiBadge color="success">Active</EuiBadge>
)}
</EuiSkeletonRectangle>
</EuiFlexItem>

<EuiFlexItem grow={false}>
{isLoading ? (
<EuiSkeletonRectangle
width={100}
height={100}
borderRadius="none"
/>
) : (
<EuiSkeletonRectangle
isLoading={isLoading}
contentAriaLabel="Demo skeleton image"
width={100}
height={100}
borderRadius="none"
>
<EuiImage
width={100}
height={100}
src="https://picsum.photos/300/300"
alt="A randomized image"
/>
)}
</EuiSkeletonRectangle>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<EuiSkeletonRectangle
isLoading={isLoading}
contentAriaLabel="Demo skeleton card"
width={203}
height={148}
borderRadius="m"
>
<EuiCard
icon={<EuiIcon size="xxl" type="logoCloud" />}
title="Elastic Cloud"
description="Example card description."
onClick={() => {}}
/>
</EuiSkeletonRectangle>
</EuiFlexItem>
</EuiFlexGroup>
</>
Expand Down
Loading