1
- import React , { Component } from 'react' ;
1
+ import React , { useEffect , useRef , useState } from 'react' ;
2
2
import PropTypes from 'prop-types' ;
3
3
import GraphSpinner from '../fragments/Loading/spinners/GraphSpinner.jsx' ;
4
4
import DefaultSpinner from '../fragments/Loading/spinners/DefaultSpinner.jsx' ;
@@ -7,66 +7,142 @@ import CircleSpinner from '../fragments/Loading/spinners/CircleSpinner.jsx';
7
7
import DotSpinner from '../fragments/Loading/spinners/DotSpinner.jsx' ;
8
8
import { mergeRight } from 'ramda' ;
9
9
10
- function getSpinner ( spinnerType ) {
11
- switch ( spinnerType ) {
12
- case 'graph' :
13
- return GraphSpinner ;
14
- case 'cube' :
15
- return CubeSpinner ;
16
- case 'circle' :
17
- return CircleSpinner ;
18
- case 'dot' :
19
- return DotSpinner ;
20
- default :
21
- return DefaultSpinner ;
22
- }
23
- }
24
-
25
- const hiddenContainer = { visibility : 'hidden' , position : 'relative' } ;
26
-
27
- const coveringSpinner = {
28
- visibility : 'visible' ,
29
- position : 'absolute' ,
30
- top : '0' ,
31
- height : '100%' ,
32
- width : '100%' ,
33
- display : 'flex' ,
34
- justifyContent : 'center' ,
35
- alignItems : 'center' ,
10
+ const spinnerComponentOptions = {
11
+ graph : GraphSpinner ,
12
+ cube : CubeSpinner ,
13
+ circle : CircleSpinner ,
14
+ dot : DotSpinner ,
36
15
} ;
37
16
17
+ const getSpinner = spinnerType =>
18
+ spinnerComponentOptions [ spinnerType ] || DefaultSpinner ;
19
+
38
20
/**
39
21
* A Loading component that wraps any other component and displays a spinner until the wrapped component has rendered.
40
22
*/
41
- export default class Loading extends Component {
42
- render ( ) {
43
- const {
44
- loading_state,
45
- color,
46
- className,
47
- style,
48
- parent_className,
49
- parent_style,
50
- fullscreen,
51
- debug,
52
- type : spinnerType ,
53
- } = this . props ;
54
-
55
- const isLoading = loading_state && loading_state . is_loading ;
56
- const Spinner = isLoading && getSpinner ( spinnerType ) ;
57
-
58
- return (
59
- < div
60
- className = { parent_className }
61
- style = {
62
- isLoading
63
- ? mergeRight ( hiddenContainer , parent_style )
64
- : parent_style
23
+ const Loading = ( {
24
+ children,
25
+ loading_state,
26
+ display,
27
+ color,
28
+ className,
29
+ style,
30
+ parent_className,
31
+ parent_style,
32
+ overlay_style,
33
+ fullscreen,
34
+ debug,
35
+ show_initially,
36
+ type : spinnerType ,
37
+ delay_hide,
38
+ delay_show,
39
+ target_components,
40
+ custom_spinner,
41
+ } ) => {
42
+ const coveringSpinner = {
43
+ visibility : 'visible' ,
44
+ position : 'absolute' ,
45
+ top : '0' ,
46
+ height : '100%' ,
47
+ width : '100%' ,
48
+ display : 'flex' ,
49
+ justifyContent : 'center' ,
50
+ alignItems : 'center' ,
51
+ } ;
52
+ const hiddenContainer = mergeRight (
53
+ { visibility : 'hidden' , position : 'relative' } ,
54
+ overlay_style
55
+ ) ;
56
+
57
+ /* Overrides default Loading behavior if target_components is set. By default,
58
+ * Loading fires when any recursive child enters loading state. This makes loading
59
+ * opt-in: Loading animation only enabled when one of target components enters loading state.
60
+ */
61
+ const isTarget = ( ) => {
62
+ if ( ! target_components ) {
63
+ return true ;
64
+ }
65
+ const isMatchingComponent = ( ) => {
66
+ return Object . entries ( target_components ) . some (
67
+ ( [ component_name , prop_names ] ) => {
68
+ // Convert prop_names to an array if it's not already
69
+ const prop_names_array = Array . isArray ( prop_names )
70
+ ? prop_names
71
+ : [ prop_names ] ;
72
+
73
+ return (
74
+ loading_state . component_name === component_name &&
75
+ ( prop_names_array . includes ( '*' ) ||
76
+ prop_names_array . some (
77
+ prop_name =>
78
+ loading_state . prop_name === prop_name
79
+ ) )
80
+ ) ;
81
+ }
82
+ ) ;
83
+ } ;
84
+ return isMatchingComponent ;
85
+ } ;
86
+
87
+ const [ showSpinner , setShowSpinner ] = useState ( show_initially ) ;
88
+ const dismissTimer = useRef ( ) ;
89
+ const showTimer = useRef ( ) ;
90
+
91
+ // delay_hide and delay_show is from dash-bootstrap-components dbc.Spinner
92
+ useEffect ( ( ) => {
93
+ if ( display === 'show' || display === 'hide' ) {
94
+ setShowSpinner ( display === 'show' ) ;
95
+ return ;
96
+ }
97
+
98
+ if ( loading_state ) {
99
+ if ( loading_state . is_loading ) {
100
+ // if component is currently loading and there's a dismiss timer active
101
+ // we need to clear it.
102
+ if ( dismissTimer . current ) {
103
+ dismissTimer . current = clearTimeout ( dismissTimer . current ) ;
104
+ }
105
+ // if component is currently loading but the spinner is not showing and
106
+ // there is no timer set to show, then set a timeout to show
107
+ if ( ! showSpinner && ! showTimer . current ) {
108
+ showTimer . current = setTimeout ( ( ) => {
109
+ setShowSpinner ( isTarget ( ) ) ;
110
+ showTimer . current = null ;
111
+ } , delay_show ) ;
65
112
}
66
- >
67
- { this . props . children }
68
- < div style = { isLoading ? coveringSpinner : { } } >
69
- { isLoading && (
113
+ } else {
114
+ // if component is not currently loading and there's a show timer
115
+ // active we need to clear it
116
+ if ( showTimer . current ) {
117
+ showTimer . current = clearTimeout ( showTimer . current ) ;
118
+ }
119
+ // if component is not currently loading and the spinner is showing and
120
+ // there's no timer set to dismiss it, then set a timeout to hide it
121
+ if ( showSpinner && ! dismissTimer . current ) {
122
+ dismissTimer . current = setTimeout ( ( ) => {
123
+ setShowSpinner ( false ) ;
124
+ dismissTimer . current = null ;
125
+ } , delay_hide ) ;
126
+ }
127
+ }
128
+ }
129
+ } , [ delay_hide , delay_show , loading_state , display ] ) ;
130
+
131
+ const Spinner = showSpinner && getSpinner ( spinnerType ) ;
132
+
133
+ return (
134
+ < div
135
+ className = { parent_className }
136
+ style = {
137
+ showSpinner
138
+ ? mergeRight ( hiddenContainer , parent_style )
139
+ : parent_style
140
+ }
141
+ >
142
+ { children }
143
+ < div style = { showSpinner ? coveringSpinner : { } } >
144
+ { showSpinner &&
145
+ ( custom_spinner || (
70
146
< Spinner
71
147
className = { className }
72
148
style = { style }
@@ -75,18 +151,21 @@ export default class Loading extends Component {
75
151
debug = { debug }
76
152
fullscreen = { fullscreen }
77
153
/>
78
- ) }
79
- </ div >
154
+ ) ) }
80
155
</ div >
81
- ) ;
82
- }
83
- }
156
+ </ div >
157
+ ) ;
158
+ } ;
84
159
85
160
Loading . _dashprivate_isLoadingComponent = true ;
86
161
87
162
Loading . defaultProps = {
88
163
type : 'default' ,
89
164
color : '#119DFF' ,
165
+ delay_show : 0 ,
166
+ delay_hide : 0 ,
167
+ show_initially : true ,
168
+ display : 'auto' ,
90
169
} ;
91
170
92
171
Loading . propTypes = {
@@ -106,24 +185,24 @@ Loading.propTypes = {
106
185
] ) ,
107
186
108
187
/**
109
- * Property that determines which spinner to show
188
+ * Property that determines which built-in spinner to show
110
189
* one of 'graph', 'cube', 'circle', 'dot', or 'default'.
111
190
*/
112
191
type : PropTypes . oneOf ( [ 'graph' , 'cube' , 'circle' , 'dot' , 'default' ] ) ,
113
192
114
193
/**
115
- * Boolean that makes the spinner display full-screen
194
+ * Boolean that makes the built-in spinner display full-screen
116
195
*/
117
196
fullscreen : PropTypes . bool ,
118
197
119
198
/**
120
- * If true, the spinner will display the component_name and prop_name
199
+ * If true, the built-in spinner will display the component_name and prop_name
121
200
* while loading
122
201
*/
123
202
debug : PropTypes . bool ,
124
203
125
204
/**
126
- * Additional CSS class for the spinner root DOM node
205
+ * Additional CSS class for the built-in spinner root DOM node
127
206
*/
128
207
className : PropTypes . string ,
129
208
@@ -133,17 +212,22 @@ Loading.propTypes = {
133
212
parent_className : PropTypes . string ,
134
213
135
214
/**
136
- * Additional CSS styling for the spinner root DOM node
215
+ * Additional CSS styling for the built-in spinner root DOM node
137
216
*/
138
217
style : PropTypes . object ,
139
218
140
219
/**
141
220
* Additional CSS styling for the outermost dcc.Loading parent div DOM node
142
221
*/
143
222
parent_style : PropTypes . object ,
223
+ /**
224
+ * Additional CSS styling for the spinner overlay. This is applied to the
225
+ * dcc.Loading children while the spinner is active. The default is `{'visibility': 'hidden'}`
226
+ */
227
+ overlay_style : PropTypes . object ,
144
228
145
229
/**
146
- * Primary colour used for the loading spinners
230
+ * Primary color used for the built-in loading spinners
147
231
*/
148
232
color : PropTypes . string ,
149
233
@@ -164,4 +248,46 @@ Loading.propTypes = {
164
248
*/
165
249
component_name : PropTypes . string ,
166
250
} ) ,
251
+
252
+ /**
253
+ * Setting display to "show" or "hide" will override the loading state coming from dash-renderer
254
+ */
255
+ display : PropTypes . oneOf ( [ 'auto' , 'show' , 'hide' ] ) ,
256
+
257
+ /**
258
+ * Add a time delay (in ms) to the spinner being removed to prevent flickering.
259
+ */
260
+ delay_hide : PropTypes . number ,
261
+
262
+ /**
263
+ * Add a time delay (in ms) to the spinner being shown after the loading_state
264
+ * is set to True.
265
+ */
266
+ delay_show : PropTypes . number ,
267
+
268
+ /**
269
+ * Whether the Spinner should show on app start-up before the loading state
270
+ * has been determined. Default True. Use when also setting `delay_show`.
271
+ */
272
+ show_initially : PropTypes . bool ,
273
+
274
+ /**
275
+ * Specify component and prop to trigger showing the loading spinner
276
+ * example: `{"output-container": "children", "grid": ["rowData", "columnDefs]}`
277
+ *
278
+ */
279
+ target_components : PropTypes . objectOf (
280
+ PropTypes . oneOfType ( [
281
+ PropTypes . string ,
282
+ PropTypes . arrayOf ( PropTypes . string ) ,
283
+ ] )
284
+ ) ,
285
+
286
+ /**
287
+ * Component to use rather than the built-in spinner specified in the `type` prop.
288
+ *
289
+ */
290
+ custom_spinner : PropTypes . node ,
167
291
} ;
292
+
293
+ export default Loading ;
0 commit comments