Skip to content

Commit 27c3498

Browse files
committed
New features for dcc.Loading
1 parent 91b6acf commit 27c3498

File tree

1 file changed

+163
-61
lines changed

1 file changed

+163
-61
lines changed
Lines changed: 163 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {Component} from 'react';
1+
import React, {useEffect, useRef, useState} from 'react';
22
import PropTypes from 'prop-types';
33
import GraphSpinner from '../fragments/Loading/spinners/GraphSpinner.jsx';
44
import DefaultSpinner from '../fragments/Loading/spinners/DefaultSpinner.jsx';
@@ -7,66 +7,121 @@ import CircleSpinner from '../fragments/Loading/spinners/CircleSpinner.jsx';
77
import DotSpinner from '../fragments/Loading/spinners/DotSpinner.jsx';
88
import {mergeRight} from 'ramda';
99

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,
3615
};
3716

17+
const getSpinner = spinnerType =>
18+
spinnerComponents[spinnerType] || DefaultSpinner;
19+
20+
const hiddenContainer = {position: 'relative'};
21+
3822
/**
3923
* A Loading component that wraps any other component and displays a spinner until the wrapped component has rendered.
4024
*/
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);
6583
}
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 || (
70125
<Spinner
71126
className={className}
72127
style={style}
@@ -75,18 +130,22 @@ export default class Loading extends Component {
75130
debug={debug}
76131
fullscreen={fullscreen}
77132
/>
78-
)}
79-
</div>
133+
))}
80134
</div>
81-
);
82-
}
83-
}
135+
</div>
136+
);
137+
};
84138

85139
Loading._dashprivate_isLoadingComponent = true;
86140

87141
Loading.defaultProps = {
88142
type: 'default',
89143
color: '#119DFF',
144+
delay_show: 0,
145+
delay_hide: 0,
146+
show_initially: true,
147+
opacity: 0.5,
148+
backgroundColor: 'white',
90149
};
91150

92151
Loading.propTypes = {
@@ -143,10 +202,21 @@ Loading.propTypes = {
143202
parent_style: PropTypes.object,
144203

145204
/**
146-
* Primary colour used for the loading spinners
205+
* Primary color used for the loading spinners
147206
*/
148207
color: PropTypes.string,
149208

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+
150220
/**
151221
* Object that holds the loading state object coming from dash-renderer
152222
*/
@@ -164,4 +234,36 @@ Loading.propTypes = {
164234
*/
165235
component_name: PropTypes.string,
166236
}),
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,
167267
};
268+
269+
export default Loading;

0 commit comments

Comments
 (0)