Skip to content

Commit

Permalink
fix(Spin): significantly improve performance of the component in Safa…
Browse files Browse the repository at this point in the history
…ri (#111)

Co-authored-by: Sergey Garin <sergey.garin@cube.dev>
Co-authored-by: Andrey Yamanov <tenphi@gmail.com>
  • Loading branch information
3 people authored Jul 7, 2022
1 parent 9ee0e88 commit f45b927
Show file tree
Hide file tree
Showing 14 changed files with 346 additions and 124 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-pans-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cube-dev/ui-kit": patch
---

[CC-677](https://cubedevinc.atlassian.net/browse/CC-677) significantly improved performance of the `Spin` component in all browsers.
7 changes: 6 additions & 1 deletion .github/workflows/size-limit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,9 @@ jobs:
github,
repo: context.repo,
prNumber: context.payload.pull_request.number
})
})
- name: Throw error
if: steps.measure_size.outcome != 'success'
run: |
echo "Size limit has been exceeded"
exit 1
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = [
}),
);
},
limit: '220kB',
limit: '250kB',
},
{
name: 'Tree shaking (just a Button)',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import { LoadingAnimation } from './LoadingAnimation';
import {
LoadingAnimation,
CubeLoadingAnimationProps,
} from './LoadingAnimation';
import { baseProps } from '../../../stories/lists/baseProps';
import { Meta, Story } from '@storybook/react';

export default {
title: 'Status/LoadingAnimation',
component: LoadingAnimation,
parameters: {
controls: {
exclude: baseProps,
},
},
};
parameters: { controls: { exclude: baseProps } },
} as Meta<CubeLoadingAnimationProps>;

const Template = ({ size }) => <LoadingAnimation size={size} />;
const Template: Story<CubeLoadingAnimationProps> = (args) => (
<LoadingAnimation {...args} />
);

export const Default = Template.bind({});
Default.args = {};
export const Small = Template.bind({});
Small.args = {
size: 'small',
};
export const Large = Template.bind({});
Large.args = {
size: 'large',
};
2 changes: 2 additions & 0 deletions src/components/status/LoadingAnimation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { LoadingAnimation } from './LoadingAnimation';
export type { CubeLoadingAnimationProps } from './LoadingAnimation';
184 changes: 184 additions & 0 deletions src/components/status/Spin/Cube.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { memo } from 'react';
import styled from 'styled-components';
import { SpinCubeProps } from './types';

const fillByPosition = {
top: '#7a77ff',
right: '#727290',
bottom: '#ff6492',
} as const;

