Skip to content

Commit 78ebf0d

Browse files
authored
Merge pull request #2760 from AnnMarieW/update-dcc-loading
New features for dcc.Loading
2 parents b588e94 + 79a01d8 commit 78ebf0d

File tree

3 files changed

+524
-66
lines changed

3 files changed

+524
-66
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [UNRELEASED]
6+
7+
## Added
8+
- [2760](https://github.com/plotly/dash/pull/2760) New additions to dcc.Loading resolving multiple issues:
9+
- `delay_show` and `delay_hide` props to prevent flickering during brief loading periods (similar to Dash Bootstrap Components dbc.Spinner)
10+
- `overlay_style` for styling the loading overlay, such as setting visibility and opacity for children
11+
- `target_components` specifies components/props triggering the loading spinner
12+
- `custom_spinner` enables using a custom component for loading messages instead of built-in spinners
13+
- `display` overrides the loading status with options for "show," "hide," or "auto"
14+
515
## [2.16.1] - 2024-03-06
616

717
## Fixed
Lines changed: 192 additions & 66 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,142 @@ 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 spinnerComponentOptions = {
11+
graph: GraphSpinner,
12+
cube: CubeSpinner,
13+
circle: CircleSpinner,
14+
dot: DotSpinner,
3615
};
3716

17+
const getSpinner = spinnerType =>
18+
spinnerComponentOptions[spinnerType] || DefaultSpinner;
19+
3820
/**
3921
* A Loading component that wraps any other component and displays a spinner until the wrapped component has rendered.
4022
*/
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);
65112
}
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 || (
70146
<Spinner
71147
className={className}
72148
style={style}
@@ -75,18 +151,21 @@ export default class Loading extends Component {
75151
debug={debug}
76152
fullscreen={fullscreen}
77153
/>
78-
)}
79-
</div>
154+
))}
80155
</div>
81-
);
82-
}
83-
}
156+
</div>
157+
);
158+
};
84159

85160
Loading._dashprivate_isLoadingComponent = true;
86161

87162
Loading.defaultProps = {
88163
type: 'default',
89164
color: '#119DFF',
165+
delay_show: 0,
166+
delay_hide: 0,
167+
show_initially: true,
168+
display: 'auto',
90169
};
91170

92171
Loading.propTypes = {
@@ -106,24 +185,24 @@ Loading.propTypes = {
106185
]),
107186

108187
/**
109-
* Property that determines which spinner to show
188+
* Property that determines which built-in spinner to show
110189
* one of 'graph', 'cube', 'circle', 'dot', or 'default'.
111190
*/
112191
type: PropTypes.oneOf(['graph', 'cube', 'circle', 'dot', 'default']),
113192

114193
/**
115-
* Boolean that makes the spinner display full-screen
194+
* Boolean that makes the built-in spinner display full-screen
116195
*/
117196
fullscreen: PropTypes.bool,
118197

119198
/**
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
121200
* while loading
122201
*/
123202
debug: PropTypes.bool,
124203

125204
/**
126-
* Additional CSS class for the spinner root DOM node
205+
* Additional CSS class for the built-in spinner root DOM node
127206
*/
128207
className: PropTypes.string,
129208

@@ -133,17 +212,22 @@ Loading.propTypes = {
133212
parent_className: PropTypes.string,
134213

135214
/**
136-
* Additional CSS styling for the spinner root DOM node
215+
* Additional CSS styling for the built-in spinner root DOM node
137216
*/
138217
style: PropTypes.object,
139218

140219
/**
141220
* Additional CSS styling for the outermost dcc.Loading parent div DOM node
142221
*/
143222
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,
144228

145229
/**
146-
* Primary colour used for the loading spinners
230+
* Primary color used for the built-in loading spinners
147231
*/
148232
color: PropTypes.string,
149233

@@ -164,4 +248,46 @@ Loading.propTypes = {
164248
*/
165249
component_name: PropTypes.string,
166250
}),
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,
167291
};
292+
293+
export default Loading;

0 commit comments

Comments
 (0)