Skip to content

Commit

Permalink
Promote spinner to standalone component (#1476)
Browse files Browse the repository at this point in the history
* Simplify markup and use colours from Source palette

* Reduce circle radius to avoid clipping

* Update spinner overrides in button component

* Update svg attributes to match other icons

* Add theming support to spinner

* Use `theme` prop in preference to style overrides

* Replace negative stroke dash array value with multiple

Using a negative value causes the animation to
jump in Safari rather than animating continuously.

* Promote spinner to standalone component

* Add docs to story

* Allow custom sizes

* Add examples of size options and theming

* Convert attributes to JSX

* Simplify theme naming and remove fill colours

* Add changeset

* Update text for change log

* Update import order
  • Loading branch information
jamesmockett committed Jun 12, 2024
1 parent b23249e commit a275431
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 72 deletions.
13 changes: 13 additions & 0 deletions .changeset/cyan-adults-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@guardian/source': major
---

Removes `SvgSpinner` from icon library and replaces with dedicated `Spinner` component. The `size` prop supports the existing set of named icon sizes for backwards compatibility, but also allows setting a custom size in pixels. The default colour scheme can be overridden with the `theme` prop.

```tsx
<>
<Spinner size="small" />
<Spinner size={40} />
<Spinner theme={{ background: 'transparent', color: 'currentColor' }} />
</>
```
3 changes: 1 addition & 2 deletions libs/@guardian/source/src/react-components/@types/Icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export interface IconProps {
* The sanctioned colours have have been set out by the design system team.
* The colours which can be changed are:
*
* `fill`
*
* `fill`
*/
theme?: Partial<ThemeIcon>;
isAnnouncedByScreenReader?: boolean;
Expand Down
16 changes: 12 additions & 4 deletions libs/@guardian/source/src/react-components/button/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { css } from '@emotion/react';
import type { ReactElement, ReactNode } from 'react';
import { cloneElement } from 'react';
import { visuallyHidden } from '../../foundations';
import { SvgSpinner } from '../icons/SvgSpinner';
import { Spinner } from '../spinner/Spinner';

export const buttonContents = ({
hideLabel,
Expand All @@ -22,9 +22,17 @@ export const buttonContents = ({
contents.push(<div key="space" className="src-button-space" />);
}
contents.push(
cloneElement(<SvgSpinner />, {
key: 'svg',
}),
cloneElement(
<Spinner
theme={{
background: 'transparent',
color: 'currentColor',
}}
/>,
{
key: 'svg',
},
),
);
} else if (iconSvg) {
if (!hideLabel) {
Expand Down
8 changes: 0 additions & 8 deletions libs/@guardian/source/src/react-components/button/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,6 @@ const loadingSpinnerSizes: Record<Size, number> = {

const applyButtonStylesToLoadingSpinner = (size: Size) => {
return css`
path,
circle {
transition: stroke ${transitions.medium};
stroke: transparent;
}
path {
stroke: currentColor;
}
svg {
/*
* The loading spinner width has been specified as 24px in the design
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ import { SvgUpload } from '../__generated__/icons/SvgUpload';
import { SvgVideo } from '../__generated__/icons/SvgVideo';
import { SvgWhatsApp } from '../__generated__/icons/SvgWhatsApp';
import { SvgWhatsAppBrand } from '../__generated__/icons/SvgWhatsAppBrand';
import { SvgSpinner } from './SvgSpinner';
import type { ThemeIcon } from './theme';

const uiIcons = {
Expand Down Expand Up @@ -238,7 +237,6 @@ const uiIcons = {
SvgTextSize,
SvgTextSmall,
SvgUpload,
SvgSpinner,
SvgSignalBrand,
SvgTelegramBrand,
SvgBin,
Expand Down
54 changes: 0 additions & 54 deletions libs/@guardian/source/src/react-components/icons/SvgSpinner.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion libs/@guardian/source/src/react-components/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ it('Should have exactly these exports', () => {
'Radio',
'RadioGroup',
'Select',
'Spinner',
'Stack',
'SvgAlert',
'SvgAlertPhone',
Expand Down Expand Up @@ -208,7 +209,6 @@ it('Should have exactly these exports', () => {
'SvgSpeechBubble',
'SvgSpeechBubbleCross',
'SvgSpeechBubblePlus',
'SvgSpinner',
'SvgStar',
'SvgStarOutline',
'SvgTelegramBrand',
Expand Down
4 changes: 3 additions & 1 deletion libs/@guardian/source/src/react-components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ export { SvgPlus } from './__generated__/icons/SvgPlus';
export { SvgQuote } from './__generated__/icons/SvgQuote';
export { SvgSettings } from './__generated__/icons/SvgSettings';
export { SvgSpeechBubble } from './__generated__/icons/SvgSpeechBubble';
export { SvgSpinner } from './icons/SvgSpinner';
export { SvgStar } from './__generated__/icons/SvgStar';
export { SvgTickRound } from './__generated__/icons/SvgTickRound';
export { SvgTwitter } from './__generated__/icons/SvgTwitter';
Expand Down Expand Up @@ -202,6 +201,9 @@ export { SvgUpload } from './__generated__/icons/SvgUpload';
export type { IconProps, IconSize } from './@types/Icons';
export type { ThemeIcon } from './icons/theme';

export { Spinner } from './spinner/Spinner';
export type { ThemeSpinner } from './spinner/theme';

export type { InputSize } from './@types/InputSize';

export { themeLabel, themeLabelBrand } from './label/theme';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { Meta, StoryFn } from '@storybook/react';
import { palette } from '../../foundations';
import type { SpinnerProps } from './Spinner';
import { Spinner } from './Spinner';

const meta: Meta<typeof Spinner> = {
title: 'React Components/Spinner',
component: Spinner,
argTypes: {
size: {
options: ['xsmall', 'small', 'medium'],
control: { type: 'select' },
},
},
};

export default meta;

const Template: StoryFn<typeof Spinner> = (args: SpinnerProps) => (
<Spinner {...args} />
);

// *****************************************************************************

export const XSmallSizeDefaultTheme: StoryFn<typeof Spinner> = Template.bind(
{},
);
XSmallSizeDefaultTheme.args = {
size: 'xsmall',
};

// *****************************************************************************

export const SmallSizeDefaultTheme: StoryFn<typeof Spinner> = Template.bind({});
SmallSizeDefaultTheme.args = {
size: 'small',
};

// *****************************************************************************

export const MediumSizeDefaultTheme: StoryFn<typeof Spinner> = Template.bind(
{},
);
MediumSizeDefaultTheme.args = {
size: 'medium',
};

// *****************************************************************************

export const CustomSizeDefaultTheme: StoryFn<typeof Spinner> = Template.bind(
{},
);
CustomSizeDefaultTheme.args = {
size: 40,
};

// *****************************************************************************

export const CustomTheme: StoryFn<typeof Spinner> = Template.bind({});
CustomTheme.args = {
theme: {
background: 'transparent',
color: palette.neutral[7],
},
};
86 changes: 86 additions & 0 deletions libs/@guardian/source/src/react-components/spinner/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { iconSize } from '../../foundations';
import type { IconSize } from '../@types/Icons';
import type { ThemeSpinner } from './theme';
import { themeSpinner } from './theme';

export interface SpinnerProps {
/**
* Size of the spinner
*/
size?: IconSize | number;
/**
* Partial or complete theme to override the spinner's default colour palette.
* The colours which can be changed are:
*
* `background`<br>
* `color`<br>
*/
theme?: Partial<ThemeSpinner>;
}

/**
* [Storybook](https://guardian.github.io/storybooks/?path=/story/source_react-components-spinner--docs) •
* [GitHub](https://github.com/guardian/csnx/tree/main/libs/@guardian/source/src/react-components/spinner/Spinner.tsx) •
* [NPM](https://www.npmjs.com/package/@guardian/source)
*
* A spinner conveys to the user that a process is ongoing. ie. a page is
* loading or an action is being processed. The spinner is purely visual and
* does not include any accessibility features. It is the responsibility of the
* consumer to ensure that the spinner is used in a way that is accessible by
* adding an appropriate label (either visually or via `aria-label`) and
* applying `aria-live` to the containing element if the user needs to be
* informed of changes to the spinner's state.
*/
export const Spinner = ({ size = 'medium', theme }: SpinnerProps) => {
const mergedTheme = { ...themeSpinner, ...theme };
const spinnerWidth = typeof size === 'number' ? size : iconSize[size];

return (
<svg
width={spinnerWidth}
viewBox="0 0 30 30"
focusable={false}
aria-hidden={true}
>
<g>
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 15 15"
to="360 15 15"
dur="2.5s"
repeatCount="indefinite"
/>
<circle
cx={15}
cy={15}
r={12.5}
strokeWidth={5}
stroke={mergedTheme.background}
fill="transparent"
/>
<circle
cx={15}
cy={15}
r={12.5}
strokeWidth={5}
strokeDasharray={82}
strokeDashoffset={82}
stroke={mergedTheme.color}
fill="transparent"
>
<animate
attributeName="stroke-dashoffset"
dur="3.5s"
from={
164
} /* Multiple of `stroke-dasharray` so animation is continuous */
to={0}
repeatCount="indefinite"
/>
</circle>
</g>
</svg>
);
};
11 changes: 11 additions & 0 deletions libs/@guardian/source/src/react-components/spinner/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { palette } from '../../foundations';

export type ThemeSpinner = {
background: string;
color: string;
};

export const themeSpinner: ThemeSpinner = {
background: palette.brand[800],
color: palette.brand[400],
};

0 comments on commit a275431

Please sign in to comment.