Skip to content

Commit f45b927

Browse files
fix(Spin): significantly improve performance of the component in Safari (#111)
Co-authored-by: Sergey Garin <[email protected]> Co-authored-by: Andrey Yamanov <[email protected]>
1 parent 9ee0e88 commit f45b927

File tree

14 files changed

+346
-124
lines changed

14 files changed

+346
-124
lines changed

.changeset/unlucky-pans-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cube-dev/ui-kit": patch
3+
---
4+
5+
[CC-677](https://cubedevinc.atlassian.net/browse/CC-677) significantly improved performance of the `Spin` component in all browsers.

.github/workflows/size-limit.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,9 @@ jobs:
9797
github,
9898
repo: context.repo,
9999
prNumber: context.payload.pull_request.number
100-
})
100+
})
101+
- name: Throw error
102+
if: steps.measure_size.outcome != 'success'
103+
run: |
104+
echo "Size limit has been exceeded"
105+
exit 1

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module.exports = [
1818
}),
1919
);
2020
},
21-
limit: '220kB',
21+
limit: '250kB',
2222
},
2323
{
2424
name: 'Tree shaking (just a Button)',
Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
import { LoadingAnimation } from './LoadingAnimation';
1+
import {
2+
LoadingAnimation,
3+
CubeLoadingAnimationProps,
4+
} from './LoadingAnimation';
25
import { baseProps } from '../../../stories/lists/baseProps';
6+
import { Meta, Story } from '@storybook/react';
37

48
export default {
59
title: 'Status/LoadingAnimation',
610
component: LoadingAnimation,
7-
parameters: {
8-
controls: {
9-
exclude: baseProps,
10-
},
11-
},
12-
};
11+
parameters: { controls: { exclude: baseProps } },
12+
} as Meta<CubeLoadingAnimationProps>;
1313

14-
const Template = ({ size }) => <LoadingAnimation size={size} />;
14+
const Template: Story<CubeLoadingAnimationProps> = (args) => (
15+
<LoadingAnimation {...args} />
16+
);
1517

