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

components: Rewrite Spinner for better performance #2384

Merged
merged 2 commits into from
Jan 23, 2023
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
67 changes: 24 additions & 43 deletions packages/components/src/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import React, { SVGAttributes } from 'react'
import { keyframes } from '@emotion/react'

import React from 'react'
import type { ThemeUICSSObject } from '@theme-ui/css'

import { Box, BoxOwnProps, BoxProps } from './Box'
import type { ForwardRef } from './types'
import { __internalProps } from './util'

const spin = keyframes({
from: {
transform: 'rotate(0deg)',
},
to: {
transform: 'rotate(360deg)',
},
})

export interface SpinnerProps
extends Omit<
React.ComponentPropsWithRef<'svg'>,
'opacity' | 'color' | 'css' | 'sx' | 'strokeWidth'
>,
BoxOwnProps {
size?: number | string
size?: number
strokeWidth?: number
title?: string
duration?: number
Expand All @@ -34,37 +23,17 @@ export const Spinner: ForwardRef<SVGSVGElement, SpinnerProps> =
size = 48,
strokeWidth = 4,
max = 1,
title = 'Loading...',
duration = 500,
title = 'Loading',
duration = 750,
...props
},
ref
) {
const r = 16 - strokeWidth
const C = 2 * r * Math.PI
const offset = C - (1 / 4) * C

const __css: ThemeUICSSObject = {
color: 'primary',
overflow: 'visible',
}

const circleProps: SVGAttributes<SVGCircleElement> = {
cx: 16,
cy: 16,
r,
strokeDasharray: C,
strokeDashoffset: offset,
}

const __circleCss: ThemeUICSSObject = {
transformOrigin: '50% 50%',
animationName: spin.toString(),
animationTimingFunction: 'linear',
animationDuration: duration + 'ms',
animationIterationCount: 'infinite',
}

const svgProps = {
strokeWidth,

Expand All @@ -77,6 +46,14 @@ export const Spinner: ForwardRef<SVGSVGElement, SpinnerProps> =
role: 'img',
}

const circleProps = {
strokeWidth,
r: 16 - strokeWidth,
cx: 16,
cy: 16,
fill: 'none',
}

return (
<Box
ref={ref}
Expand All @@ -86,14 +63,18 @@ export const Spinner: ForwardRef<SVGSVGElement, SpinnerProps> =
{...__internalProps({ __css })}
>
<title>{title}</title>
<circle cx={16} cy={16} r={r} opacity={1 / 8} />
<Box
as="circle"
{...(circleProps as {})}
{...__internalProps({
__css: __circleCss,
})}
/>
<circle {...circleProps} opacity={1 / 8} />
<circle {...circleProps} strokeDasharray="20 110">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 16 16"
to="360 16 16"
dur={`${duration}ms`}
repeatCount="indefinite"
/>
</circle>
</Box>
)
})
53 changes: 17 additions & 36 deletions packages/components/test/__snapshots__/index.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1329,22 +1329,6 @@ exports[`Slider renders 1`] = `
`;

exports[`Spinner renders 1`] = `
@keyframes animation-0 {
from {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}

to {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}

.emotion-0 {
box-sizing: border-box;
margin: 0;
Expand All @@ -1353,21 +1337,6 @@ exports[`Spinner renders 1`] = `
overflow: visible;
}

.emotion-1 {
box-sizing: border-box;
margin: 0;
min-width: 0;
transform-origin: 50% 50%;
-webkit-animation-name: animation-0;
animation-name: animation-0;
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-duration: 500ms;
animation-duration: 500ms;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}

<svg
className="emotion-0"
fill="none"
Expand All @@ -1379,22 +1348,34 @@ exports[`Spinner renders 1`] = `
width={48}
>
<title>
Loading...
Loading
</title>
<circle
cx={16}
cy={16}
fill="none"
opacity={0.125}
r={12}
strokeWidth={4}
/>
<circle
className="emotion-1"
cx={16}
cy={16}
fill="none"
r={12}
strokeDasharray={75.39822368615503}
strokeDashoffset={56.548667764616276}
/>
strokeDasharray="20 110"
strokeWidth={4}
>
<animateTransform
attributeName="transform"
attributeType="XML"
dur="750ms"
from="0 16 16"
repeatCount="indefinite"
to="360 16 16"
type="rotate"
/>
</circle>
</svg>
`;

Expand Down
9 changes: 5 additions & 4 deletions packages/docs/src/pages/components/spinner.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ import { Spinner } from 'theme-ui'
| Name | Type | Description |
| ------------- | ------ | ------------------------------------------------ |
| `title` | String | (default `'loading'`) Text for SVG `<title>` tag |
| `size` | Number | (default `48`) chart diameter |
| `size` | Number | (default `48`) indicator diameter |
| `strokeWidth` | Number | (default `4`) stroke width |
| `duration` | Number | (default `750`) duration of animation in ms |

A `title` attribute should be provided to the component for accessibility purposes.
The element uses `role='img'` by default.
Pass any overrides or additional attributes for the SVG element as props.
A `title` attribute should be provided to the component for accessibility
purposes. The element uses `role='img'` by default. Pass any overrides or
additional attributes for the SVG element as props.

## Variants

Expand Down