Skip to content

Commit 20b532d

Browse files
kyletsangtaion
andauthored
fix(Toast): Reset autohide timer only if show or autohide changed (react-bootstrap#5220)
* fix(Toast): Reset autohide timer only if show or autohide changed * Update src/Toast.js Co-authored-by: Jimmy Jia <[email protected]> * Pre-calc autohide toast condition * Update Toast.js * format Co-authored-by: Jimmy Jia <[email protected]>
1 parent 501b3bf commit 20b532d

File tree

2 files changed

+147
-62
lines changed

2 files changed

+147
-62
lines changed

src/Toast.js

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useMemo, useCallback } from 'react';
1+
import React, { useEffect, useMemo, useRef, useCallback } from 'react';
22
import PropTypes from 'prop-types';
33
import classNames from 'classnames';
44

@@ -71,30 +71,39 @@ const Toast = React.forwardRef(
7171
ref,
7272
) => {
7373
bsPrefix = useBootstrapPrefix(bsPrefix, 'toast');
74+
75+
// We use refs for these, because we don't want to restart the autohide
76+
// timer in case these values change.
7477
const delayRef = useRef(delay);
7578
const onCloseRef = useRef(onClose);
7679

7780
useEffect(() => {
78-
// We use refs for these, because we don't want to restart the autohide
79-
// timer in case these values change.
8081
delayRef.current = delay;
8182
onCloseRef.current = onClose;
8283
}, [delay, onClose]);
8384

8485
const autohideTimeout = useTimeout();
86+
const autohideToast = !!(autohide && show);
87+
8588
const autohideFunc = useCallback(() => {
86-
if (!(autohide && show)) {
87-
return;
89+
if (autohideToast) {
90+
onCloseRef.current();
8891
}
89-
onCloseRef.current();
90-
}, [autohide, show]);
92+
}, [autohideToast]);
9193

92-
autohideTimeout.set(autohideFunc, delayRef.current);
94+
useEffect(() => {
95+
// Only reset timer if show or autohide changes.
96+
autohideTimeout.set(autohideFunc, delayRef.current);
97+
}, [autohideTimeout, autohideFunc]);
98+
99+
const toastContext = useMemo(
100+
() => ({
101+
onClose,
102+
}),
103+
[onClose],
104+
);
93105

94-
const hasAnimation = useMemo(() => Transition && animation, [
95-
Transition,
96-
animation,
97-
]);
106+
const hasAnimation = !!(Transition && animation);
98107

