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

Spinner, Text, TextUI: add label to Spinner, and id to TextUI and Text #3947 #3971

Merged
merged 2 commits into from
Jan 31, 2025
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
6 changes: 2 additions & 4 deletions docs/examples/spinner/dontLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Flex, Spinner, Text } from 'gestalt';
import { Flex, Spinner } from 'gestalt';

export default function Example() {
return (
<Flex alignItems="center" height="100%" justifyContent="center" width="100%">
<Flex direction="column" gap={2}>
<Spinner accessibilityLabel="Example spinner" show />

<Text weight="bold">Loading…</Text>
<Spinner label="Loading…" show />
</Flex>
</Flex>
);
Expand Down
16 changes: 16 additions & 0 deletions docs/examples/spinner/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Box, Flex, Spinner, useReducedMotion } from 'gestalt';

export default function Example() {
const reduced = useReducedMotion();
return (
<Box height="100%" width="100%">
<Flex alignItems="center" height="100%" justifyContent="center" width="100%">
<Spinner
accessibilityLabel="test"
label="We’re adding new ideas to your homefeed"
show={!reduced}
/>
</Flex>
</Box>
);
}
74 changes: 41 additions & 33 deletions docs/pages/web/spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useAppContext } from 'docs/docs-components/appContext';
import AccessibilitySection from '../../docs-components/AccessibilitySection';
import docGen, { DocGen } from '../../docs-components/docgen';
import GeneratedPropTable from '../../docs-components/GeneratedPropTable';
Expand All @@ -15,15 +14,13 @@ import dontMultiple from '../../examples/spinner/dontMultiple';
import dontWait from '../../examples/spinner/dontWait';
import doOverlay from '../../examples/spinner/doOverlay';
import doWait from '../../examples/spinner/doWait';
import grayscale from '../../examples/spinner/grayscale';
import label from '../../examples/spinner/label';
import localizationLabels from '../../examples/spinner/localizationLabels';
import main from '../../examples/spinner/main';
import variantGrayscale from '../../examples/spinner/variantGrayscale';
import variantWhite from '../../examples/spinner/variantWhite';
import white from '../../examples/spinner/white';

