-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New features for dcc.Loading #2760
Changes from 5 commits
27c3498
b5b9710
d87be76
872e8a7
840b322
2f0ab2d
2c0961a
e710b47
0f3a65c
4edbe55
d1c0a7f
eda25eb
d62a037
5aaef7d
79a01d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import React, {Component} from 'react'; | ||
import React, {useEffect, useRef, useState} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import GraphSpinner from '../fragments/Loading/spinners/GraphSpinner.jsx'; | ||
import DefaultSpinner from '../fragments/Loading/spinners/DefaultSpinner.jsx'; | ||
|
@@ -7,66 +7,130 @@ import CircleSpinner from '../fragments/Loading/spinners/CircleSpinner.jsx'; | |
import DotSpinner from '../fragments/Loading/spinners/DotSpinner.jsx'; | ||
import {mergeRight} from 'ramda'; | ||
|
||
function getSpinner(spinnerType) { | ||
switch (spinnerType) { | ||
case 'graph': | ||
return GraphSpinner; | ||
case 'cube': | ||
return CubeSpinner; | ||
case 'circle': | ||
return CircleSpinner; | ||
case 'dot': | ||
return DotSpinner; | ||
default: | ||
return DefaultSpinner; | ||
} | ||
} | ||
|
||
const hiddenContainer = {visibility: 'hidden', position: 'relative'}; | ||
|
||
const coveringSpinner = { | ||
visibility: 'visible', | ||
position: 'absolute', | ||
top: '0', | ||
height: '100%', | ||
width: '100%', | ||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
const spinnerComponentOptions = { | ||
graph: GraphSpinner, | ||
cube: CubeSpinner, | ||
circle: CircleSpinner, | ||
dot: DotSpinner, | ||
}; | ||
|
||
const getSpinner = spinnerType => | ||
spinnerComponentOptions[spinnerType] || DefaultSpinner; | ||
|
||
/** | ||
* A Loading component that wraps any other component and displays a spinner until the wrapped component has rendered. | ||
*/ | ||
export default class Loading extends Component { | ||
render() { | ||
const { | ||
loading_state, | ||
color, | ||
className, | ||
style, | ||
parent_className, | ||
parent_style, | ||
fullscreen, | ||
debug, | ||
type: spinnerType, | ||
} = this.props; | ||
|
||
const isLoading = loading_state && loading_state.is_loading; | ||
const Spinner = isLoading && getSpinner(spinnerType); | ||
|
||
return ( | ||
<div | ||
className={parent_className} | ||
style={ | ||
isLoading | ||
? mergeRight(hiddenContainer, parent_style) | ||
: parent_style | ||
const Loading = ({ | ||
children, | ||
loading_state, | ||
mode, | ||
color, | ||
className, | ||
style, | ||
parent_className, | ||
parent_style, | ||
overlay_style, | ||
fullscreen, | ||
debug, | ||
show_initially, | ||
type: spinnerType, | ||
delay_hide, | ||
delay_show, | ||
target_components, | ||
custom_spinner, | ||
}) => { | ||
const coveringSpinner = { | ||
visibility: 'visible', | ||
position: 'absolute', | ||
top: '0', | ||
height: '100%', | ||
width: '100%', | ||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
}; | ||
const hiddenContainer = mergeRight( | ||
{visibility: 'hidden', position: 'relative'}, | ||
overlay_style | ||
); | ||
|
||
/* Overrides default Loading behavior if target_components is set. By default, | ||
* Loading fires when any recursive child enters loading state. This makes loading | ||
* opt-in: Loading animation only enabled when one of target components enters loading state. | ||
*/ | ||
const isTarget = () => { | ||
AnnMarieW marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!target_components) { | ||
return true; | ||
} | ||
const isMatchingComponent = target_components.some(component => { | ||
const [component_name, prop_name] = Object.entries(component)[0]; | ||
return ( | ||
loading_state.component_name === component_name && | ||
loading_state.prop_name === prop_name | ||
); | ||
}); | ||
return isMatchingComponent; | ||
}; | ||
|
||
const [showSpinner, setShowSpinner] = useState(show_initially); | ||
AnnMarieW marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const dismissTimer = useRef(); | ||
const showTimer = useRef(); | ||
|
||
// delay_hide and delay_show is from dash-bootstrap-components dbc.Spinner | ||
useEffect(() => { | ||
if (mode === 'on' || mode === 'off') { | ||
setShowSpinner(mode === 'on'); | ||
return; | ||
} | ||
|
||
if (loading_state) { | ||
if (loading_state.is_loading) { | ||
// if component is currently loading and there's a dismiss timer active | ||
// we need to clear it. | ||
if (dismissTimer.current) { | ||
dismissTimer.current = clearTimeout(dismissTimer.current); | ||
} | ||
// if component is currently loading but the spinner is not showing and | ||
// there is no timer set to show, then set a timeout to show | ||
if (!showSpinner && !showTimer.current) { | ||
showTimer.current = setTimeout(() => { | ||
setShowSpinner(isTarget()); | ||
showTimer.current = null; | ||
}, delay_show); | ||
} | ||
} else { | ||
// if component is not currently loading and there's a show timer | ||
// active we need to clear it | ||
if (showTimer.current) { | ||
showTimer.current = clearTimeout(showTimer.current); | ||
} | ||
// if component is not currently loading and the spinner is showing and | ||
// there's no timer set to dismiss it, then set a timeout to hide it | ||
if (showSpinner && !dismissTimer.current) { | ||
dismissTimer.current = setTimeout(() => { | ||
setShowSpinner(false); | ||
dismissTimer.current = null; | ||
}, delay_hide); | ||
} | ||
> | ||
{this.props.children} | ||
<div style={isLoading ? coveringSpinner : {}}> | ||
{isLoading && ( | ||
} | ||
} | ||
}, [delay_hide, delay_show, loading_state, mode]); | ||
|
||
const Spinner = showSpinner && getSpinner(spinnerType); | ||
|
||
return ( | ||
<div | ||
className={parent_className} | ||
style={ | ||
showSpinner | ||
? mergeRight(hiddenContainer, parent_style) | ||
: parent_style | ||
} | ||
> | ||
{children} | ||
<div style={showSpinner ? coveringSpinner : {}}> | ||
{showSpinner && | ||
(custom_spinner || ( | ||
<Spinner | ||
className={className} | ||
style={style} | ||
|
@@ -75,18 +139,21 @@ export default class Loading extends Component { | |
debug={debug} | ||
fullscreen={fullscreen} | ||
/> | ||
)} | ||
</div> | ||
))} | ||
</div> | ||
); | ||
} | ||
} | ||
</div> | ||
); | ||
}; | ||
|
||
Loading._dashprivate_isLoadingComponent = true; | ||
|
||
Loading.defaultProps = { | ||
type: 'default', | ||
color: '#119DFF', | ||
delay_show: 0, | ||
delay_hide: 0, | ||
show_initially: true, | ||
mode: 'auto', | ||
}; | ||
|
||
Loading.propTypes = { | ||
|
@@ -106,24 +173,24 @@ Loading.propTypes = { | |
]), | ||
|
||
/** | ||
* Property that determines which spinner to show | ||
* Property that determines which built-in spinner to show | ||
* one of 'graph', 'cube', 'circle', 'dot', or 'default'. | ||
*/ | ||
type: PropTypes.oneOf(['graph', 'cube', 'circle', 'dot', 'default']), | ||
|
||
/** | ||
* Boolean that makes the spinner display full-screen | ||
* Boolean that makes the built-in spinner display full-screen | ||
*/ | ||
fullscreen: PropTypes.bool, | ||
|
||
/** | ||
* If true, the spinner will display the component_name and prop_name | ||
* If true, the built-in spinner will display the component_name and prop_name | ||
* while loading | ||
*/ | ||
debug: PropTypes.bool, | ||
|
||
/** | ||
* Additional CSS class for the spinner root DOM node | ||
* Additional CSS class for the built-in spinner root DOM node | ||
*/ | ||
className: PropTypes.string, | ||
|
||
|
@@ -133,17 +200,22 @@ Loading.propTypes = { | |
parent_className: PropTypes.string, | ||
|
||
/** | ||
* Additional CSS styling for the spinner root DOM node | ||
* Additional CSS styling for the built-in spinner root DOM node | ||
*/ | ||
style: PropTypes.object, | ||
|
||
/** | ||
* Additional CSS styling for the outermost dcc.Loading parent div DOM node | ||
*/ | ||
parent_style: PropTypes.object, | ||
/** | ||
* Additional CSS styling for the spinner overlay. This is applied to the | ||
* dcc.Loading children while the spinner is active. The default is `{'visibility': 'hidden'}` | ||
*/ | ||
overlay_style: PropTypes.object, | ||
|
||
/** | ||
* Primary colour used for the loading spinners | ||
* Primary color used for the built-in loading spinners | ||
*/ | ||
color: PropTypes.string, | ||
|
||
|
@@ -164,4 +236,41 @@ Loading.propTypes = { | |
*/ | ||
component_name: PropTypes.string, | ||
}), | ||
|
||
/** | ||
* Setting mode to "on" or "off" will override the loading state coming from dash-renderer | ||
*/ | ||
mode: PropTypes.oneOf(['auto', 'on', 'off']), | ||
|
||
/** | ||
* Add a time delay (in ms) to the spinner being removed to prevent flickering. | ||
*/ | ||
delay_hide: PropTypes.number, | ||
|
||
/** | ||
* Add a time delay (in ms) to the spinner being shown after the loading_state | ||
* is set to True. | ||
*/ | ||
delay_show: PropTypes.number, | ||
|
||
/** | ||
* Whether the Spinner should show on app start-up before the loading state | ||
* has been determined. Default True. Use when also setting `delay_show`. | ||
*/ | ||
show_initially: PropTypes.bool, | ||
|
||
/** | ||
* Specify component and prop to trigger showing the loading spinner | ||
* example: `[{"output-container": "children"}]` | ||
AnnMarieW marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
*/ | ||
target_components: PropTypes.arrayOf(PropTypes.object), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this would be better as objectOf(arrayOf(string)) or perhaps objectOf(oneOfType([string, arrayOf(string)])) so you could do something like: target_components={"my_table": ["data", "columns"], "my_dropdown": "options"} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added this, plus it's possible to use a wildcard. This will trigger loading for any prop in the
|
||
|
||
/** | ||
* Component to use rather than the built-in spinner specified in the `type` prop. | ||
* | ||
*/ | ||
custom_spinner: PropTypes.node, | ||
}; | ||
|
||
export default Loading; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overlay_style
to setdcc.Loading
to be semi-translucent feels like a really common use case. I wonder if it's worth adding a prop likeopacity
to specify explicitly with some default styles applied?This way developers don't need to remember the more complex e.g.
overlay_style={"visibility":"visible", "opacity": .5, "backgroundColor": "white"}
if they just want to make the overlay translucent?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that, but I expect most people would tweak the opacity and background color depending on the theme (light/dark). Or do know some CSS that would work in most cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could probably pick a neutral grey? I've found
#7F8487
is a good middle ground grey that works well in light and dark.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can try that - but still want to give the ability for people to use any CSS in
overlay_style
. Are you suggesting another prop, perhaps that sets{"visibility":"visible", "opacity": .5, "backgroundColor": "#7F8487"}
?Then what if people set both props?
If there are a couple examples in the docs, it might work OK to just leave it as-is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, maybe let's leave as-is for now with good examples in the docs, and see if there is community feedback from there?