@@ -58,7 +58,7 @@ type SpinnerProps = Readonly<
58
58
}
59
59
> ;
60
60
61
- const leavesIterable = Array . from ( { length : SPINNER_LEAF_COUNT } , ( _ , i ) => i ) ;
61
+ const leafIndices = Array . from ( { length : SPINNER_LEAF_COUNT } , ( _ , i ) => i ) ;
62
62
const animationSettings : CSSProperties = isChromatic ( )
63
63
? { }
64
64
: {
@@ -76,12 +76,44 @@ export const Spinner: FC<SpinnerProps> = ({
76
76
unmountedWhileLoading = false ,
77
77
...delegatedProps
78
78
} ) => {
79
+ // Disallow negative timeout values and fractional values, but also round
80
+ // the delay down if it's small enough that it would effectively be
81
+ // immediate from a user perspective
82
+ let safeDelay = Math . trunc ( spinnerStartDelayMs ) ;
83
+ if ( safeDelay < 100 ) {
84
+ safeDelay = 0 ;
85
+ }
86
+
87
+ // Doing a bunch of mid-render state syncs to minimize risks of
88
+ // contradictory states during re-renders. It's ugly, but it's what the
89
+ // React team officially recommends. Be very careful with this logic; React
90
+ // only bails out of redundant state updates if they happen outside of a
91
+ // render. Inside a render, if you keep calling a state dispatch, you will
92
+ // get an infinite render loop, no matter what the state value is.
93
+ const [ delayDone , setDelayDone ] = useState ( safeDelay === 0 ) ;
94
+ if ( delayDone && ! loading ) {
95
+ setDelayDone ( false ) ;
96
+ }
97
+ if ( ! delayDone && loading && safeDelay === 0 ) {
98
+ setDelayDone ( true ) ;
99
+ }
100
+ useEffect ( ( ) => {
101
+ if ( safeDelay === 0 ) {
102
+ return ;
103
+ }
104
+
105
+ const delayId = window . setTimeout ( ( ) => {
106
+ setDelayDone ( true ) ;
107
+ } , safeDelay ) ;
108
+ return ( ) => window . clearTimeout ( delayId ) ;
109
+ } , [ safeDelay ] ) ;
110
+
79
111
/**
80
- * @todo Figure out if this conditional logic causes a component to lose
81
- * state. I would hope not, since the children prop is the same in both
112
+ * @todo Figure out if this conditional logic can ever cause a component to
113
+ * lose state. I would hope not, since the children prop is the same in both
82
114
* cases, but I need to test this out
83
115
*/
84
- const showSpinner = useShowSpinner ( loading , spinnerStartDelayMs ) ;
116
+ const showSpinner = delayDone && loading ;
85
117
if ( ! showSpinner ) {
86
118
return children ;
87
119
}
@@ -98,19 +130,19 @@ export const Spinner: FC<SpinnerProps> = ({
98
130
className = { cn ( className , spinnerVariants ( { size } ) ) }
99
131
>
100
132
< title > Loading…</ title >
101
- { leavesIterable . map ( ( leafIndex ) => (
133
+ { leafIndices . map ( ( index ) => (
102
134
< rect
103
- key = { leafIndex }
135
+ key = { index }
104
136
x = "10.9"
105
137
y = "2"
106
138
width = "2"
107
139
height = "5.5"
108
140
rx = "1"
109
141
style = { {
110
142
...animationSettings ,
111
- transform : `rotate(${ leafIndex * ( 360 / SPINNER_LEAF_COUNT ) } deg)` ,
143
+ transform : `rotate(${ index * ( 360 / SPINNER_LEAF_COUNT ) } deg)` ,
112
144
transformOrigin : "center" ,
113
- animationDelay : `${ - leafIndex * 0.1 } s` ,
145
+ animationDelay : `${ - index * 0.1 } s` ,
114
146
} }
115
147
/>
116
148
) ) }
@@ -125,41 +157,3 @@ export const Spinner: FC<SpinnerProps> = ({
125
157
</ >
126
158
) ;
127
159
} ;
128
-
129
- // Not a big fan of one-time custom hooks, but it helps insulate the main
130
- // component from the chaos of handling all these state syncs, when the ultimate
131
- // result is a simple boolean. A lot of browsers will be able to inline the
132
- // function definition in some cases anyway
133
- function useShowSpinner (
134
- loading : boolean ,
135
- spinnerStartDelayMs : number ,
136
- ) : boolean {
137
- // Doing a bunch of mid-render state syncs to minimize risks of
138
- // contradictory states during re-renders. It's ugly, but it's what the
139
- // React team officially recommends. Be very careful with this logic; React
140
- // only bails out of redundant state updates if they happen outside of a
141
- // render. Inside a render, if you keep calling a state dispatch, you will
142
- // get an infinite render loop, no matter what the state value is.
143
- const [ spinnerActive , setSpinnerActive ] = useState ( spinnerStartDelayMs === 0 ) ;
144
- const unmountImmediatelyOnRerender = spinnerActive && ! loading ;
145
- if ( unmountImmediatelyOnRerender ) {
146
- setSpinnerActive ( false ) ;
147
- }
148
- const mountImmediatelyOnRerender =
149
- ! spinnerActive && loading && spinnerStartDelayMs === 0 ;
150
- if ( mountImmediatelyOnRerender ) {
151
- setSpinnerActive ( true ) ;
152
- }
153
- useEffect ( ( ) => {
154
- if ( spinnerStartDelayMs === 0 ) {
155
- return ;
156
- }
157
-
158
- const delayId = window . setTimeout ( ( ) => {
159
- setSpinnerActive ( true ) ;
160
- } , spinnerStartDelayMs ) ;
161
- return ( ) => window . clearTimeout ( delayId ) ;
162
- } , [ spinnerStartDelayMs ] ) ;
163
-
164
- return spinnerActive && loading ;
165
- }
0 commit comments