Skip to content
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

Merged
merged 15 commits into from
Mar 8, 2024
Merged
241 changes: 175 additions & 66 deletions components/dash-core-components/src/components/Loading.react.js
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';
Expand All @@ -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
);
Comment on lines +52 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overlay_style to set dcc.Loading to be semi-translucent feels like a really common use case. I wonder if it's worth adding a prop like opacity 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?

Copy link
Collaborator Author

@AnnMarieW AnnMarieW Feb 26, 2024

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?

Copy link
Member

@ndrezn ndrezn Mar 4, 2024

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.

Copy link
Collaborator Author

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?

Copy link
Member

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?


/* 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}
Expand All @@ -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 = {
Expand All @@ -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,

Expand All @@ -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,

Expand All @@ -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),
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 "my_table" component

target_components={"my_table": "*", "my_dropdown": "options"}


/**
* Component to use rather than the built-in spinner specified in the `type` prop.
*
*/
custom_spinner: PropTypes.node,
};

export default Loading;