export default function DocsPage({ generatedDocGen }: { generatedDocGen: DocGen }) {
const { experiments } = useAppContext();
const isVREnabled = experiments === 'Tokens';

return (
<Page title={generatedDocGen?.displayName}>
<PageHeader description={generatedDocGen?.description} name={generatedDocGen?.displayName}>
Expand Down Expand Up @@ -147,18 +144,54 @@ export default function DocsPage({ generatedDocGen }: { generatedDocGen: DocGen

<AccessibilitySection
description={`
Be sure to include \`accessibilityLabel\`. Labels should relate to the specific part of the product where Spinner is being used (e.g. "Loading homefeed" when used on the homefeed surface). Don't forget to localize the label!
\`accessibilityLabel\` should relate to the specific part of the product where Spinner is being used (e.g. "Loading homefeed" when used on the homefeed surface).

Note that \`accessibilityLabel\` is optional as DefaultLabelProvider provides default strings. Use custom labels if they need to be more specific.

If \`label\` is provided, \`accessibilityLabel\` is not needed. \`accessibilityLabel\` overrides \`label\` for assistive technologies. Therefore, avoid overriding \`label\` if \`accessibilityLabel\` is less specific.

The override order for labels: \`accessibilityLabel\` overrides \`label\`, \`label\` overrides default label from DefaultLabelProvider. DefaultLabelProvider is only accessible if there is not \`accessibilityLabel\` not \`label\`.
`}
name={generatedDocGen?.displayName}
/>

<LocalizationSection
code={localizationLabels}
name={generatedDocGen?.displayName}
notes={`Note that \`accessibilityLabel\` is optional as DefaultLabelProvider provides default strings. Use custom labels if they need to be more specific.`}
notes={`Note that \`accessibilityLabel\` is optional as DefaultLabelProvider provides default strings. Use custom labels if they need to be more specific. DefaultLabelProvider is override by \`label\`.`}
/>

<MainSection name="Variants">
<MainSection.Subsection
columns={2}
description={`
By default, Spinner has color-change animation. Non-default color variant are \`grayscale\` and \`white\`.
`}
title="Colors"
>
<MainSection.Card
cardSize="lg"
sandpackExample={
<SandpackExample code={grayscale} layout="column" name="Delay variant" />
}
title="Grayscale"
/>
<MainSection.Card
cardSize="lg"
sandpackExample={<SandpackExample code={white} layout="column" name="Delay variant" />}
title="White"
/>
</MainSection.Subsection>

<MainSection.Subsection
description="Spinner supports a label. See the [Accessibility guidelines](#Accessibility) for more information"
title="Label"
>
<MainSection.Card
sandpackExample={<SandpackExample code={label} name="Label variant" />}
/>
</MainSection.Subsection>

<MainSection.Subsection
description={`
By default, Spinner uses a 300ms delay to improve perceived performance. This can be disabled if needed.
Expand All @@ -169,31 +202,6 @@ export default function DocsPage({ generatedDocGen }: { generatedDocGen: DocGen
sandpackExample={<SandpackExample code={delay} name="Delay variant" />}
/>
</MainSection.Subsection>

{isVREnabled && (
<MainSection.Subsection
columns={2}
description={`
By default, Spinner has color-change animation. Non-default color variant are \`grayscale\` and \`white\`.
`}
title="Colors"
>
<MainSection.Card
cardSize="lg"
sandpackExample={
<SandpackExample code={variantGrayscale} layout="column" name="Delay variant" />
}
title="Grayscale"
/>
<MainSection.Card
cardSize="lg"
sandpackExample={
<SandpackExample code={variantWhite} layout="column" name="Delay variant" />
}
title="White"
/>
</MainSection.Subsection>
)}
</MainSection>

<QualityChecklist component={generatedDocGen?.displayName} />
Expand Down
12 changes: 3 additions & 9 deletions packages/gestalt/src/Icon/InternalIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type IconColor =

type IconName = keyof typeof icons | keyof typeof compactIconsVR;
type Props = {
accessibilityDescribedby?: string;
accessibilityLabel: string;
color?: IconColor;
dataTestId?: string;
Expand All @@ -38,16 +39,8 @@ type Props = {
// @ts-expect-error - TS2322 - Type 'string[]' is not assignable to type 'readonly ("replace" | "search" | "link" | "text" | "dash" | "3D" | "3D-move" | "360" | "accessibility" | "ad" | "ad-group" | "add" | "add-circle" | "add-layout" | "add-pin" | "add-section" | ... 317 more ... | "wave")[]'.
const IconNames: ReadonlyArray<IconName> = Object.keys(icons);

/**
* [Icons](https://gestalt.pinterest.systems/web/icon) are the symbolic representation of an action or information, providing visual context and improving usability.
*
* See the [Iconography and SVG guidelines](https://gestalt.pinterest.systems/foundations/iconography/library) to explore the full icon library.
*
* ![Icon light mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/Icon-list.spec.ts-snapshots/Icon-list-chromium-darwin.png)
* ![Icon dark mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/Icon-list-dark.spec.ts-snapshots/Icon-list-dark-chromium-darwin.png)
*
*/
function InternalIcon({
accessibilityDescribedby,
accessibilityLabel,
color = 'subtle',
dangerouslySetSvgPath,
Expand Down Expand Up @@ -133,6 +126,7 @@ function InternalIcon({
return (
// @ts-expect-error - TS2322 - Type '{ children: Element; "aria-hidden": true | null; "aria-label": string; className: string; height: string | number; role: "img"; viewBox: string; width: string | number; }' is not assignable to type 'SVGProps<SVGSVGElement>'.
<svg
aria-describedby={accessibilityDescribedby}
aria-hidden={ariaHidden}
aria-label={accessibilityLabel}
className={cs}
Expand Down
5 changes: 5 additions & 0 deletions packages/gestalt/src/Spinner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ test('Spinner renders when passed show', () => {
expect(tree).toMatchSnapshot();
});

test('Spinner renders label', () => {
const tree = create(<Spinner {...baseProps} label="Label" show />).toJSON();
expect(tree).toMatchSnapshot();
});

test('Spinner renders with no delay', () => {
const tree = create(<Spinner {...baseProps} delay={false} show />).toJSON();
expect(tree).toMatchSnapshot();
Expand Down
48 changes: 39 additions & 9 deletions packages/gestalt/src/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useId } from 'react';
import classnames from 'classnames';
import Box from './Box';
import { useDefaultLabelContext } from './contexts/DefaultLabelProvider';
import Icon from './Icon';
import Flex from './Flex';
import InternalIcon from './Icon/InternalIcon';
import styles from './Spinner.css';
import VRSpinner from './Spinner/VRSpinner';
import TextUI from './TextUI';
import useInExperiment from './useInExperiment';

const SIZE_NAME_TO_PIXEL = {
Expand All @@ -25,6 +28,10 @@ type Props = {
* Whether or not to render with a 300ms delay. The delay is for perceived performance, so you should rarely need to remove it. See the [delay variant](https://gestalt.pinterest.systems/web/spinner#Delay) for more details.
*/
delay?: boolean;
/**
* Adds a label under the spinning animation.
*/
label?: string;
/**
* Indicates if Spinner should be visible. Controlling the component with this prop ensures the outro animation is played. If outro animation is not intended, prefer conditional rendering.
*/
Expand All @@ -45,10 +52,12 @@ export default function Spinner({
accessibilityLabel,
color = 'subtle',
delay = true,
label,
show,
size = 'md',
}: Props) {
const { accessibilityLabel: accessibilityLabelDefault } = useDefaultLabelContext('Spinner');
const id = useId();

const isInVRExperiment = useInExperiment({
webExperimentName: 'web_gestalt_visualrefresh',
Expand All @@ -59,20 +68,43 @@ export default function Spinner({
return (
<VRSpinner
accessibilityLabel={accessibilityLabel}
// 'subtle' maps to 'default' as it is not a VR color variant
color={color === 'subtle' ? 'default' : color}
color={color === 'subtle' ? 'default' : color} // 'subtle' maps to 'default' as it is not a VR color variant
delay={delay}
label={label}
show={show}
// 'md' maps to 'lg' as it doesn't exist in VR Spinner
size={size === 'md' ? 'lg' : size}
size={size === 'md' ? 'lg' : size} // 'md' maps to 'lg' as it doesn't exist in VR Spinner
/>
);
}

return show ? (
if (!show) return null;

return label ? (
<Box padding={1}>
<Flex direction="column" gap={6}>
<Box display="flex" justifyContent="around" overflow="hidden">
<div className={classnames(styles.icon, { [styles.delay]: delay })}>
<InternalIcon
accessibilityDescribedby={id}
accessibilityLabel={accessibilityLabel ?? label ?? accessibilityLabelDefault}
// map non-classic colors to subtle
color={color === 'default' || color === 'subtle' ? color : 'subtle'}
icon="knoop"
size={SIZE_NAME_TO_PIXEL[size]}
/>
</div>
</Box>
<Box minWidth={200}>
<TextUI align="center" id={id} size="sm">
{label}
</TextUI>
</Box>
</Flex>
</Box>
) : (
<Box display="flex" justifyContent="around" overflow="hidden">
<div className={classnames(styles.icon, { [styles.delay]: delay })}>
<Icon
<InternalIcon
accessibilityLabel={accessibilityLabel ?? accessibilityLabelDefault}
// map non-classic colors to subtle
color={color === 'default' || color === 'subtle' ? color : 'subtle'}
Expand All @@ -81,8 +113,6 @@ export default function Spinner({
/>
</div>
</Box>
) : (
<div />
);
}

Expand Down
41 changes: 36 additions & 5 deletions packages/gestalt/src/Spinner/VRSpinner.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useEffect, useState } from 'react';
import { useEffect, useId, useState } from 'react';
import classnames from 'classnames';
import vrLightDesignTokens from 'gestalt-design-tokens/dist/json/vr-theme/variables-light.json';
import styles from './VRSpinner.css';
import Box from '../Box';
import { useDefaultLabelContext } from '../contexts/DefaultLabelProvider';
import Flex from '../Flex';
import TextUI from '../TextUI';

const SIZE_NAME_TO_PIXEL = {
sm: 32,
lg: 56,
} as const;

type SpinnerBodyProps = {
accessibilityDescribedby?: string;
accessibilityLabel: string;
delay: boolean;
show: boolean;
Expand All @@ -20,6 +23,7 @@ type SpinnerBodyProps = {
};

function SpinnerBody({
accessibilityDescribedby,
accessibilityLabel,
delay,
show,
Expand All @@ -41,7 +45,6 @@ function SpinnerBody({
return (
<Box display="flex" justifyContent="around">
<div
aria-label={accessibilityLabel}
className={classnames(styles.spinner, {
[styles.exit]: !show,
})}
Expand All @@ -54,7 +57,13 @@ function SpinnerBody({
} as React.CSSProperties
}
>
<svg fill="none" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg">
<svg
aria-describedby={accessibilityDescribedby}
aria-label={accessibilityLabel}
fill="none"
viewBox="0 0 56 56"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="28"
cy="12"
Expand All @@ -81,6 +90,7 @@ function SpinnerBody({

type Props = {
accessibilityLabel?: string;
label?: string;
delay?: boolean;
show: boolean;
size?: 'sm' | 'lg';
Expand All @@ -90,12 +100,14 @@ type Props = {
export default function Spinner({
accessibilityLabel,
delay = true,
label,
show: showProp,
size = 'lg',
color = 'default',
}: Props) {
const [show, setShow] = useState(showProp);
const { accessibilityLabel: accessibilityLabelDefault } = useDefaultLabelContext('Spinner');
const id = useId();

const unmountSpinner = () => {
if (!showProp) setShow(false);
Expand All @@ -107,9 +119,28 @@ export default function Spinner({

if (!show) return null;

return (
return label ? (
<Box padding={1}>
<Flex direction="column" gap={6}>
<SpinnerBody
accessibilityDescribedby={id}
accessibilityLabel={accessibilityLabel ?? label ?? accessibilityLabelDefault}
color={color}
delay={delay}
onExitAnimationEnd={unmountSpinner}
show={showProp}
size={size}
/>
<Box minWidth={200}>
<TextUI align="center" id={id} size="sm">
{label}
</TextUI>
</Box>
</Flex>
</Box>
) : (
<SpinnerBody
accessibilityLabel={accessibilityLabel || accessibilityLabelDefault}
accessibilityLabel={accessibilityLabel ?? accessibilityLabelDefault}
color={color}
delay={delay}
onExitAnimationEnd={unmountSpinner}
Expand Down
Loading
Loading