From 27c349805239ca4a3089871d7e47886d10e1224f Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Thu, 15 Feb 2024 08:51:17 -0800 Subject: [PATCH 1/9] New features for dcc.Loading --- .../src/components/Loading.react.js | 224 +++++++++++++----- 1 file changed, 163 insertions(+), 61 deletions(-) diff --git a/components/dash-core-components/src/components/Loading.react.js b/components/dash-core-components/src/components/Loading.react.js index b46b8ebc2b..6dabc51c66 100644 --- a/components/dash-core-components/src/components/Loading.react.js +++ b/components/dash-core-components/src/components/Loading.react.js @@ -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,121 @@ 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 spinnerComponents = { + graph: GraphSpinner, + cube: CubeSpinner, + circle: CircleSpinner, + dot: DotSpinner, }; +const getSpinner = spinnerType => + spinnerComponents[spinnerType] || DefaultSpinner; + +const hiddenContainer = {position: 'relative'}; + /** * 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 ( -
{ + const coveringSpinner = { + visibility: 'visible', + position: 'absolute', + top: '0', + height: '100%', + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: backgroundColor, + opacity: opacity, + }; + + const isTarget = () => { + 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); + const dismissTimer = useRef(); + const showTimer = useRef(); + + // delay_hide and delay_show is from dash-bootstrap-components dbc.Spinner + useEffect(() => { + 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); } - > - {this.props.children} -
- {isLoading && ( + // 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); + } + } + } + }, [delay_hide, delay_show, loading_state]); + + const Spinner = showSpinner && getSpinner(spinnerType); + + return ( +
+ {children} +
+ {showSpinner && + (custom_spinner || ( - )} -
+ ))}
- ); - } -} +
+ ); +}; Loading._dashprivate_isLoadingComponent = true; Loading.defaultProps = { type: 'default', color: '#119DFF', + delay_show: 0, + delay_hide: 0, + show_initially: true, + opacity: 0.5, + backgroundColor: 'white', }; Loading.propTypes = { @@ -143,10 +202,21 @@ Loading.propTypes = { parent_style: PropTypes.object, /** - * Primary colour used for the loading spinners + * Primary color used for the loading spinners */ color: PropTypes.string, + /** + * Opacity of loading Spinner div. Can take a value from 0.0 - 1.0. The lower + * the value, the more transparent: + */ + opacity: PropTypes.number, + + /** + * Background color of the loading Spinner div + */ + backgroundColor: PropTypes.string, + /** * Object that holds the loading state object coming from dash-renderer */ @@ -164,4 +234,36 @@ Loading.propTypes = { */ component_name: PropTypes.string, }), + + /** + * 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. + */ + show_initially: PropTypes.bool, + + /** + * Specify component and prop to trigger showing the loading spinner + * example: `[{"output-container": "children"}]` + * + */ + target_components: PropTypes.arrayOf(PropTypes.object), + + /** + * Component to use rather than the spinner specified in the `type` prop. + * + */ + custom_spinner: PropTypes.node, }; + +export default Loading; From b5b9710769617f62565640712ae680d7d64755cc Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Thu, 15 Feb 2024 16:07:23 -0800 Subject: [PATCH 2/9] Added overlay_style prop removed opacity and backgroundColor props --- .../src/components/Loading.react.js | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/components/dash-core-components/src/components/Loading.react.js b/components/dash-core-components/src/components/Loading.react.js index 6dabc51c66..0bd44ccbca 100644 --- a/components/dash-core-components/src/components/Loading.react.js +++ b/components/dash-core-components/src/components/Loading.react.js @@ -7,7 +7,7 @@ import CircleSpinner from '../fragments/Loading/spinners/CircleSpinner.jsx'; import DotSpinner from '../fragments/Loading/spinners/DotSpinner.jsx'; import {mergeRight} from 'ramda'; -const spinnerComponents = { +const spinnerComponentOptions = { graph: GraphSpinner, cube: CubeSpinner, circle: CircleSpinner, @@ -15,9 +15,7 @@ const spinnerComponents = { }; const getSpinner = spinnerType => - spinnerComponents[spinnerType] || DefaultSpinner; - -const hiddenContainer = {position: 'relative'}; + spinnerComponentOptions[spinnerType] || DefaultSpinner; /** * A Loading component that wraps any other component and displays a spinner until the wrapped component has rendered. @@ -30,14 +28,13 @@ const Loading = ({ style, parent_className, parent_style, + overlay_style, fullscreen, debug, show_initially, type: spinnerType, delay_hide, delay_show, - opacity, - backgroundColor, target_components, custom_spinner, }) => { @@ -50,10 +47,16 @@ const Loading = ({ display: 'flex', justifyContent: 'center', alignItems: 'center', - backgroundColor: backgroundColor, - opacity: opacity, }; + 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 = () => { if (!target_components) { return true; @@ -144,8 +147,6 @@ Loading.defaultProps = { delay_show: 0, delay_hide: 0, show_initially: true, - opacity: 0.5, - backgroundColor: 'white', }; Loading.propTypes = { @@ -200,22 +201,16 @@ Loading.propTypes = { * Additional CSS styling for the outermost dcc.Loading parent div DOM node */ parent_style: PropTypes.object, - /** - * Primary color used for the loading spinners + * 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'} */ - color: PropTypes.string, + overlay_style: PropTypes.object, /** - * Opacity of loading Spinner div. Can take a value from 0.0 - 1.0. The lower - * the value, the more transparent: - */ - opacity: PropTypes.number, - - /** - * Background color of the loading Spinner div + * Primary color used for the loading spinners */ - backgroundColor: PropTypes.string, + color: PropTypes.string, /** * Object that holds the loading state object coming from dash-renderer From d87be7647577b476bf8c107e55d4616a5be5adbf Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Sun, 18 Feb 2024 11:45:18 -0800 Subject: [PATCH 3/9] update doc strings --- .../src/components/Loading.react.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/components/dash-core-components/src/components/Loading.react.js b/components/dash-core-components/src/components/Loading.react.js index 0bd44ccbca..f7acbd545b 100644 --- a/components/dash-core-components/src/components/Loading.react.js +++ b/components/dash-core-components/src/components/Loading.react.js @@ -166,24 +166,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, @@ -193,7 +193,7 @@ 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, @@ -203,12 +203,12 @@ Loading.propTypes = { 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'} + * dcc.Loading children while the spinner is active. The default is `{'visibility': 'hidden'}` */ overlay_style: PropTypes.object, /** - * Primary color used for the loading spinners + * Primary color used for the built-in loading spinners */ color: PropTypes.string, @@ -243,7 +243,7 @@ Loading.propTypes = { /** * Whether the Spinner should show on app start-up before the loading state - * has been determined. Default True. + * has been determined. Default True. Use when also setting `delay_show`. */ show_initially: PropTypes.bool, @@ -255,7 +255,7 @@ Loading.propTypes = { target_components: PropTypes.arrayOf(PropTypes.object), /** - * Component to use rather than the spinner specified in the `type` prop. + * Component to use rather than the built-in spinner specified in the `type` prop. * */ custom_spinner: PropTypes.node, From 872e8a72a182e24a0eed7aec693ca2b4ef2c7790 Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Sun, 18 Feb 2024 13:16:00 -0800 Subject: [PATCH 4/9] add the mode prop for manual override of loading state --- .../src/components/Loading.react.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/components/dash-core-components/src/components/Loading.react.js b/components/dash-core-components/src/components/Loading.react.js index f7acbd545b..60a10fafcf 100644 --- a/components/dash-core-components/src/components/Loading.react.js +++ b/components/dash-core-components/src/components/Loading.react.js @@ -23,6 +23,7 @@ const getSpinner = spinnerType => const Loading = ({ children, loading_state, + mode, color, className, style, @@ -77,6 +78,11 @@ const Loading = ({ // 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 @@ -108,7 +114,7 @@ const Loading = ({ } } } - }, [delay_hide, delay_show, loading_state]); + }, [delay_hide, delay_show, loading_state, mode]); const Spinner = showSpinner && getSpinner(spinnerType); @@ -147,6 +153,7 @@ Loading.defaultProps = { delay_show: 0, delay_hide: 0, show_initially: true, + mode: 'auto', }; Loading.propTypes = { @@ -230,6 +237,11 @@ 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. */ From 2f0ab2d962c2397ee5b0a9029741b45b48ce278b Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Tue, 27 Feb 2024 10:08:21 -0800 Subject: [PATCH 5/9] refactored target_components prop --- .../src/components/Loading.react.js | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/components/dash-core-components/src/components/Loading.react.js b/components/dash-core-components/src/components/Loading.react.js index 60a10fafcf..0210ad32d2 100644 --- a/components/dash-core-components/src/components/Loading.react.js +++ b/components/dash-core-components/src/components/Loading.react.js @@ -62,13 +62,25 @@ const Loading = ({ 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 + 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; }; @@ -261,10 +273,15 @@ Loading.propTypes = { /** * Specify component and prop to trigger showing the loading spinner - * example: `[{"output-container": "children"}]` + * example: `{"output-container": "children", "grid": ["rowData", "columnDefs]}` * */ - target_components: PropTypes.arrayOf(PropTypes.object), + 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. From 2c0961af81abdb53361bfc89a7307414c35f762f Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Thu, 29 Feb 2024 05:27:04 -0800 Subject: [PATCH 6/9] changed prop name from mode to display --- .../src/components/Loading.react.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/components/dash-core-components/src/components/Loading.react.js b/components/dash-core-components/src/components/Loading.react.js index 0210ad32d2..71538254c1 100644 --- a/components/dash-core-components/src/components/Loading.react.js +++ b/components/dash-core-components/src/components/Loading.react.js @@ -23,7 +23,7 @@ const getSpinner = spinnerType => const Loading = ({ children, loading_state, - mode, + display, color, className, style, @@ -90,8 +90,8 @@ const Loading = ({ // delay_hide and delay_show is from dash-bootstrap-components dbc.Spinner useEffect(() => { - if (mode === 'on' || mode === 'off') { - setShowSpinner(mode === 'on'); + if (display === 'show' || display === 'hide') { + setShowSpinner(display === 'show'); return; } @@ -126,7 +126,7 @@ const Loading = ({ } } } - }, [delay_hide, delay_show, loading_state, mode]); + }, [delay_hide, delay_show, loading_state, display]); const Spinner = showSpinner && getSpinner(spinnerType); @@ -165,7 +165,7 @@ Loading.defaultProps = { delay_show: 0, delay_hide: 0, show_initially: true, - mode: 'auto', + display: 'auto', }; Loading.propTypes = { @@ -250,9 +250,9 @@ Loading.propTypes = { }), /** - * Setting mode to "on" or "off" will override the loading state coming from dash-renderer + * Setting display to "show" or "hide" will override the loading state coming from dash-renderer */ - mode: PropTypes.oneOf(['auto', 'on', 'off']), + display: PropTypes.oneOf(['auto', 'show', 'hide']), /** * Add a time delay (in ms) to the spinner being removed to prevent flickering. From 4edbe554cfb93de75c896d09e356413aef8e8e94 Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Sun, 3 Mar 2024 15:24:02 -0800 Subject: [PATCH 7/9] added tests --- .../loading/test_loading_component.py | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) diff --git a/components/dash-core-components/tests/integration/loading/test_loading_component.py b/components/dash-core-components/tests/integration/loading/test_loading_component.py index b84c1ce72d..7568edc0b5 100644 --- a/components/dash-core-components/tests/integration/loading/test_loading_component.py +++ b/components/dash-core-components/tests/integration/loading/test_loading_component.py @@ -1,6 +1,7 @@ from multiprocessing import Lock from dash import Dash, Input, Output, dcc, html from dash.testing import wait +import time def test_ldcp001_loading_component_initialization(dash_dcc): @@ -366,3 +367,324 @@ def test_ldcp008_graph_in_loading_fits_container_height(dash_dcc): ) == dash_dcc.wait_for_element(".outer-container").size.get("height") assert dash_dcc.get_logs() == [] + + +def test_ldcp009_loading_component_overlay_style(dash_dcc): + lock = Lock() + + app = Dash(__name__) + + app.layout = html.Div( + [ + dcc.Loading( + [html.Div(id="div-1")], + className="loading", + overlay_style={ + "visibility": "visible", + "opacity": 0.5, + "backgroundColor": "white", + }, + ) + ], + id="root", + ) + + @app.callback(Output("div-1", "children"), [Input("root", "n_clicks")]) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return "changed" + + return "content" + + with lock: + dash_dcc.start_server(app) + dash_dcc.wait_for_text_to_equal("#div-1", "content") + + dash_dcc.find_element("#root").click() + + dash_dcc.find_element(".loading .dash-spinner") + # unlike the default, the content should be visible + dash_dcc.wait_for_text_to_equal("#div-1", "content") + dash_dcc.wait_for_style_to_equal("#root > div", "opacity", "0.5") + + dash_dcc.wait_for_text_to_equal("#div-1", "changed") + + assert dash_dcc.get_logs() == [] + + +# multiple components, only one triggers the spinner +def test_ldcp010_loading_component_target_components(dash_dcc): + + lock = Lock() + + app = Dash(__name__) + + app.layout = html.Div( + [ + dcc.Loading( + [ + html.Button(id="btn-1"), + html.Button(id="btn-2"), + ], + className="loading-1", + target_components={"btn-2": "children"}, + ) + ], + id="root", + ) + + @app.callback(Output("btn-1", "children"), [Input("btn-2", "n_clicks")]) + def updateDiv1(n_clicks): + if n_clicks: + with lock: + return "changed 1" + + return "content 1" + + @app.callback(Output("btn-2", "children"), [Input("btn-1", "n_clicks")]) + def updateDiv2(n_clicks): + if n_clicks: + with lock: + return "changed 2" + + return "content 2" + + dash_dcc.start_server(app) + + dash_dcc.wait_for_text_to_equal("#btn-1", "content 1") + dash_dcc.wait_for_text_to_equal("#btn-2", "content 2") + + with lock: + dash_dcc.find_element("#btn-1").click() + + dash_dcc.find_element(".loading-1 .dash-spinner") + dash_dcc.wait_for_text_to_equal("#btn-2", "") + + dash_dcc.wait_for_text_to_equal("#btn-2", "changed 2") + + with lock: + dash_dcc.find_element("#btn-2").click() + spinners = dash_dcc.find_elements(".loading-1 .dash-spinner") + dash_dcc.wait_for_text_to_equal("#btn-1", "") + + dash_dcc.wait_for_text_to_equal("#btn-1", "changed 1") + assert spinners == [] + + assert dash_dcc.get_logs() == [] + + +# update multiple props of same component, only targeted id/prop triggers spinner +def test_ldcp011_loading_component_target_components(dash_dcc): + lock = Lock() + + app = Dash(__name__) + + app.layout = html.Div( + [ + dcc.Loading( + [ + html.Button(id="btn-1"), + html.Button(id="btn-2"), + html.Button(id="btn-3"), + ], + className="loading-1", + target_components={"btn-1": "className"}, + ) + ], + id="root", + ) + + @app.callback(Output("btn-1", "children"), [Input("btn-2", "n_clicks")]) + def updateDiv1(n_clicks): + if n_clicks: + with lock: + return "changed 1" + return "content 1" + + @app.callback(Output("btn-1", "className"), [Input("btn-3", "n_clicks")]) + def updateDiv2(n_clicks): + if n_clicks: + with lock: + return "new-class" + return "" + + dash_dcc.start_server(app) + + dash_dcc.wait_for_text_to_equal("#btn-1", "content 1") + + with lock: + dash_dcc.find_element("#btn-2").click() + + spinners = dash_dcc.find_elements(".loading-1 .dash-spinner") + dash_dcc.wait_for_text_to_equal("#btn-1", "") + dash_dcc.wait_for_text_to_equal("#btn-1", "changed 1") + assert spinners == [] + + with lock: + dash_dcc.find_element("#btn-3").click() + + dash_dcc.find_element(".loading-1 .dash-spinner") + dash_dcc.wait_for_text_to_equal("#btn-1", "") + + dash_dcc.wait_for_class_to_equal("#btn-1", "new-class") + + assert dash_dcc.get_logs() == [] + + +def test_ldcp012_loading_component_custom_spinner(dash_dcc): + lock = Lock() + + app = Dash(__name__) + + app.layout = html.Div( + [ + dcc.Loading( + [html.Div(id="div-1")], + className="loading", + custom_spinner=html.Div(id="my-spinner"), + ) + ], + id="root", + ) + + @app.callback(Output("div-1", "children"), [Input("root", "n_clicks")]) + def updateDiv(n_clicks): + if n_clicks: + with lock: + return "changed" + return "content" + + with lock: + dash_dcc.start_server(app) + dash_dcc.wait_for_text_to_equal("#div-1", "content") + + dash_dcc.find_element("#root").click() + + dash_spinner = dash_dcc.find_elements(".loading .dash-spinner") + dash_dcc.find_element("#my-spinner") + # mounted but hidden, so looks like no text + dash_dcc.wait_for_text_to_equal("#div-1", "") + + dash_dcc.wait_for_text_to_equal("#div-1", "changed") + assert dash_spinner == [] + + assert dash_dcc.get_logs() == [] + + +def test_ldcp013_loading_component_display_show(dash_dcc): + + app = Dash(__name__) + + app.layout = html.Div( + [ + dcc.Loading( + [html.Div("content", id="div-1")], className="loading", display="show" + ) + ], + id="root", + ) + dash_dcc.start_server(app) + + dash_dcc.find_elements(".loading .dash-spinner") + # mounted but hidden, so looks like no text + dash_dcc.wait_for_text_to_equal("#div-1", "") + + assert dash_dcc.get_logs() == [] + + +# Same as ldcp002, but with the display="hide", the spinner should not show +def test_ldcp014_loading_component_delay_hide(dash_dcc): + lock = Lock() + + app = Dash(__name__) + + app.layout = html.Div( + [dcc.Loading([html.Div(id="div-1")], className="loading", display="hide")], + id="root", + ) + + @app.callback(Output("div-1", "children"), [Input("root", "n_clicks")]) + def updateDiv(n_clicks): + if n_clicks: + with lock: + return "changed" + return "content" + + with lock: + dash_dcc.start_server(app) + dash_dcc.wait_for_text_to_equal("#div-1", "content") + + dash_dcc.find_element("#root").click() + + spinners = dash_dcc.find_elements(".loading .dash-spinner") + + dash_dcc.wait_for_text_to_equal("#div-1", "changed") + assert spinners == [] + + assert dash_dcc.get_logs() == [] + + +# Same as ldcp002, but with the delay, the spinner should not show +def test_ldcp015_loading_component_delay_show(dash_dcc): + lock = Lock() + + app = Dash(__name__) + + app.layout = html.Div( + [dcc.Loading([html.Div(id="div-1")], className="loading", delay_show=2500)], + id="root", + ) + + @app.callback(Output("div-1", "children"), [Input("root", "n_clicks")]) + def updateDiv(n_clicks): + if n_clicks: + with lock: + return "changed" + return "content" + + with lock: + dash_dcc.start_server(app) + dash_dcc.wait_for_text_to_equal("#div-1", "content") + + dash_dcc.find_element("#root").click() + + spinners = dash_dcc.find_elements(".loading .dash-spinner") + # mounted but hidden, so looks like no text + dash_dcc.wait_for_text_to_equal("#div-1", "") + + dash_dcc.wait_for_text_to_equal("#div-1", "changed") + assert spinners == [] + + assert dash_dcc.get_logs() == [] + + +def test_ldcp016_loading_component_delay_hide(dash_dcc): + lock = Lock() + + app = Dash(__name__) + + app.layout = html.Div( + [dcc.Loading([html.Div(id="div-1")], className="loading", delay_hide=300)], + id="root", + ) + + @app.callback(Output("div-1", "children"), [Input("root", "n_clicks")]) + def updateDiv(n_clicks): + if n_clicks: + with lock: + return "changed" + return "content" + + with lock: + dash_dcc.start_server(app) + dash_dcc.wait_for_text_to_equal("#div-1", "content") + + dash_dcc.find_element("#root").click() + dash_dcc.find_element(".loading .dash-spinner") + + time.sleep(0.2) + dash_dcc.find_element(".loading .dash-spinner") + dash_dcc.wait_for_text_to_equal("#div-1", "changed") + + assert dash_dcc.get_logs() == [] From d1c0a7ff40686c81175e557f801a820ff82c4985 Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Sun, 3 Mar 2024 16:39:26 -0800 Subject: [PATCH 8/9] added changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d9cf35d2..5b0a01faee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.0] - 2024-03-01 ## Fixed From d62a037a0bbfce48b642b52b3025a9a7a2cf2d24 Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Mon, 4 Mar 2024 10:27:23 -0800 Subject: [PATCH 9/9] fixed typo in test --- .../tests/integration/loading/test_loading_component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dash-core-components/tests/integration/loading/test_loading_component.py b/components/dash-core-components/tests/integration/loading/test_loading_component.py index 7568edc0b5..e511f01308 100644 --- a/components/dash-core-components/tests/integration/loading/test_loading_component.py +++ b/components/dash-core-components/tests/integration/loading/test_loading_component.py @@ -586,7 +586,7 @@ def test_ldcp013_loading_component_display_show(dash_dcc): ) dash_dcc.start_server(app) - dash_dcc.find_elements(".loading .dash-spinner") + dash_dcc.find_element(".loading .dash-spinner") # mounted but hidden, so looks like no text dash_dcc.wait_for_text_to_equal("#div-1", "")