Skip to content

Commit 047a94b

Browse files
authored
Merge pull request #561 from facultyai/debounce-spinner
Add debounce prop to spinner + tests
2 parents 93f2ba3 + 0f5fe71 commit 047a94b

File tree

2 files changed

+92
-12
lines changed

2 files changed

+92
-12
lines changed

src/components/Spinner.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, {Fragment} from 'react';
1+
import React, {useEffect, useRef, useState} from 'react';
22
import PropTypes from 'prop-types';
3-
import {omit, type} from 'ramda';
3+
import {omit} from 'ramda';
44
import {Spinner as RSSpinner} from 'reactstrap';
55
import {bootstrapColors} from '../private/BootstrapColors';
66

@@ -21,9 +21,27 @@ const Spinner = props => {
2121
fullscreen,
2222
fullscreenClassName,
2323
fullscreen_style,
24+
debounce,
25+
show_initially,
2426
...otherProps
2527
} = props;
2628

29+
const [showSpinner, setShowSpinner] = useState(show_initially);
30+
const timer = useRef();
31+
32+
useEffect(() => {
33+
if (loading_state) {
34+
if (timer.current) {
35+
clearTimeout(timer.current);
36+
}
37+
if (loading_state.is_loading && !showSpinner) {
38+
setShowSpinner(true);
39+
} else if (!loading_state.is_loading && showSpinner) {
40+
timer.current = setTimeout(() => setShowSpinner(false), debounce);
41+
}
42+
}
43+
}, [loading_state]);
44+
2745
const isBootstrapColor = bootstrapColors.has(color);
2846

2947
const fullscreenStyle = {
@@ -75,8 +93,6 @@ const Spinner = props => {
7593
...spinner_style
7694
};
7795

78-
const showSpinner = loading_state && loading_state.is_loading;
79-
8096
return (
8197
<div style={showSpinner ? hiddenStyle : {}}>
8298
{children}
@@ -105,6 +121,11 @@ const Spinner = props => {
105121

106122
Spinner._dashprivate_isLoadingComponent = true;
107123

124+
Spinner.defaultProps = {
125+
debounce: 0,
126+
show_initially: true
127+
};
128+
108129
Spinner.propTypes = {
109130
/**
110131
* The ID of this component, used to identify dash components
@@ -162,7 +183,19 @@ Spinner.propTypes = {
162183
* Boolean that determines if the loading spinner will be displayed
163184
* full-screen or not.
164185
*/
165-
fullscreen: PropTypes.bool
186+
fullscreen: PropTypes.bool,
187+
188+
/**
189+
* When using the spinner as a loading spinner, add a time delay (in ms) to
190+
* the spinner being removed to prevent flickering.
191+
*/
192+
debounce: PropTypes.number,
193+
194+
/**
195+
* Whether the Spinner should show on app start-up before the loading state
196+
* has been determined. Default True.
197+
*/
198+
show_initially: PropTypes.bool
166199
};
167200

168201
export default Spinner;

src/components/__tests__/Spinner.test.js

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import React from 'react';
2-
import {render} from '@testing-library/react';
2+
import {act, render} from '@testing-library/react';
33
import Spinner from '../Spinner';
44

5+
jest.useFakeTimers();
6+
57
describe('Spinner', () => {
68
test('renders a div with class "border-spinner"', () => {
79
const spinner = render(<Spinner />);
@@ -12,21 +14,39 @@ describe('Spinner', () => {
1214
});
1315

1416
test("renders its content if object isn't loading", () => {
15-
const {container: All, rerender} = render(
16-
<Spinner>Some spinner content</Spinner>
17+
const {container: container, rerender} = render(
18+
<Spinner loading_state={{is_loading: false}}>
19+
Some spinner content
20+
</Spinner>
1721
);
1822

19-
expect(All).toHaveTextContent('Some spinner content');
23+
// spinner is initially visible until we've had time to update based on
24+
// loading state. this can be disabled with show_initially={false}
25+
act(() => jest.advanceTimersByTime(10));
26+
27+
expect(container).toHaveTextContent('Some spinner content');
28+
expect(container.querySelector('div.spinner-border')).toBe(null);
2029

2130
rerender(
2231
<Spinner loading_state={{is_loading: true}}>Some spinner content</Spinner>
2332
);
2433

25-
const overAll = All.firstChild;
34+
const overAll = container.firstChild;
2635
const spinner = overAll.lastChild;
2736

28-
expect(overAll).toHaveTextContent('Some spinner content');
29-
expect(spinner.firstChild).toHaveClass('spinner-border');
37+
act(() => jest.advanceTimersByTime(10));
38+
39+
expect(container).toHaveTextContent('Some spinner content');
40+
expect(container.querySelector('div.spinner-border')).not.toBe(null);
41+
});
42+
43+
test("doesn't show initially when show_initially is false", () => {
44+
const {container: container} = render(
45+
<Spinner show_initially={false}>Some spinner content</Spinner>
46+
);
47+
48+
expect(container).toHaveTextContent('Some spinner content');
49+
expect(container.querySelector('div.spinner-border')).toBe(null);
3050
});
3151

3252
test('applies additional CSS classes when props are set', () => {
@@ -64,4 +84,31 @@ describe('Spinner', () => {
6484
expect(spinnerSuccess).toHaveClass('text-success');
6585
expect(spinnerDark).toHaveClass('text-dark');
6686
});
87+
88+
test('spinner can be debounced with debounce prop', () => {
89+
const {container: container, rerender} = render(
90+
<Spinner loading_state={{is_loading: true}} debounce={1000}>
91+
Some spinner content
92+
</Spinner>
93+
);
94+
95+
const overAll = container.firstChild;
96+
const spinner = overAll.lastChild;
97+
98+
expect(overAll).toHaveTextContent('Some spinner content');
99+
expect(spinner.firstChild).toHaveClass('spinner-border');
100+
101+
rerender(
102+
<Spinner loading_state={{is_loading: false}} debounce={1000}>
103+
Some spinner content
104+
</Spinner>
105+
);
106+
107+
expect(overAll).toHaveTextContent('Some spinner content');
108+
expect(spinner.firstChild).toHaveClass('spinner-border');
109+
110+
act(() => jest.advanceTimersByTime(1000));
111+
112+
expect(container.querySelector('div.spinner-border')).toBe(null);
113+
});
67114
});

0 commit comments

Comments
 (0)