export const Cube = memo(styled.div.attrs<SpinCubeProps>(({ position }) => ({
role: 'presentation',
style: {
'--cube-spin-animation-name': `cube-spin-${position}`,
'--cube-spin-fill': fillByPosition[position],
},
}))`
--cube-spin-cube-border-width: calc(4 / 100 * var(--cube-spin-size));
--cube-spin-cube-border-compensation: calc(
-1 * (var(--cube-spin-cube-border-width))
);
--cube-spin-cube-size: calc(
(100% - 2 * var(--cube-spin-cube-border-width)) / 2
);
box-sizing: content-box;
position: absolute;
top: var(--cube-spin-cube-border-compensation);
left: var(--cube-spin-cube-border-compensation);
width: var(--cube-spin-cube-size);
height: var(--cube-spin-cube-size);
border: var(--cube-spin-cube-border-width) solid transparent;
overflow: hidden;
contain: size layout style paint;
pointer-events: none;
user-select: none;
animation-name: var(--cube-spin-animation-name);
animation-duration: 2.2s;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.5, 0.05, 0.3, 0.95);
@media (prefers-reduced-motion) {
animation-play-state: paused;
}
&::before {
--cube-spin-cube-round-radius: calc((4 / 100) * var(--cube-spin-size));
content: '';
display: block;
width: 100%;
height: 100%;
border-radius: var(--cube-spin-cube-round-radius);
background-color: var(--cube-spin-fill);
}
@keyframes cube-spin-top {
0% {
transform: translate(0%, 0);
}
8% {
transform: translate(100%, 0);
}
17% {
transform: translate(100%, 0);
}
25% {
transform: translate(100%, 0);
}
33% {
transform: translate(100%, 100%);
}
42% {
transform: translate(100%, 100%);
}
50% {
transform: translate(100%, 100%);
}
58% {
transform: translate(0, 100%);
}
67% {
transform: translate(0, 100%);
}
75% {
transform: translate(0, 100%);
}
83% {
transform: translate(0, 0);
}
92% {
transform: translate(0, 0);
}
100% {
transform: translate(0, 0);
}
}
@keyframes cube-spin-right {
0% {
transform: translate(100%, 100%);
}
8% {
transform: translate(100%, 100%);
}
17% {
transform: translate(100%, 100%);
}
25% {
transform: translate(0, 100%);
}
33% {
transform: translate(0, 100%);
}
42% {
transform: translate(0, 100%);
}
50% {
transform: translate(0, 0);
}
58% {
transform: translate(0, 0);
}
67% {
transform: translate(0, 0);
}
75% {
transform: translate(100%, 0);
}
83% {
transform: translate(100%, 0);
}
92% {
transform: translate(100%, 0);
}
100% {
transform: translate(100%, 100%);
}
}
@keyframes cube-spin-bottom {
0% {
transform: translate(0, 100%);
}
8% {
transform: translate(0, 100%);
}
17% {
transform: translate(0, 0);
}
25% {
transform: translate(0, 0);
}
33% {
transform: translate(0, 0);
}
42% {
transform: translate(100%, 0);
}
50% {
transform: translate(100%, 0);
}
58% {
transform: translate(100%, 0);
}
67% {
transform: translate(100%, 100%);
}
75% {
transform: translate(100%, 100%);
}
83% {
transform: translate(100%, 100%);
}
92% {
transform: translate(0, 100%);
}
100% {
transform: translate(0, 100%);
}
}
`);
30 changes: 30 additions & 0 deletions src/components/status/Spin/InternalSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { memo } from 'react';
import { tasty } from '../../../tasty';
import { Cube } from './Cube';
import { SpinsContainer } from './SpinsContainer';
import { InternalSpinnerProps, SpinSize } from './types';

const SpinsBox = tasty({ styles: { position: 'relative', blockSize: '100%' } });

export const InternalSpinner = memo(function InternalSpinner(
props: InternalSpinnerProps,
): JSX.Element {
const { size } = props;

return (
// Even though using size as a key resets the animation, it helps safari to resize the cubes.
<SpinsContainer key={size} ownSize={CUBE_SIZE_MAP[size]}>
<SpinsBox>
<Cube position="top" />
<Cube position="right" />
<Cube position="bottom" />
</SpinsBox>
</SpinsContainer>
);
});

const CUBE_SIZE_MAP: Record<SpinSize, number> = {
small: 24,
default: 32,
large: 48,
};
31 changes: 23 additions & 8 deletions src/components/status/Spin/Spin.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { Meta, Story } from '@storybook/react';
import { Paragraph } from '../../content/Paragraph';
import { Spin } from './Spin';
import { baseProps } from '../../../stories/lists/baseProps';
import { CubeSpinProps } from './types';

export default {
title: 'Status/Spin',
component: Spin,
parameters: {
controls: {
exclude: baseProps,
},
},
};
excludeStories: ['StressTest'],
} as Meta<CubeSpinProps>;

const Template = ({ size }) => <Spin size={size} />;
const Template: Story<CubeSpinProps> = (args) => <Spin {...args} />;

export const Default = Template.bind({});
Default.args = {};

export const Small = Template.bind({});
Small.args = { size: 'small' };

export const Large = Template.bind({});
Large.args = { size: 'large' };

export const WithChildren = Template.bind({});
WithChildren.args = { spinning: false, children: <Paragraph>Hello</Paragraph> };

export const StressTest: Story<CubeSpinProps> = (args) => (
<>
{Array.from({ length: 500 }).map((_, i) => (
<Spin key={i} {...args} styles={{ display: 'inline-flex' }} />
))}
</>
);
Loading

0 comments on commit f45b927

Please sign in to comment.