Skip to content

Commit 2783418

Browse files
mogzolzombieJ
authored andcommitted
Fix motion popups sometimes getting stuck (#157) (#158)
* Fix motion popups sometimes getting stuck (#157) If the popup transitioned from visible to invisible before the status reached 'motion', the popup would get stuck in a visible state. This changes the code to immediately set status to 'stable' during a transition from visible to invisible if the status is something before 'motion'. Also fixes an issue where an animation frame could change the status to something invalid after a transition from visible to invisible. * Add tests * Remove if statement around raf.cancel
1 parent 2cd87ca commit 2783418

File tree

2 files changed

+96
-1
lines changed

2 files changed

+96
-1
lines changed

src/Popup.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,14 @@ class Popup extends Component<PopupProps, PopupState> {
118118
// Init render should always be stable
119119
newState.status = 'stable';
120120
} else if (visible !== prevVisible) {
121-
newState.status = visible || supportMotion(mergedMotion) ? null : 'stable';
121+
if (
122+
visible ||
123+
(supportMotion(mergedMotion) && ['motion', 'AfterMotion', 'stable'].includes(status))
124+
) {
125+
newState.status = null;
126+
} else {
127+
newState.status = 'stable';
128+
}
122129

123130
if (visible) {
124131
newState.alignClassName = null;
@@ -136,6 +143,9 @@ class Popup extends Component<PopupProps, PopupState> {
136143
const { status } = this.state;
137144
const { getRootDomNode, visible, stretch } = this.props;
138145

146+
// If there is a pending state update, cancel it, a new one will be set if necessary
147+
this.cancelFrameState();
148+
139149
if (visible && status !== 'stable') {
140150
switch (status) {
141151
case null: {

tests/popup.test.jsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from 'react';
2+
import { mount } from 'enzyme';
3+
import raf from 'raf';
4+
import Popup from '../src/Popup';
5+
6+
jest.mock('raf', () => {
7+
const rafMock = jest.fn(() => 1);
8+
rafMock.cancel = jest.fn();
9+
return rafMock;
10+
});
11+
12+
describe('Popup', () => {
13+
afterEach(() => {
14+
raf.mockClear();
15+
raf.cancel.mockClear();
16+
});
17+
18+
describe('Popup getDerivedStateFromProps status behavior', () => {
19+
it('returns stable on init', () => {
20+
const props = { visible: false };
21+
const state = { prevVisible: null, status: 'something' };
22+
23+
expect(Popup.getDerivedStateFromProps(props, state).status).toBe('stable');
24+
});
25+
26+
it('does not change when visible is unchanged', () => {
27+
const props = { visible: true };
28+
const state = { prevVisible: true, status: 'something' };
29+
30+
expect(Popup.getDerivedStateFromProps(props, state).status).toBe('something');
31+
});
32+
33+
it('returns null when visible is changed to true', () => {
34+
const props = { visible: true };
35+
const state = { prevVisible: false, status: 'something' };
36+
37+
expect(Popup.getDerivedStateFromProps(props, state).status).toBe(null);
38+
});
39+
40+
it('returns stable when visible is changed to false and motion is not supported', () => {
41+
const props = { visible: false };
42+
const state = { prevVisible: true, status: 'something' };
43+
44+
expect(Popup.getDerivedStateFromProps(props, state).status).toBe('stable');
45+
});
46+
47+
it('returns null when visible is changed to false and motion is started', () => {
48+
const props = {
49+
visible: false,
50+
motion: {
51+
motionName: 'enter',
52+
},
53+
};
54+
const state = { prevVisible: true, status: 'motion' };
55+
56+
expect(Popup.getDerivedStateFromProps(props, state).status).toBe(null);
57+
});
58+
59+
it('returns stable when visible is changed to false and motion is not started', () => {
60+
const props = {
61+
visible: false,
62+
motion: {
63+
motionName: 'enter',
64+
},
65+
};
66+
const state = { prevVisible: true, status: 'beforeMotion' };
67+
68+
expect(Popup.getDerivedStateFromProps(props, state).status).toBe('stable');
69+
});
70+
});
71+
72+
it('Popup cancels pending animation frames on update', () => {
73+
const wrapper = mount(
74+
<Popup visible motion={{}}>
75+
<div>popup content</div>
76+
</Popup>,
77+
);
78+
79+
expect(raf).toHaveBeenCalledTimes(1);
80+
81+
raf.cancel.mockClear();
82+
wrapper.setProps({ visible: false });
83+
expect(raf.cancel).toHaveBeenCalledTimes(1);
84+
});
85+
});

0 commit comments

Comments
 (0)