6
6
7
7
import isChromatic from "chromatic/isChromatic" ;
8
8
import { type VariantProps , cva } from "class-variance-authority" ;
9
- import type { FC , ReactNode } from "react" ;
9
+ import { type FC , useEffect , useState } from "react" ;
10
10
import { cn } from "utils/cn" ;
11
11
12
12
const SPINNER_LEAF_COUNT = 8 ;
@@ -26,35 +26,62 @@ const spinnerVariants = cva("", {
26
26
type SpinnerProps = Readonly <
27
27
React . SVGProps < SVGSVGElement > &
28
28
VariantProps < typeof spinnerVariants > & {
29
- children ?: ReactNode ;
30
- loading ?: boolean ;
29
+ loading : boolean ;
31
30
unmountedWhileLoading ?: boolean ;
32
31
spinnerStartDelayMs ?: number ;
33
32
}
34
33
> ;
35
34
36
35
const leavesIterable = Array . from ( { length : SPINNER_LEAF_COUNT } , ( _ , i ) => i ) ;
37
-
38
36
export const Spinner : FC < SpinnerProps > = ( {
39
37
className,
40
38
size,
41
39
loading,
42
40
children,
43
- ...props
41
+ spinnerStartDelayMs = 0 ,
42
+ unmountedWhileLoading = false ,
43
+ ...delegatedProps
44
44
} ) => {
45
- if ( ! loading ) {
45
+ // Doing some mid-render state syncs to minimize re-renders and risks of
46
+ // contradictory states. It's ugly, but it's what the React team recommends
47
+ const noDelay = spinnerStartDelayMs === 0 ;
48
+ const [ mountSpinner , setMountSpinner ] = useState ( noDelay ) ;
49
+ const unmountImmediatelyOnRerender = mountSpinner && ! loading ;
50
+ if ( unmountImmediatelyOnRerender ) {
51
+ setMountSpinner ( false ) ;
52
+ }
53
+ const mountImmediatelyOnRerender = ! mountSpinner && noDelay ;
54
+ if ( mountImmediatelyOnRerender ) {
55
+ setMountSpinner ( true ) ;
56
+ }
57
+ useEffect ( ( ) => {
58
+ if ( spinnerStartDelayMs === 0 ) {
59
+ return ;
60
+ }
61
+
62
+ const delayId = window . setTimeout ( ( ) => {
63
+ setMountSpinner ( true ) ;
64
+ } , spinnerStartDelayMs ) ;
65
+ return ( ) => window . clearTimeout ( delayId ) ;
66
+ } , [ spinnerStartDelayMs ] ) ;
67
+
68
+ // Past this point, only showSpinner should need to be referenced
69
+ const showSpinner = loading && mountSpinner ;
70
+ if ( ! showSpinner ) {
46
71
return children ;
47
72
}
48
73
49
74
return (
50
75
< svg
76
+ // Fill is the only prop that should be allowed to be overridden;
77
+ // all other props must come after destructuring
78
+ fill = "currentColor"
79
+ { ...delegatedProps }
51
80
viewBox = "0 0 24 24"
52
81
xmlns = "http://www.w3.org/2000/svg"
53
- fill = "currentColor"
54
82
className = { cn ( className , spinnerVariants ( { size } ) ) }
55
- { ...props }
56
83
>
57
- < title > Loading spinner </ title >
84
+ < title > Loading… </ title >
58
85
{ leavesIterable . map ( ( leafIndex ) => (
59
86
< rect
60
87
key = { leafIndex }
0 commit comments