Skip to content

Commit

Permalink
Spinner, Text, TextUI: add label to Spinner, and id to TextUI and Text
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertCarreras authored Jan 31, 2025
1 parent 47c9f9d commit ebe7f09
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 61 deletions.
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
File renamed without changes.
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>
);
}
File renamed without changes.
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

0 comments on commit ebe7f09

Please sign in to comment.