Skip to content

Commit d48536d

Browse files
authored
switch spinner animations to svg (tldraw#6459)
I noticed some weird issues with the SVG animation on our spinner when working on tldraw#6412. The animation wouldn't play immediately, and it looked like the browser was hanging. Switching to CSS animations fixes this, and is better anyway because it doesn't force the browser to repaint the SVG each frame. I made a couple other related changes while working on this: 1. `@keyframes` declarations in library CSS files are now namespaced like the class names, so we don't clash with application defined keyframes 2. the UI `<Spinner />` component now re-uses the editor one ### Change type - [x] `improvement` ### Test plan ### Release notes ### api changes - The `<Spinner />` component now accepts SVG props like `className`
1 parent 1bd95e3 commit d48536d

7 files changed

Lines changed: 42 additions & 51 deletions

File tree

packages/editor/api-report.api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ export const DefaultShapeIndicators: NamedExoticComponent<TLShapeIndicatorsProps
648648
export function DefaultSnapIndicator({ className, line, zoom }: TLSnapIndicatorProps): JSX_2.Element;
649649

650650
// @public (undocumented)
651-
export function DefaultSpinner(): JSX_2.Element;
651+
export function DefaultSpinner(props: React.SVGProps<SVGSVGElement>): JSX_2.Element;
652652

653653
// @public (undocumented)
654654
export const DefaultSvgDefs: () => null;
@@ -3359,7 +3359,7 @@ export interface TLEditorComponents {
33593359
// (undocumented)
33603360
SnapIndicator?: ComponentType<TLSnapIndicatorProps> | null;
33613361
// (undocumented)
3362-
Spinner?: ComponentType | null;
3362+
Spinner?: ComponentType<React.SVGProps<SVGSVGElement>> | null;
33633363
// (undocumented)
33643364
SvgDefs?: ComponentType | null;
33653365
// (undocumented)

packages/editor/editor.css

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -968,14 +968,14 @@ input,
968968
font-size: 14px;
969969
font-weight: 500;
970970
opacity: 0;
971-
animation: fade-in 0.2s ease-in-out forwards;
971+
animation: tl-fade-in 0.2s ease-in-out forwards;
972972
animation-delay: 0.2s;
973973
position: absolute;
974974
inset: 0px;
975975
z-index: var(--layer-canvas-blocker);
976976
}
977977

978-
@keyframes fade-in {
978+
@keyframes tl-fade-in {
979979
0% {
980980
opacity: 0;
981981
}
@@ -984,6 +984,19 @@ input,
984984
}
985985
}
986986

987+
.tl-spinner {
988+
animation: tl-spin 1s linear infinite;
989+
}
990+
991+
@keyframes tl-spin {
992+
0% {
993+
transform: rotate(0deg);
994+
}
995+
100% {
996+
transform: rotate(360deg);
997+
}
998+
}
999+
9871000
/* ---------------------- Brush --------------------- */
9881001

9891002
.tl-brush {

packages/editor/src/lib/components/default-components/DefaultSpinner.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1+
import classNames from 'classnames'
2+
13
/** @public @react */
2-
export function DefaultSpinner() {
4+
export function DefaultSpinner(props: React.SVGProps<SVGSVGElement>) {
35
return (
4-
<svg width={16} height={16} viewBox="0 0 16 16" aria-hidden="false">
6+
<svg
7+
width={16}
8+
height={16}
9+
viewBox="0 0 16 16"
10+
aria-hidden="false"
11+
{...props}
12+
className={classNames('tl-spinner', props.className)}
13+
>
514
<g strokeWidth={2} fill="none" fillRule="evenodd">
615
<circle strokeOpacity={0.25} cx={8} cy={8} r={7} stroke="currentColor" />
7-
<path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor">
8-
<animateTransform
9-
attributeName="transform"
10-
type="rotate"
11-
from="0 8 8"
12-
to="360 8 8"
13-
dur="1s"
14-
repeatCount="indefinite"
15-
/>
16-
</path>
16+
<path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor" />
1717
</g>
1818
</svg>
1919
)

packages/editor/src/lib/hooks/useEditorComponents.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export interface TLEditorComponents {
6969
ShapeIndicator?: ComponentType<TLShapeIndicatorProps> | null
7070
ShapeIndicators?: ComponentType | null
7171
SnapIndicator?: ComponentType<TLSnapIndicatorProps> | null
72-
Spinner?: ComponentType | null
72+
Spinner?: ComponentType<React.SVGProps<SVGSVGElement>> | null
7373
SvgDefs?: ComponentType | null
7474
ZoomBrush?: ComponentType<TLBrushProps> | null
7575

packages/editor/src/lib/license/Watermark.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
143143
}
144144
145145
.${className}:hover > button {
146-
animation: delayed_link 0.2s forwards ease-in-out;
146+
animation: ${className}_delayed_link 0.2s forwards ease-in-out;
147147
animation-delay: 0.32s;
148148
}
149149
@@ -153,7 +153,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
153153
}
154154
155155
156-
@keyframes delayed_link {
156+
@keyframes ${className}_delayed_link {
157157
0% {
158158
cursor: inherit;
159159
opacity: .38;

packages/tldraw/src/lib/ui.css

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@
964964
justify-content: center;
965965
border-radius: 99px;
966966
opacity: 0;
967-
animation: fade-in;
967+
animation: tl-fade-in;
968968
animation-duration: 0.12s;
969969
animation-delay: 2s;
970970
animation-fill-mode: forwards;
@@ -1365,11 +1365,11 @@
13651365

13661366
@media (prefers-reduced-motion: no-preference) {
13671367
.tlui-toast__container[data-state='open'] {
1368-
animation: slide-in 200ms cubic-bezier(0.785, 0.135, 0.15, 0.86);
1368+
animation: tlui-slide-in 200ms cubic-bezier(0.785, 0.135, 0.15, 0.86);
13691369
}
13701370

13711371
.tlui-toast__container[data-state='closed'] {
1372-
animation: hide 100ms ease-in;
1372+
animation: tlui-fade-out 100ms ease-in;
13731373
}
13741374

13751375
.tlui-toast__container[data-swipe='move'] {
@@ -1382,7 +1382,7 @@
13821382
}
13831383

13841384
.tlui-toast__container[data-swipe='end'] {
1385-
animation: swipe-out 100ms ease-out;
1385+
animation: tlui-slide-out 100ms ease-out;
13861386
}
13871387
}
13881388

@@ -1397,7 +1397,7 @@
13971397
z-index: var(--layer-canvas-overlays);
13981398
background-color: var(--color-overlay);
13991399
pointer-events: all;
1400-
animation: fadeIn 0.12s ease-out;
1400+
animation: tl-fade-in 0.12s ease-out;
14011401
display: grid;
14021402
place-items: center;
14031403
overflow-y: auto;
@@ -1964,7 +1964,7 @@
19641964
}
19651965

19661966
/* ------------------- Animations ------------------- */
1967-
@keyframes hide {
1967+
@keyframes tlui-fade-out {
19681968
0% {
19691969
opacity: 1;
19701970
}
@@ -1973,7 +1973,7 @@
19731973
}
19741974
}
19751975

1976-
@keyframes slide-in {
1976+
@keyframes tlui-slide-in {
19771977
from {
19781978
transform: translateX(calc(100% + var(--space-3)));
19791979
}
@@ -1982,7 +1982,7 @@
19821982
}
19831983
}
19841984

1985-
@keyframes swipe-out {
1985+
@keyframes tlui-slide-out {
19861986
from {
19871987
transform: translateX(var(--radix-toast-swipe-end-x));
19881988
}
Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,10 @@
1+
import { DefaultSpinner } from '@tldraw/editor'
12
import React from 'react'
23
import { useTranslation } from '../hooks/useTranslation/useTranslation'
34

45
/** @internal */
56
export function Spinner(props: React.SVGProps<SVGSVGElement>) {
67
const msg = useTranslation()
78

8-
return (
9-
<svg
10-
width={16}
11-
height={16}
12-
viewBox="0 0 16 16"
13-
{...props}
14-
aria-label={msg('app.loading')}
15-
aria-hidden="false"
16-
>
17-
<g strokeWidth={2} fill="none" fillRule="evenodd">
18-
<circle strokeOpacity={0.25} cx={8} cy={8} r={7} stroke="currentColor" />
19-
<path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor">
20-
<animateTransform
21-
attributeName="transform"
22-
type="rotate"
23-
from="0 8 8"
24-
to="360 8 8"
25-
dur="1s"
26-
repeatCount="indefinite"
27-
/>
28-
</path>
29-
</g>
30-
</svg>
31-
)
9+
return <DefaultSpinner aria-label={msg('app.loading')} {...props} />
3210
}

0 commit comments

Comments
 (0)