99108
const toast = (
100109
<div
@@ -113,10 +122,6 @@ const Toast = React.forwardRef(
113122
</div>
114123
);
115124

116-
const toastContext = {
117-
onClose,
118-
};
119-
120125
return (
121126
<ToastContext.Provider value={toastContext}>
122127
{hasAnimation ? (

test/ToastSpec.js

Lines changed: 126 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import { mount } from 'enzyme';
44
import Toast from '../src/Toast';
55

66
describe('<Toast>', () => {
7+
let clock;
8+
9+
beforeEach(() => {
10+
clock = sinon.useFakeTimers();
11+
});
12+
13+
afterEach(() => {
14+
clock.restore();
15+
});
16+
717
it('should render an entire toast', () => {
818
mount(
919
<Toast>
@@ -44,58 +54,128 @@ describe('<Toast>', () => {
4454
});
4555

4656
it('should trigger the onClose event after the autohide delay', () => {
47-
const clock = sinon.useFakeTimers();
48-
49-
try {
50-
const onCloseSpy = sinon.spy();
51-
mount(
52-
<Toast onClose={onCloseSpy} delay={500} show autohide>
53-
<Toast.Header>header-content</Toast.Header>
54-
<Toast.Body>body-content</Toast.Body>
55-
</Toast>,
56-
);
57-
clock.tick(1000);
58-
expect(onCloseSpy).to.have.been.calledOnce;
59-
} finally {
60-
clock.restore();
61-
}
57+
const onCloseSpy = sinon.spy();
58+
mount(
59+
<Toast onClose={onCloseSpy} delay={500} show autohide>
60+
<Toast.Header>header-content</Toast.Header>
61+
<Toast.Body>body-content</Toast.Body>
62+
</Toast>,
63+
);
64+
clock.tick(1000);
65+
expect(onCloseSpy).to.have.been.calledOnce;
6266
});
6367

6468
it('should not trigger the onClose event if autohide is not set', () => {
65-
const clock = sinon.useFakeTimers();
66-
67-
try {
68-
const onCloseSpy = sinon.spy();
69-
mount(
70-
<Toast onClose={onCloseSpy}>
71-
<Toast.Header>header-content</Toast.Header>
72-
<Toast.Body>body-content</Toast.Body>
73-
</Toast>,
74-
);
75-
clock.tick(3000);
76-
expect(onCloseSpy).not.to.have.been.called;
77-
} finally {
78-
clock.restore();
79-
}
69+
const onCloseSpy = sinon.spy();
70+
mount(
71+
<Toast onClose={onCloseSpy}>
72+
<Toast.Header>header-content</Toast.Header>
73+
<Toast.Body>body-content</Toast.Body>
74+
</Toast>,
75+
);
76+
clock.tick(3000);
77+
expect(onCloseSpy).not.to.have.been.called;
8078
});
8179

8280
it('should clearTimeout after unmount', () => {
83-
const clock = sinon.useFakeTimers();
84-
85-
try {
86-
const onCloseSpy = sinon.spy();
87-
const wrapper = mount(
88-
<Toast delay={500} onClose={onCloseSpy} show autohide>
89-
<Toast.Header>header-content</Toast.Header>
90-
<Toast.Body>body-content</Toast.Body>
91-
</Toast>,
92-
);
93-
wrapper.unmount();
94-
clock.tick(1000);
95-
expect(onCloseSpy).not.to.have.been.called;
96-
} finally {
97-
clock.restore();
98-
}
81+
const onCloseSpy = sinon.spy();
82+
const wrapper = mount(
83+
<Toast delay={500} onClose={onCloseSpy} show autohide>
84+
<Toast.Header>header-content</Toast.Header>
85+
<Toast.Body>body-content</Toast.Body>
86+
</Toast>,
87+
);
88+
wrapper.unmount();
89+
clock.tick(1000);
90+
expect(onCloseSpy).not.to.have.been.called;
91+
});
92+
93+
it('should not reset autohide timer when element re-renders with same props', () => {
94+
const onCloseSpy = sinon.spy();
95+
const wrapper = mount(
96+
<Toast delay={500} onClose={onCloseSpy} show autohide>
97+
<Toast.Header>header-content</Toast.Header>
98+
<Toast.Body>body-content</Toast.Body>
99+
</Toast>,
100+
);
101+
102+
clock.tick(250);
103+
104+
// Trigger render with no props changes.
105+
wrapper.setProps({});
106+
107+
clock.tick(300);
108+
expect(onCloseSpy).to.have.been.calledOnce;
109+
});
110+
111+
it('should not reset autohide timer when delay is changed', () => {
112+
const onCloseSpy = sinon.spy();
113+
const wrapper = mount(
114+
<Toast delay={500} onClose={onCloseSpy} show autohide>
115+
<Toast.Header>header-content</Toast.Header>
116+
<Toast.Body>body-content</Toast.Body>
117+
</Toast>,
118+
);
119+
120+
clock.tick(250);
121+
122+
wrapper.setProps({ delay: 10000 });
123+
124+
clock.tick(300);
125+
expect(onCloseSpy).to.have.been.calledOnce;
126+
});
127+
128+
it('should not reset autohide timer when onClosed is changed', () => {
129+
const onCloseSpy = sinon.spy();
130+
const onCloseSpy2 = sinon.spy();
131+
const wrapper = mount(
132+
<Toast delay={500} onClose={onCloseSpy} show autohide>
133+
<Toast.Header>header-content</Toast.Header>
134+
<Toast.Body>body-content</Toast.Body>
135+
</Toast>,
136+
);
137+
138+
clock.tick(250);
139+
140+
wrapper.setProps({ onClose: onCloseSpy2 });
141+
142+
clock.tick(300);
143+
expect(onCloseSpy).not.to.have.been.called;
144+
expect(onCloseSpy2).to.have.been.calledOnce;
145+
});
146+
147+
it('should not call onClose if autohide is changed from true to false', () => {
148+
const onCloseSpy = sinon.spy();
149+
const wrapper = mount(
150+
<Toast delay={500} onClose={onCloseSpy} show autohide>
151+
<Toast.Header>header-content</Toast.Header>
152+
<Toast.Body>body-content</Toast.Body>
153+
</Toast>,
154+
);
155+
156+
clock.tick(250);
157+
158+
wrapper.setProps({ autohide: false });
159+
160+
clock.tick(300);
161+
expect(onCloseSpy).not.to.have.been.called;
162+
});
163+
164+
it('should not call onClose if show is changed from true to false', () => {
165+
const onCloseSpy = sinon.spy();
166+
const wrapper = mount(
167+
<Toast delay={500} onClose={onCloseSpy} show autohide>
168+
<Toast.Header>header-content</Toast.Header>
169+
<Toast.Body>body-content</Toast.Body>
170+
</Toast>,
171+
);
172+
173+
clock.tick(250);
174+
175+
wrapper.setProps({ show: false });
176+
177+
clock.tick(300);
178+
expect(onCloseSpy).not.to.have.been.called;
99179
});
100180

101181
it('should render with bsPrefix', () => {

0 commit comments

Comments
 (0)