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,121 @@ 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 spinnerComponents = {
11
+ graph : GraphSpinner ,
12
+ cube : CubeSpinner ,
13
+ circle : CircleSpinner ,
14
+ dot : DotSpinner ,
36
15
} ;
37
16
17
+ const getSpinner = spinnerType =>
18
+ spinnerComponents [ spinnerType ] || DefaultSpinner ;
19
+
20
+ const hiddenContainer = { position : 'relative' } ;
21
+
38
22
/**
39
23
* A Loading component that wraps any other component and displays a spinner until the wrapped component has rendered.
40
24
*/
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
25
+ const Loading = ( {
26
+ children,
27
+ loading_state,
28
+ color,
29
+ className,
30
+ style,
31
+ parent_className,
32
+ parent_style,
33
+ fullscreen,
34
+ debug,
35
+ show_initially,
36
+ type : spinnerType ,
37
+ delay_hide,
38
+ delay_show,
39
+ opacity,
40
+ backgroundColor,
41
+ target_components,
42
+ custom_spinner,
43
+ } ) => {
44
+ const coveringSpinner = {
45
+ visibility : 'visible' ,
46
+ position : 'absolute' ,
47
+ top : '0' ,
48
+ height : '100%' ,
49
+ width : '100%' ,
50
+ display : 'flex' ,
51
+ justifyContent : 'center' ,
52
+ alignItems : 'center' ,
53
+ backgroundColor : backgroundColor ,
54
+ opacity : opacity ,
55
+ } ;
56
+
57
+ const isTarget = ( ) => {
58
+ if ( ! target_components ) {
59
+ return true ;
60
+ }
61
+ const isMatchingComponent = target_components . some ( component => {
62
+ const [ component_name , prop_name ] = Object . entries ( component ) [ 0 ] ;
63
+ return (
64
+ loading_state . component_name === component_name &&
65
+ loading_state . prop_name === prop_name
66
+ ) ;
67
+ } ) ;
68
+ return isMatchingComponent ;
69
+ } ;
70
+
71
+ const [ showSpinner , setShowSpinner ] = useState ( show_initially ) ;
72
+ const dismissTimer = useRef ( ) ;
73
+ const showTimer = useRef ( ) ;
74
+
75
+ // delay_hide and delay_show is from dash-bootstrap-components dbc.Spinner
76
+ useEffect ( ( ) => {
77
+ if ( loading_state ) {
78
+ if ( loading_state . is_loading ) {
79
+ // if component is currently loading and there's a dismiss timer active
80
+ // we need to clear it.
81
+ if ( dismissTimer . current ) {
82
+ dismissTimer . current = clearTimeout ( dismissTimer . current ) ;
65
83
}
66
- >
67
- { this . props . children }
68
- < div style = { isLoading ? coveringSpinner : { } } >
69
- { isLoading && (
84
+ // if component is currently loading but the spinner is not showing and
85
+ // there is no timer set to show, then set a timeout to show
86
+ if ( ! showSpinner && ! showTimer . current ) {
87
+ showTimer . current = setTimeout ( ( ) => {
88
+ setShowSpinner ( isTarget ( ) ) ;
89
+ showTimer . current = null ;
90
+ } , delay_show ) ;
91
+ }
92
+ } else {
93
+ // if component is not currently loading and there's a show timer
94
+ // active we need to clear it
95
+ if ( showTimer . current ) {
96
+ showTimer . current = clearTimeout ( showTimer . current ) ;
97
+ }
98
+ // if component is not currently loading and the spinner is showing and
99
+ // there's no timer set to dismiss it, then set a timeout to hide it
100
+ if ( showSpinner && ! dismissTimer . current ) {
101
+ dismissTimer . current = setTimeout ( ( ) => {
102
+ setShowSpinner ( false ) ;
103
+ dismissTimer . current = null ;
104
+ } , delay_hide ) ;
105
+ }
106
+ }
107
+ }
108
+ } , [ delay_hide , delay_show , loading_state ] ) ;
109
+
110
+ const Spinner = showSpinner && getSpinner ( spinnerType ) ;
111
+
112
+ return (
113
+ < div
114
+ className = { parent_className }
115
+ style = {
116
+ showSpinner
117
+ ? mergeRight ( hiddenContainer , parent_style )
118
+ : parent_style
119
+ }
120
+ >
121
+ { children }
122
+ < div style = { showSpinner ? coveringSpinner : { } } >
123
+ { showSpinner &&
124
+ ( custom_spinner || (
70
125
< Spinner
71
126
className = { className }
72
127
style = { style }
@@ -75,18 +130,22 @@ export default class Loading extends Component {
75
130
debug = { debug }
76
131
fullscreen = { fullscreen }
77
132
/>
78
- ) }
79
- </ div >
133
+ ) ) }
80
134
</ div >
81
- ) ;
82
- }
83
- }
135
+ </ div >
136
+ ) ;
137
+ } ;
84
138
85
139
Loading . _dashprivate_isLoadingComponent = true ;
86
140
87
141
Loading . defaultProps = {
88
142
type : 'default' ,
89
143
color : '#119DFF' ,
144
+ delay_show : 0 ,
145
+ delay_hide : 0 ,
146
+ show_initially : true ,
147
+ opacity : 0.5 ,
148
+ backgroundColor : 'white' ,
90
149
} ;
91
150
92
151
Loading . propTypes = {
@@ -143,10 +202,21 @@ Loading.propTypes = {
143
202
parent_style : PropTypes . object ,
144
203
145
204
/**
146
- * Primary colour used for the loading spinners
205
+ * Primary color used for the loading spinners
147
206
*/
148
207
color : PropTypes . string ,
149
208
209
+ /**
210
+ * Opacity of loading Spinner div. Can take a value from 0.0 - 1.0. The lower
211
+ * the value, the more transparent:
212
+ */
213
+ opacity : PropTypes . number ,
214
+
215
+ /**
216
+ * Background color of the loading Spinner div
217
+ */
218
+ backgroundColor : PropTypes . string ,
219
+
150
220
/**
151
221
* Object that holds the loading state object coming from dash-renderer
152
222
*/
@@ -164,4 +234,36 @@ Loading.propTypes = {
164
234
*/
165
235
component_name : PropTypes . string ,
166
236
} ) ,
237
+
238
+ /**
239
+ * Add a time delay (in ms) to the spinner being removed to prevent flickering.
240
+ */
241
+ delay_hide : PropTypes . number ,
242
+
243
+ /**
244
+ * Add a time delay (in ms) to the spinner being shown after the loading_state
245
+ * is set to True.
246
+ */
247
+ delay_show : PropTypes . number ,
248
+
249
+ /**
250
+ * Whether the Spinner should show on app start-up before the loading state
251
+ * has been determined. Default True.
252
+ */
253
+ show_initially : PropTypes . bool ,
254
+
255
+ /**
256
+ * Specify component and prop to trigger showing the loading spinner
257
+ * example: `[{"output-container": "children"}]`
258
+ *
259
+ */
260
+ target_components : PropTypes . arrayOf ( PropTypes . object ) ,
261
+
262
+ /**
263
+ * Component to use rather than the spinner specified in the `type` prop.
264
+ *
265
+ */
266
+ custom_spinner : PropTypes . node ,
167
267
} ;
268
+
269
+ export default Loading ;
0 commit comments