Skip to content

Commit f81eebc

Browse files
committed
fix: fix Modal flickering & cleanup
1 parent 0107899 commit f81eebc

File tree

2 files changed

+74
-91
lines changed

2 files changed

+74
-91
lines changed

src/components/Modal.tsx

Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -112,27 +112,13 @@ function Modal({
112112
testID = 'modal',
113113
}: Props) {
114114
const theme = useInternalTheme(themeOverrides);
115-
const visibleRef = React.useRef(visible);
116-
117-
React.useEffect(() => {
118-
visibleRef.current = visible;
119-
});
120-
121115
const onDismissCallback = useLatestCallback(onDismiss);
122-
123116
const { scale } = theme.animation;
124-
125117
const { top, bottom } = useSafeAreaInsets();
126-
127118
const opacity = useAnimatedValue(visible ? 1 : 0);
119+
const [visibleInternal, setVisibleInternal] = React.useState(visible);
128120

129-
const [rendered, setRendered] = React.useState(visible);
130-
131-
if (visible && !rendered) {
132-
setRendered(true);
133-
}
134-
135-
const showModal = React.useCallback(() => {
121+
const showModalAnimation = React.useCallback(() => {
136122
Animated.timing(opacity, {
137123
toValue: 1,
138124
duration: scale * DEFAULT_DURATION,
@@ -141,7 +127,7 @@ function Modal({
141127
}).start();
142128
}, [opacity, scale]);
143129

144-
const hideModal = React.useCallback(() => {
130+
const hideModalAnimation = React.useCallback(() => {
145131
Animated.timing(opacity, {
146132
toValue: 0,
147133
duration: scale * DEFAULT_DURATION,
@@ -152,17 +138,24 @@ function Modal({
152138
return;
153139
}
154140

155-
if (visible) {
156-
onDismissCallback();
157-
}
158-
159-
if (visibleRef.current) {
160-
showModal();
161-
} else {
162-
setRendered(false);
163-
}
141+
setVisibleInternal(false);
164142
});
165-
}, [onDismissCallback, opacity, scale, showModal, visible]);
143+
}, [opacity, scale]);
144+
145+
React.useEffect(() => {
146+
if (visibleInternal === visible) {
147+
return;
148+
}
149+
150+
if (!visibleInternal && visible) {
151+
setVisibleInternal(true);
152+
return showModalAnimation();
153+
}
154+
155+
if (visibleInternal && !visible) {
156+
return hideModalAnimation();
157+
}
158+
}, [visible, showModalAnimation, hideModalAnimation, visibleInternal]);
166159

167160
React.useEffect(() => {
168161
if (!visible) {
@@ -171,7 +164,7 @@ function Modal({
171164

172165
const onHardwareBackPress = () => {
173166
if (dismissable || dismissableBackButton) {
174-
hideModal();
167+
onDismissCallback();
175168
}
176169

177170
return true;
@@ -183,37 +176,26 @@ function Modal({
183176
onHardwareBackPress
184177
);
185178
return () => subscription.remove();
186-
}, [dismissable, dismissableBackButton, hideModal, visible]);
187-
188-
const prevVisible = React.useRef<boolean | null>(null);
179+
}, [dismissable, dismissableBackButton, onDismissCallback, visible]);
189180

190-
React.useEffect(() => {
191-
if (prevVisible.current !== visible) {
192-
if (visible) {
193-
showModal();
194-
} else {
195-
hideModal();
196-
}
197-
}
198-
prevVisible.current = visible;
199-
});
200-
201-
if (!rendered) return null;
181+
if (!visibleInternal) {
182+
return null;
183+
}
202184

203185
return (
204186
<Animated.View
205187
pointerEvents={visible ? 'auto' : 'none'}
206188
accessibilityViewIsModal
207189
accessibilityLiveRegion="polite"
208190
style={StyleSheet.absoluteFill}
209-
onAccessibilityEscape={hideModal}
191+
onAccessibilityEscape={onDismissCallback}
210192
testID={testID}
211193
>
212194
<AnimatedPressable
213195
accessibilityLabel={overlayAccessibilityLabel}
214196
accessibilityRole="button"
215197
disabled={!dismissable}
216-
onPress={dismissable ? hideModal : undefined}
198+
onPress={dismissable ? onDismissCallback : undefined}
217199
importantForAccessibility="no"
218200
style={[
219201
styles.backdrop,

src/components/__tests__/Modal.test.tsx

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,23 @@ describe('Modal', () => {
9898
});
9999
});
100100
describe('when open', () => {
101-
describe('if closed via touching backdrop', () => {
102-
it('will run the animation but not fade out', async () => {
101+
describe('if backdrop touched', () => {
102+
it('should invoke the onDismiss function immediately', () => {
103+
const onDismiss = jest.fn();
103104
const { getByTestId } = render(
104-
<Modal testID="modal" visible onDismiss={() => {}}>
105+
<Modal testID="modal" visible onDismiss={onDismiss}>
105106
{null}
106107
</Modal>
107108
);
108109

109-
expect(getByTestId('modal-surface-outer-layer')).toHaveStyle({
110-
opacity: 1,
111-
});
110+
expect(onDismiss).not.toHaveBeenCalled();
112111

113112
act(() => {
114113
fireEvent.press(getByTestId('modal-backdrop'));
115114
});
116115

116+
expect(onDismiss).toHaveBeenCalled();
117+
117118
expect(getByTestId('modal-surface-outer-layer')).toHaveStyle({
118119
opacity: 1,
119120
});
@@ -129,36 +130,58 @@ describe('Modal', () => {
129130
expect(getByTestId('modal-surface-outer-layer')).toHaveStyle({
130131
opacity: 1,
131132
});
133+
134+
expect(onDismiss).toHaveBeenCalledTimes(1);
132135
});
136+
});
133137

134-
it('should invoke the onDismiss function after the animation', () => {
135-
const onDismiss = jest.fn();
136-
const { getByTestId } = render(
137-
<Modal testID="modal" visible onDismiss={onDismiss}>
138-
{null}
139-
</Modal>
140-
);
138+
it('runs the closing animation if visible toggled', () => {
139+
const { getByTestId, queryByTestId, rerender } = render(
140+
<Modal testID="modal" visible onDismiss={() => {}}>
141+
{null}
142+
</Modal>
143+
);
141144

142-
expect(onDismiss).not.toHaveBeenCalled();
145+
expect(getByTestId('modal-surface-outer-layer')).toHaveStyle({
146+
opacity: 1,
147+
});
143148

144-
act(() => {
145-
fireEvent.press(getByTestId('modal-backdrop'));
146-
});
149+
act(() => {
150+
fireEvent.press(getByTestId('modal-backdrop'));
151+
});
147152

148-
expect(onDismiss).not.toHaveBeenCalled();
153+
rerender(
154+
<Modal testID="modal" visible={false} onDismiss={() => {}}>
155+
{null}
156+
</Modal>
157+
);
149158

150-
act(() => {
151-
jest.runAllTimers();
152-
});
159+
expect(getByTestId('modal-surface-outer-layer')).toHaveStyle({
160+
opacity: 1,
161+
});
153162

154-
expect(onDismiss).toHaveBeenCalledTimes(1);
163+
expect(getByTestId('modal-backdrop')).toHaveStyle({
164+
opacity: 1,
165+
});
166+
167+
expect(getByTestId('modal-surface-outer-layer')).toHaveStyle({
168+
opacity: 1,
155169
});
170+
171+
act(() => {
172+
jest.runAllTimers();
173+
});
174+
175+
expect(queryByTestId('modal-surface-outer-layer')).not.toBeOnTheScreen();
176+
177+
expect(queryByTestId('modal-backdrop')).not.toBeOnTheScreen();
156178
});
157179

158180
describe('if closed via Android back button', () => {
159-
it('will run the animation but not fade out', async () => {
181+
it('invokes onDismiss', async () => {
182+
const onDismiss = jest.fn();
160183
const { getByTestId } = render(
161-
<Modal testID="modal" visible onDismiss={() => {}}>
184+
<Modal testID="modal" visible onDismiss={onDismiss}>
162185
{null}
163186
</Modal>
164187
);
@@ -186,28 +209,6 @@ describe('Modal', () => {
186209
expect(getByTestId('modal-surface-outer-layer')).toHaveStyle({
187210
opacity: 1,
188211
});
189-
});
190-
191-
it('should invoke the onDismiss function after the animation', () => {
192-
const onDismiss = jest.fn();
193-
194-
render(
195-
<Modal testID="modal" visible onDismiss={onDismiss}>
196-
{null}
197-
</Modal>
198-
);
199-
200-
expect(onDismiss).not.toHaveBeenCalled();
201-
202-
act(() => {
203-
BackHandler.mockPressBack();
204-
});
205-
206-
expect(onDismiss).not.toHaveBeenCalled();
207-
208-
act(() => {
209-
jest.runAllTimers();
210-
});
211212

212213
expect(onDismiss).toHaveBeenCalledTimes(1);
213214
});

0 commit comments

Comments
 (0)