Skip to content

Commit 62abaff

Browse files
vonovakaleksanbstianjensen
authored
fix: dismiss android datepicker dialog upon unmount (#337)
* Dismiss android pickers on react component unmount This commit makes the behavior of the android datetimepickers more in line with other controlled components in react, where unmounting the component actually dismisses the ui widget as well. Co-authored-by: Stian Jensen <[email protected]> * remove duplication, rename close() to dismiss() * cover android dialog dismiss by e2e test * fix lint * fix e2e test Co-authored-by: Aleksander Vognild Burkow <[email protected]> Co-authored-by: Stian Jensen <[email protected]>
1 parent 5d32a63 commit 62abaff

File tree

10 files changed

+106
-23
lines changed

10 files changed

+106
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.reactcommunity.rndatetimepicker;
2+
3+
import androidx.fragment.app.DialogFragment;
4+
import androidx.fragment.app.FragmentActivity;
5+
import androidx.fragment.app.FragmentManager;
6+
7+
import com.facebook.react.bridge.Promise;
8+
9+
public class Common {
10+
11+
public static void dismissDialog(FragmentActivity activity, String fragmentTag, Promise promise) {
12+
if (activity == null) {
13+
promise.reject(
14+
RNConstants.ERROR_NO_ACTIVITY,
15+
"Tried to close a " + fragmentTag + " dialog while not attached to an Activity");
16+
return;
17+
}
18+
19+
FragmentManager fragmentManager = activity.getSupportFragmentManager();
20+
final DialogFragment oldFragment = (DialogFragment) fragmentManager.findFragmentByTag(fragmentTag);
21+
22+
boolean fragmentFound = oldFragment != null;
23+
if (fragmentFound) {
24+
oldFragment.dismiss();
25+
}
26+
27+
promise.resolve(fragmentFound);
28+
}
29+
}

android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogModule.java

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import com.facebook.react.common.annotations.VisibleForTesting;
2323
import com.facebook.react.module.annotations.ReactModule;
2424

25+
import static com.reactcommunity.rndatetimepicker.Common.dismissDialog;
26+
2527
/**
2628
* {@link NativeModule} that allows JS to show a native date picker dialog and get called back when
2729
* the user selects a date.
@@ -84,6 +86,11 @@ public void onClick(DialogInterface dialog, int which) {
8486
}
8587
}
8688

89+
@ReactMethod
90+
public void dismiss(Promise promise) {
91+
FragmentActivity activity = (FragmentActivity) getCurrentActivity();
92+
dismissDialog(activity, FRAGMENT_TAG, promise);
93+
}
8794
/**
8895
* Show a date picker dialog.
8996
*

android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogModule.java

+8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import androidx.fragment.app.FragmentActivity;
2424
import androidx.fragment.app.FragmentManager;
2525

26+
import static com.reactcommunity.rndatetimepicker.Common.dismissDialog;
27+
2628
/**
2729
* {@link NativeModule} that allows JS to show a native time picker dialog and get called back when
2830
* the user selects a time.
@@ -83,6 +85,12 @@ public void onClick(DialogInterface dialog, int which) {
8385
}
8486
}
8587

88+
@ReactMethod
89+
public void dismiss(Promise promise) {
90+
FragmentActivity activity = (FragmentActivity) getCurrentActivity();
91+
dismissDialog(activity, FRAGMENT_TAG, promise);
92+
}
93+
8694
@ReactMethod
8795
public void open(@Nullable final ReadableMap options, Promise promise) {
8896

example/App.js

+19
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,25 @@ export const App = () => {
180180
testID="neutralButtonLabelTextInput"
181181
/>
182182
</View>
183+
184+
<View style={styles.header}>
185+
<ThemedText style={{margin: 10, flex: 1}}>
186+
[android] show and dismiss picker after 3 secs
187+
</ThemedText>
188+
</View>
189+
<View style={styles.button}>
190+
<Button
191+
testID="showAndDismissPickerButton"
192+
onPress={() => {
193+
setShow(true);
194+
setTimeout(() => {
195+
setShow(false);
196+
}, 3000);
197+
}}
198+
title="Show and dismiss picker!"
199+
/>
200+
</View>
201+
183202
<View style={styles.button}>
184203
<Button
185204
testID="showPickerButton"

example/e2e/detoxTest.spec.js

+19-23
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ const {
33
getDateText,
44
elementById,
55
elementByText,
6+
getDateTimePickerIOS,
7+
getDatePickerAndroid,
68
} = require('./utils/matchers');
79
const {
810
userChangesMinuteValue,
911
userOpensPicker,
1012
userTapsCancelButtonAndroid,
1113
userTapsOkButtonAndroid,
1214
} = require('./utils/actions');
13-
const {isAndroid, isIOS} = require('./utils/utils');
15+
const {isAndroid, isIOS, wait} = require('./utils/utils');
1416

1517
describe('Example', () => {
1618
beforeEach(async () => {
@@ -35,21 +37,17 @@ describe('Example', () => {
3537
await userOpensPicker({mode: 'date', display: 'default'});
3638

3739
if (isIOS()) {
38-
await expect(
39-
element(by.type('UIPickerView').withAncestor(by.id('dateTimePicker'))),
40-
).toBeVisible();
40+
await expect(getDateTimePickerIOS()).toBeVisible();
4141
} else {
42-
await expect(element(by.type('android.widget.DatePicker'))).toBeVisible();
42+
await expect(getDatePickerAndroid()).toBeVisible();
4343
}
4444
});
4545

4646
it('nothing should happen if date does not change', async () => {
4747
await userOpensPicker({mode: 'date', display: 'default'});
4848

4949
if (isIOS()) {
50-
await expect(
51-
element(by.type('UIPickerView').withAncestor(by.id('dateTimePicker'))),
52-
).toBeVisible();
50+
await expect(getDateTimePickerIOS()).toBeVisible();
5351
} else {
5452
const testElement = element(
5553
by
@@ -70,9 +68,7 @@ describe('Example', () => {
7068
const dateText = getDateText();
7169

7270
if (isIOS()) {
73-
const testElement = element(
74-
by.type('UIPickerView').withAncestor(by.id('dateTimePicker')),
75-
);
71+
const testElement = getDateTimePickerIOS();
7672
await testElement.setColumnToValue(0, 'November');
7773
await testElement.setColumnToValue(1, '3');
7874
await testElement.setColumnToValue(2, '1800');
@@ -96,9 +92,7 @@ describe('Example', () => {
9692
await userOpensPicker({mode: 'time', display: 'default'});
9793

9894
if (isIOS()) {
99-
await expect(
100-
element(by.type('UIPickerView').withAncestor(by.id('dateTimePicker'))),
101-
).toBeVisible();
95+
await expect(getDateTimePickerIOS()).toBeVisible();
10296
} else {
10397
await expect(element(by.type('android.widget.TimePicker'))).toBeVisible();
10498
}
@@ -108,9 +102,7 @@ describe('Example', () => {
108102
await userOpensPicker({mode: 'time', display: 'default'});
109103

110104
if (isIOS()) {
111-
await expect(
112-
element(by.type('UIPickerView').withAncestor(by.id('dateTimePicker'))),
113-
).toBeVisible();
105+
await expect(getDateTimePickerIOS()).toBeVisible();
114106
} else {
115107
await userChangesMinuteValue();
116108
await userTapsCancelButtonAndroid();
@@ -124,9 +116,7 @@ describe('Example', () => {
124116
const timeText = getTimeText();
125117

126118
if (isIOS()) {
127-
const testElement = element(
128-
by.type('UIPickerView').withAncestor(by.id('dateTimePicker')),
129-
);
119+
const testElement = getDateTimePickerIOS();
130120
await testElement.setColumnToValue(0, '2');
131121
await testElement.setColumnToValue(1, '44');
132122
await testElement.setColumnToValue(2, 'PM');
@@ -149,6 +139,14 @@ describe('Example', () => {
149139
await expect(dateText).toHaveText('01/01/1970');
150140
});
151141

142+
it(':android: when component unmounts, dialog is dismissed', async () => {
143+
await elementById('showAndDismissPickerButton').tap();
144+
await expect(getDatePickerAndroid()).toBeVisible();
145+
await wait(3500);
146+
147+
await expect(getDatePickerAndroid()).toNotExist();
148+
});
149+
152150
describe('given 5-minute interval', () => {
153151
it(':android: clock picker should correct 18-minute selection to 20-minute one', async () => {
154152
try {
@@ -195,9 +193,7 @@ describe('Example', () => {
195193
it(':ios: picker should offer only options divisible by 5 (0, 5, 10,...)', async () => {
196194
await userOpensPicker({mode: 'time', display: 'spinner', interval: 5});
197195

198-
const testElement = element(
199-
by.type('UIPickerView').withAncestor(by.id('dateTimePicker')),
200-
);
196+
const testElement = getDateTimePickerIOS();
201197
await testElement.setColumnToValue(0, '2');
202198
await testElement.setColumnToValue(2, 'PM');
203199
const timeText = getTimeText();

example/e2e/utils/matchers.js

+6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ const getTimeText = () => element(by.id('timeText'));
22
const getDateText = () => element(by.id('dateText'));
33
const elementById = (id) => element(by.id(id));
44
const elementByText = (text) => element(by.text(text));
5+
const getDateTimePickerIOS = () =>
6+
element(by.type('UIPickerView').withAncestor(by.id('dateTimePicker')));
7+
const getDatePickerAndroid = () =>
8+
element(by.type('android.widget.DatePicker'));
59

610
module.exports = {
711
getTimeText,
812
getDateText,
913
elementById,
1014
elementByText,
15+
getDateTimePickerIOS,
16+
getDatePickerAndroid,
1117
};

example/e2e/utils/utils.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
const isAndroid = () => device.getPlatform() === 'android';
22
const isIOS = () => device.getPlatform() === 'ios';
3+
const wait = async (time = 1000) =>
4+
new Promise((resolve) => setTimeout(resolve, time));
35

46
module.exports = {
57
isAndroid,
68
isIOS,
9+
wait,
710
};

src/datepicker.android.js

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export default class DatePickerAndroid {
4242
return NativeModules.RNDatePickerAndroid.open(options);
4343
}
4444

45+
static async dismiss(): Promise<boolean> {
46+
return NativeModules.RNDatePickerAndroid.dismiss();
47+
}
48+
4549
/**
4650
* A date has been selected.
4751
*/

src/datetimepicker.android.js

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from './constants';
1616
import pickers from './picker';
1717
import invariant from 'invariant';
18+
import {useEffect} from 'react';
1819

1920
import type {AndroidEvent, AndroidNativeProps} from './types';
2021

@@ -66,6 +67,12 @@ export default function RNDateTimePicker(props: AndroidNativeProps) {
6667
break;
6768
}
6869

70+
useEffect(() => {
71+
// This effect runs on unmount, and will ensure the picker is closed.
72+
// This allows for controlling the opening state of the picker through declarative logic in jsx.
73+
return () => (pickers[mode] ?? pickers[MODE_DATE]).dismiss();
74+
}, [mode]);
75+
6976
picker.then(
7077
function resolve({action, day, month, year, minute, hour}) {
7178
const date = new Date(value);

src/timepicker.android.js

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export default class TimePickerAndroid {
4040
return NativeModules.RNTimePickerAndroid.open(options);
4141
}
4242

43+
static async dismiss(): Promise<boolean> {
44+
return NativeModules.RNDatePickerAndroid.dismiss();
45+
}
46+
4347
/**
4448
* A time has been selected.
4549
*/

0 commit comments

Comments
 (0)