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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [UNRELEASED]

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

## [2.16.1] - 2024-03-06

## Fixed
Expand Down
258 changes: 192 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,142 @@ 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,
display,
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 = () => {
return Object.entries(target_components).some(
([component_name, prop_names]) => {
// Convert prop_names to an array if it's not already
const prop_names_array = Array.isArray(prop_names)
? prop_names
: [prop_names];

return (
loading_state.component_name === component_name &&
(prop_names_array.includes('*') ||
prop_names_array.some(
prop_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 (display === 'show' || display === 'hide') {
setShowSpinner(display === 'show');
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);
}
>
{this.props.children}
<div style={isLoading ? coveringSpinner : {}}>
{isLoading && (
} 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);
}
}
}
}, [delay_hide, delay_show, loading_state, display]);

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 +151,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,
display: 'auto',
};

Loading.propTypes = {
Expand All @@ -106,24 +185,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 +212,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 +248,46 @@ Loading.propTypes = {
*/
component_name: PropTypes.string,
}),

/**
* Setting display to "show" or "hide" will override the loading state coming from dash-renderer
*/
display: PropTypes.oneOf(['auto', 'show', 'hide']),

/**
* 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", "grid": ["rowData", "columnDefs]}`
*
*/
target_components: PropTypes.objectOf(
PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
])
),

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

export default Loading;
Loading