6
6
7
7
import isChromatic from "chromatic/isChromatic" ;
8
8
import { type VariantProps , cva } from "class-variance-authority" ;
9
- import { type FC , useEffect , useState } from "react" ;
9
+ import { type CSSProperties , type FC , useEffect , useState } from "react" ;
10
10
import { cn } from "utils/cn" ;
11
11
12
12
const SPINNER_LEAF_COUNT = 8 ;
@@ -27,12 +27,44 @@ type SpinnerProps = Readonly<
27
27
React . SVGProps < SVGSVGElement > &
28
28
VariantProps < typeof spinnerVariants > & {
29
29
loading : boolean ;
30
+
31
+ /**
32
+ * Indicates whether the children prop should be unmounted during
33
+ * a loading state. Defaults to false - unmounting HTML elements
34
+ * like form controls can lead to invalid HTML, so this prop should
35
+ * be used with care and only if it prevents render performance
36
+ * issues.
37
+ */
30
38
unmountedWhileLoading ?: boolean ;
39
+
40
+ /**
41
+ * Specifies whether there should be a delay before the spinner
42
+ * appears on screen. If not specified, the spinner always appears
43
+ * immediately.
44
+ *
45
+ * Can help avoid page flickering issues. (e.g., You have a modal
46
+ * that takes a moment to close, and it has Spinner content inside
47
+ * it. The user triggers a loading transition, and you want to show
48
+ * the spinner at some point if a transition takes long enough, but
49
+ * if the spinner mounting and modal closing happen in too quick of
50
+ * a succession, the UI looks janky. So even though you might flip
51
+ * the loading state immediately, you want to wait a second to show
52
+ * it in case the modal can close quickly enough. It's lying to the
53
+ * user in a way that makes the UI feel more polished.)
54
+ */
31
55
spinnerStartDelayMs ?: number ;
32
56
}
33
57
> ;
34
58
35
59
const leavesIterable = Array . from ( { length : SPINNER_LEAF_COUNT } , ( _ , i ) => i ) ;
60
+ const animationSettings : CSSProperties = isChromatic ( )
61
+ ? { }
62
+ : {
63
+ transitionDuration : `${ 0.1 * SPINNER_LEAF_COUNT } s` ,
64
+ transitionTimingFunction : "ease-in-out" ,
65
+ animationIterationCount : "infinite" ,
66
+ } ;
67
+
36
68
export const Spinner : FC < SpinnerProps > = ( {
37
69
className,
38
70
size,
@@ -42,6 +74,11 @@ export const Spinner: FC<SpinnerProps> = ({
42
74
unmountedWhileLoading = false ,
43
75
...delegatedProps
44
76
} ) => {
77
+ /**
78
+ * @todo Figure out if this conditional logic causes a component to lose
79
+ * state. I would hope not, since the children prop is the same in both
80
+ * cases, but I need to test this out
81
+ */
45
82
const showSpinner = useShowSpinner ( loading , spinnerStartDelayMs ) ;
46
83
if ( ! showSpinner ) {
47
84
return children ;
@@ -67,26 +104,28 @@ export const Spinner: FC<SpinnerProps> = ({
67
104
width = "2"
68
105
height = "5.5"
69
106
rx = "1"
70
- className = {
71
- // 0.8 is hard-coded because of Tailwind; the value
72
- // should always be (0.1 * SPINNER_LEAF_COUNT)
73
- isChromatic ( ) ? "" : "animate-[loading_0.8s_ease-in-out_infinite]"
74
- }
75
107
style = { {
108
+ ...animationSettings ,
76
109
transform : `rotate(${ leafIndex * ( 360 / SPINNER_LEAF_COUNT ) } deg)` ,
77
110
transformOrigin : "center" ,
78
111
animationDelay : `${ - leafIndex * 0.1 } s` ,
79
112
} }
80
113
/>
81
114
) ) }
82
115
</ svg >
83
- { ! unmountedWhileLoading && < div className = "sr-only" > { children } </ div > }
116
+
117
+ { ! unmountedWhileLoading && (
118
+ < div className = "sr-only" >
119
+ This content is loading:
120
+ { children }
121
+ </ div >
122
+ ) }
84
123
</ >
85
124
) ;
86
125
} ;
87
126
88
127
// Not a big fan of one-time custom hooks, but it helps insulate the main
89
- // component from the chaos of handling all these states , when the ultimate
128
+ // component from the chaos of handling all these state syncs , when the ultimate
90
129
// result is a simple boolean. V8 will be able to inline the function definition
91
130
// in some cases anyway
92
131
function useShowSpinner (
0 commit comments