diff --git a/src/components/Loading.react.js b/src/components/Loading.react.js index 92cf41223..cfbc8ba0b 100644 --- a/src/components/Loading.react.js +++ b/src/components/Loading.react.js @@ -37,9 +37,7 @@ export default class Loading extends Component { type, } = this.props; - const isLoading = getLoadingState(this); - - if (isLoading) { + if (loading_state && loading_state.is_loading) { const Spinner = getSpinner(type); return ( ); } + if ( R.type(this.props.children) !== 'Object' || R.type(this.props.children) !== 'Function' @@ -62,6 +61,8 @@ export default class Loading extends Component { } } +Loading._dashprivate_isLoadingComponent = true; + Loading.defaultProps = { type: 'default', color: '#119DFF', @@ -126,23 +127,3 @@ Loading.propTypes = { component_name: PropTypes.string, }), }; - -function getLoadingState(element) { - if ( - element.props && - element.props.loading_state && - element.props.loading_state.is_loading - ) { - return true; - } - - const children = element.props && element.props.children; - if (!children) { - return false; - } - - return R.any( - child => child.type !== Loading && getLoadingState(child), - Array.isArray(children) ? children : [children] - ); -} diff --git a/src/fragments/Loading/spinners/CircleSpinner.jsx b/src/fragments/Loading/spinners/CircleSpinner.jsx index f796525af..c7d1369a3 100644 --- a/src/fragments/Loading/spinners/CircleSpinner.jsx +++ b/src/fragments/Loading/spinners/CircleSpinner.jsx @@ -28,7 +28,7 @@ const CircleSpinner = ({ return (
{debugTitle} -
+
@@ -160,7 +160,7 @@ const CircleSpinner = ({ .dash-spinner-container .sk-circle .sk-circle12:before { -webkit-animation-delay: -0.1s; animation-delay: -0.1s; } - + @-webkit-keyframes dash-sk-circleBounceDelay { 0%, 80%, 100% { -webkit-transform: scale(0); @@ -170,7 +170,7 @@ const CircleSpinner = ({ transform: scale(1); } } - + @keyframes dash-sk-circleBounceDelay { 0%, 80%, 100% { -webkit-transform: scale(0); diff --git a/src/fragments/Loading/spinners/CubeSpinner.jsx b/src/fragments/Loading/spinners/CubeSpinner.jsx index 8e4608e4f..73dd71c79 100644 --- a/src/fragments/Loading/spinners/CubeSpinner.jsx +++ b/src/fragments/Loading/spinners/CubeSpinner.jsx @@ -20,7 +20,7 @@ const CubeSpinner = ({status, color, fullscreen, debug, className, style}) => { return (
{debugTitle} -
+
@@ -61,16 +61,16 @@ const CubeSpinner = ({status, color, fullscreen, debug, className, style}) => { width: 80px; margin: 7rem auto; } - + .dash-cube-side { width: 100%; height: 100%; position: absolute; display: inline-block; } - + .dash-cube-side--front { - background-color: ${color}; + background-color: ${color}; animation: blowout-front 4s infinite; transform: rotateY(0deg) translateZ(40px); } @@ -79,31 +79,31 @@ const CubeSpinner = ({status, color, fullscreen, debug, className, style}) => { transform: rotateX(180deg) translateZ(40px); animation: blowout-back 4s infinite; } - + .dash-cube-side--left { background-color: ${changeColor(color).darken(0.2)}; transform: rotateY(-90deg) translateZ(40px); animation: blowout-left 4s infinite; } - + .dash-cube-side--right { background-color: ${changeColor(color).darken(0.4)}; transform: rotateY(90deg) translateZ(40px); animation: blowout-right 4s infinite; } - + .dash-cube-side--top { background-color: ${changeColor(color).darken(0.2)}; transform: rotateX(90deg) translateZ(40px); animation: blowout-top 4s infinite; } - + .dash-cube-side--bottom { background-color: ${changeColor(color).darken(0.4)}; transform: rotateX(-90deg) translateZ(40px); animation: blowout-bottom 4s infinite; } - + @keyframes rotate { 0% { transform: rotateX(0deg) rotateY(0deg); @@ -115,7 +115,7 @@ const CubeSpinner = ({status, color, fullscreen, debug, className, style}) => { transform: rotateX(360deg) rotateY(360deg); } } - + @keyframes blowout-bottom { 20% { transform: rotateX(-90deg) translateZ(40px); diff --git a/src/fragments/Loading/spinners/DefaultSpinner.jsx b/src/fragments/Loading/spinners/DefaultSpinner.jsx index 96dcec654..83b7ec392 100644 --- a/src/fragments/Loading/spinners/DefaultSpinner.jsx +++ b/src/fragments/Loading/spinners/DefaultSpinner.jsx @@ -28,7 +28,7 @@ const DefaultSpinner = ({ return (
{debugTitle} -
+
@@ -59,48 +59,48 @@ const DefaultSpinner = ({ text-align: center; font-size: 10px; } - + .dash-default-spinner > div { background-color: ${color}; height: 100%; width: 6px; display: inline-block; margin-right: 4px; - + -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out; animation: sk-stretchdelay 1.2s infinite ease-in-out; } - + .dash-default-spinner .dash-default-spinner-rect2 { -webkit-animation-delay: -1.1s; animation-delay: -1.1s; } - + .dash-default-spinner .dash-default-spinner-rect3 { -webkit-animation-delay: -1.0s; animation-delay: -1.0s; } - + .dash-default-spinner .dash-default-spinner-rect4 { -webkit-animation-delay: -0.9s; animation-delay: -0.9s; } - + .dash-default-spinner .dash-default-spinner-rect5 { -webkit-animation-delay: -0.8s; animation-delay: -0.8s; } - + @-webkit-keyframes sk-stretchdelay { - 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } + 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 20% { -webkit-transform: scaleY(1.0) } } - + @keyframes sk-stretchdelay { - 0%, 40%, 100% { + 0%, 40%, 100% { transform: scaleY(0.4); -webkit-transform: scaleY(0.4); - } 20% { + } 20% { transform: scaleY(1.0); -webkit-transform: scaleY(1.0); } diff --git a/src/fragments/Loading/spinners/DotSpinner.jsx b/src/fragments/Loading/spinners/DotSpinner.jsx index 48b20f11f..7f41a1264 100644 --- a/src/fragments/Loading/spinners/DotSpinner.jsx +++ b/src/fragments/Loading/spinners/DotSpinner.jsx @@ -21,7 +21,7 @@ const DotSpinner = ({status, color, fullscreen, debug, className, style}) => { return (
{debugTitle} -
+
@@ -48,38 +48,38 @@ const DotSpinner = ({status, color, fullscreen, debug, className, style}) => { width: 70px; text-align: center; } - + .dash-dot-spinner > div { width: 18px; height: 18px; background-color: ${color}; - + border-radius: 100%; display: inline-block; -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; animation: sk-bouncedelay 1.4s infinite ease-in-out both; } - + .dash-dot-spinner .dash-dot-spinner-bounce1 { -webkit-animation-delay: -0.32s; animation-delay: -0.32s; } - + .dash-dot-spinner .dash-dot-spinner-bounce2 { -webkit-animation-delay: -0.16s; animation-delay: -0.16s; } - + @-webkit-keyframes sk-bouncedelay { 0%, 80%, 100% { -webkit-transform: scale(0) } 40% { -webkit-transform: scale(1.0) } } - + @keyframes sk-bouncedelay { - 0%, 80%, 100% { + 0%, 80%, 100% { -webkit-transform: scale(0); transform: scale(0); - } 40% { + } 40% { -webkit-transform: scale(1.0); transform: scale(1.0); } diff --git a/src/fragments/Loading/spinners/GraphSpinner.jsx b/src/fragments/Loading/spinners/GraphSpinner.jsx index f427399a9..5cc4ac5e2 100644 --- a/src/fragments/Loading/spinners/GraphSpinner.jsx +++ b/src/fragments/Loading/spinners/GraphSpinner.jsx @@ -19,18 +19,18 @@ const GraphSpinner = ({status, fullscreen, debug, className, style}) => {
{debugTitle} -
-
+
+
-
+
-
+
@@ -56,7 +56,7 @@ const GraphSpinner = ({status, fullscreen, debug, className, style}) => { .dash-loading-title { text-align: center; } - .dash-spinner { + .dash-graph-spinner { display: flex; margin: 0 auto; width: 200px; @@ -65,14 +65,14 @@ const GraphSpinner = ({status, fullscreen, debug, className, style}) => { z-index: -2; border-radius: 4px; } - .dash-spinner__bottom { + .dash-graph-spinner__bottom { display: flex; margin-top: auto; flex-direction: column; height: 12px; width: 100%; } - .dash-spinner__background { + .dash-graph-spinner__background { width: 100%; height: 100%; display: block; diff --git a/test/test_integration.py b/test/test_integration.py index fc6054ff5..679d57cc3 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -216,13 +216,311 @@ def test_upload_gallery(self): self.snapshot('test_upload_gallery') + def test_loading_component_initialization(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Div(id='div-1') + ], className='loading') + ], id='root') + + @app.callback( + Output('div-1', 'children'), + [Input('root', 'n_clicks')] + ) + def updateDiv(children): + with lock: + return 'content' + + with lock: + self.startServer(app) + self.wait_for_element_by_css_selector( + '.loading .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading #div-1' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_loading_component_action(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Div(id='div-1') + ], className='loading') + ], 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 + + return 'content' + + with lock: + self.startServer(app) + self.wait_for_element_by_css_selector( + '.loading #div-1' + ) + + self.driver.find_element_by_id('root').click() + + self.wait_for_element_by_css_selector( + '.loading .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading #div-1' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_multiple_loading_components(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Button(id='btn-1') + ], className='loading-1'), + dcc.Loading([ + html.Button(id='btn-2') + ], className='loading-2') + ], id='root') + + @app.callback( + Output('btn-1', 'value'), + [Input('btn-2', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + @app.callback( + Output('btn-2', 'value'), + [Input('btn-1', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + self.startServer(app) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-1').click() + + self.wait_for_element_by_css_selector( + '.loading-2 .dash-spinner' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-2').click() + + self.wait_for_element_by_css_selector( + '.loading-1 .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_nested_loading_components(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Button(id='btn-1'), + dcc.Loading([ + html.Button(id='btn-2') + ], className='loading-2') + ], className='loading-1') + ], id='root') + + @app.callback( + Output('btn-1', 'value'), + [Input('btn-2', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + @app.callback( + Output('btn-2', 'value'), + [Input('btn-1', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + self.startServer(app) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-1').click() + + self.wait_for_element_by_css_selector( + '.loading-2 .dash-spinner' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-2').click() + + self.wait_for_element_by_css_selector( + '.loading-1 .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_dynamic_loading_component(self): + lock = Lock() + + app = dash.Dash(__name__) + app.config['suppress_callback_exceptions'] = True + + app.layout = html.Div([ + html.Button(id='btn-1'), + html.Div(id='div-1') + ]) + + @app.callback( + Output('div-1', 'children'), + [Input('btn-1', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is None: + return + + with lock: + return html.Div([ + html.Button(id='btn-2'), + dcc.Loading([ + html.Button(id='btn-3') + ], className='loading-1') + ]) + + @app.callback( + Output('btn-3', 'content'), + [Input('btn-2', 'n_clicks')] + ) + def updateDynamic(n_clicks): + if n_clicks is None: + return + + with lock: + return 'content' + + self.startServer(app) + + self.wait_for_element_by_css_selector( + '#btn-1' + ) + self.wait_for_element_by_css_selector( + '#div-1' + ) + + self.driver.find_element_by_id('btn-1').click() + + self.wait_for_element_by_css_selector( + '#div-1 #btn-2' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-3' + ) + + with lock: + self.driver.find_element_by_id('btn-2').click() + + self.wait_for_element_by_css_selector( + '.loading-1 .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '#div-1 #btn-2' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-3' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + def test_loading_slider(self): lock = Lock() - lock.acquire() app = dash.Dash(__name__) app.layout = html.Div([ + html.Button(id='test-btn'), html.Label(id='test-div', children=['Horizontal Slider']), dcc.Slider( id='horizontal-slider', @@ -236,19 +534,29 @@ def test_loading_slider(self): @app.callback( Output('horizontal-slider', 'value'), - [Input('test-div', 'children')] + [Input('test-btn', 'n_clicks')] ) - def delayed_value(children): - lock.acquire() - lock.release() - return 5 + def user_delayed_value(n_clicks): + with lock: + return 5 - self.startServer(app) + with lock: + self.startServer(app) + + self.wait_for_element_by_css_selector( + '#horizontal-slider[data-dash-is-loading="true"]' + ) self.wait_for_element_by_css_selector( - '#horizontal-slider[data-dash-is-loading="true"]' + '#horizontal-slider:not([data-dash-is-loading="true"])' ) - lock.release() + + with lock: + self.driver.find_element_by_id('test-btn').click() + + self.wait_for_element_by_css_selector( + '#horizontal-slider[data-dash-is-loading="true"]' + ) self.wait_for_element_by_css_selector( '#horizontal-slider:not([data-dash-is-loading="true"])' @@ -314,11 +622,11 @@ def test_vertical_slider(self): def test_loading_range_slider(self): lock = Lock() - lock.acquire() app = dash.Dash(__name__) app.layout = html.Div([ + html.Button(id='test-btn'), html.Label(id='test-div', children=['Horizontal Range Slider']), dcc.RangeSlider( id='horizontal-range-slider', @@ -332,19 +640,29 @@ def test_loading_range_slider(self): @app.callback( Output('horizontal-range-slider', 'value'), - [Input('test-div', 'children')] + [Input('test-btn', 'n_clicks')] ) def delayed_value(children): - lock.acquire() - lock.release() - return [4, 6] + with lock: + return [4, 6] - self.startServer(app) + with lock: + self.startServer(app) + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider[data-dash-is-loading="true"]' + ) self.wait_for_element_by_css_selector( - '#horizontal-range-slider[data-dash-is-loading="true"]' + '#horizontal-range-slider:not([data-dash-is-loading="true"])' ) - lock.release() + + with lock: + self.driver.find_element_by_id('test-btn').click() + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider[data-dash-is-loading="true"]' + ) self.wait_for_element_by_css_selector( '#horizontal-range-slider:not([data-dash-is-loading="true"])' diff --git a/test/unit/__snapshots__/Loading.test.js.snap b/test/unit/__snapshots__/Loading.test.js.snap index 85e05510b..29d3a8c2b 100644 --- a/test/unit/__snapshots__/Loading.test.js.snap +++ b/test/unit/__snapshots__/Loading.test.js.snap @@ -1,80 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Loading checks all it's children for a loading_state: Loading spinner for children 1`] = ` -"
" -`; +exports[`Loading checks all it's children for a loading_state: Loading spinner for children 1`] = `"
Child 1
Child 2
Child 3
"`; exports[`Loading renders with multiple children: Loading with is_loading=true 1`] = ` -"