1618
export const Default = Template.bind({});
1719
Default.args = {};
20+
export const Small = Template.bind({});
21+
Small.args = {
22+
size: 'small',
23+
};
24+
export const Large = Template.bind({});
25+
Large.args = {
26+
size: 'large',
27+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { LoadingAnimation } from './LoadingAnimation';
2+
export type { CubeLoadingAnimationProps } from './LoadingAnimation';
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { memo } from 'react';
2+
import styled from 'styled-components';
3+
import { SpinCubeProps } from './types';
4+
5+
const fillByPosition = {
6+
top: '#7a77ff',
7+
right: '#727290',
8+
bottom: '#ff6492',
9+
} as const;
10+
11+
export const Cube = memo(styled.div.attrs<SpinCubeProps>(({ position }) => ({
12+
role: 'presentation',
13+
style: {
14+
'--cube-spin-animation-name': `cube-spin-${position}`,
15+
'--cube-spin-fill': fillByPosition[position],
16+
},
17+
}))`
18+
--cube-spin-cube-border-width: calc(4 / 100 * var(--cube-spin-size));
19+
--cube-spin-cube-border-compensation: calc(
20+
-1 * (var(--cube-spin-cube-border-width))
21+
);
22+
--cube-spin-cube-size: calc(
23+
(100% - 2 * var(--cube-spin-cube-border-width)) / 2
24+
);
25+
26+
box-sizing: content-box;
27+
position: absolute;
28+
top: var(--cube-spin-cube-border-compensation);
29+
left: var(--cube-spin-cube-border-compensation);
30+
width: var(--cube-spin-cube-size);
31+
height: var(--cube-spin-cube-size);
32+
border: var(--cube-spin-cube-border-width) solid transparent;
33+
overflow: hidden;
34+
contain: size layout style paint;
35+
pointer-events: none;
36+
user-select: none;
37+
38+
animation-name: var(--cube-spin-animation-name);
39+
animation-duration: 2.2s;
40+
animation-iteration-count: infinite;
41+
animation-timing-function: cubic-bezier(0.5, 0.05, 0.3, 0.95);
42+
43+
@media (prefers-reduced-motion) {
44+
animation-play-state: paused;
45+
}
46+
47+
&::before {
48+
--cube-spin-cube-round-radius: calc((4 / 100) * var(--cube-spin-size));
49+
50+
content: '';
51+
display: block;
52+
width: 100%;
53+
height: 100%;
54+
border-radius: var(--cube-spin-cube-round-radius);
55+
56+
background-color: var(--cube-spin-fill);
57+
}
58+
59+
@keyframes cube-spin-top {
60+
0% {
61+
transform: translate(0%, 0);
62+
}
63+
8% {
64+
transform: translate(100%, 0);
65+
}
66+
17% {
67+
transform: translate(100%, 0);
68+
}
69+
25% {
70+
transform: translate(100%, 0);
71+
}
72+
33% {
73+
transform: translate(100%, 100%);
74+
}
75+
42% {
76+
transform: translate(100%, 100%);
77+
}
78+
50% {
79+
transform: translate(100%, 100%);
80+
}
81+
58% {
82+
transform: translate(0, 100%);
83+
}
84+
67% {
85+
transform: translate(0, 100%);
86+
}
87+
75% {
88+
transform: translate(0, 100%);
89+
}
90+
83% {
91+
transform: translate(0, 0);
92+
}
93+
92% {
94+
transform: translate(0, 0);
95+
}
96+
100% {
97+
transform: translate(0, 0);
98+
}
99+
}
100+
101+
@keyframes cube-spin-right {
102+
0% {
103+
transform: translate(100%, 100%);
104+
}
105+
8% {
106+
transform: translate(100%, 100%);
107+
}
108+
17% {
109+
transform: translate(100%, 100%);
110+
}
111+
25% {
112+
transform: translate(0, 100%);
113+
}
114+
33% {
115+
transform: translate(0, 100%);
116+
}
117+
42% {
118+
transform: translate(0, 100%);
119+
}
120+
50% {
121+
transform: translate(0, 0);
122+
}
123+
58% {
124+
transform: translate(0, 0);
125+
}
126+
67% {
127+
transform: translate(0, 0);
128+
}
129+
75% {
130+
transform: translate(100%, 0);
131+
}
132+
83% {
133+
transform: translate(100%, 0);
134+
}
135+
92% {
136+
transform: translate(100%, 0);
137+
}
138+
100% {
139+
transform: translate(100%, 100%);
140+
}
141+
}
142+
143+
@keyframes cube-spin-bottom {
144+
0% {
145+
transform: translate(0, 100%);
146+
}
147+
8% {
148+
transform: translate(0, 100%);
149+
}
150+
17% {
151+
transform: translate(0, 0);
152+
}
153+
25% {
154+
transform: translate(0, 0);
155+
}
156+
33% {
157+
transform: translate(0, 0);
158+
}
159+
42% {
160+
transform: translate(100%, 0);
161+
}
162+
50% {
163+
transform: translate(100%, 0);
164+
}
165+
58% {
166+
transform: translate(100%, 0);
167+
}
168+
67% {
169+
transform: translate(100%, 100%);
170+
}
171+
75% {
172+
transform: translate(100%, 100%);
173+
}
174+
83% {
175+
transform: translate(100%, 100%);
176+
}
177+
92% {
178+
transform: translate(0, 100%);
179+
}
180+
100% {
181+
transform: translate(0, 100%);
182+
}
183+
}
184+
`);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { memo } from 'react';
2+
import { tasty } from '../../../tasty';
3+
import { Cube } from './Cube';
4+
import { SpinsContainer } from './SpinsContainer';
5+
import { InternalSpinnerProps, SpinSize } from './types';
6+
7+
const SpinsBox = tasty({ styles: { position: 'relative', blockSize: '100%' } });
8+
9+
export const InternalSpinner = memo(function InternalSpinner(
10+
props: InternalSpinnerProps,
11+
): JSX.Element {
12+
const { size } = props;
13+
14+
return (
15+
// Even though using size as a key resets the animation, it helps safari to resize the cubes.
16+
<SpinsContainer key={size} ownSize={CUBE_SIZE_MAP[size]}>
17+
<SpinsBox>
18+
<Cube position="top" />
19+
<Cube position="right" />
20+
<Cube position="bottom" />
21+
</SpinsBox>
22+
</SpinsContainer>
23+
);
24+
});
25+
26+
const CUBE_SIZE_MAP: Record<SpinSize, number> = {
27+
small: 24,
28+
default: 32,
29+
large: 48,
30+
};
Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
1+
import { Meta, Story } from '@storybook/react';
2+
import { Paragraph } from '../../content/Paragraph';
13
import { Spin } from './Spin';
2-
import { baseProps } from '../../../stories/lists/baseProps';
4+
import { CubeSpinProps } from './types';
35

46
export default {
57
title: 'Status/Spin',
68
component: Spin,
7-
parameters: {
8-
controls: {
9-
exclude: baseProps,
10-
},
11-
},
12-
};
9+
excludeStories: ['StressTest'],
10+
} as Meta<CubeSpinProps>;
1311

14-
const Template = ({ size }) => <Spin size={size} />;
12+
const Template: Story<CubeSpinProps> = (args) => <Spin {...args} />;
1513

1614
export const Default = Template.bind({});
1715
Default.args = {};
16+
17+
export const Small = Template.bind({});
18+
Small.args = { size: 'small' };
19+
20+
export const Large = Template.bind({});
21+
Large.args = { size: 'large' };
22+
23+
export const WithChildren = Template.bind({});
24+
WithChildren.args = { spinning: false, children: <Paragraph>Hello</Paragraph> };
25+
26+
export const StressTest: Story<CubeSpinProps> = (args) => (
27+
<>
28+
{Array.from({ length: 500 }).map((_, i) => (
29+
<Spin key={i} {...args} styles={{ display: 'inline-flex' }} />
30+
))}
31+
</>
32+
);

0 commit comments

Comments
 (0)