Skip to content

[Android] Fix issue with minimumDate/maximumDate when using timeZoneOffsetInMinutes #519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 19, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

@SuppressLint("ValidFragment")
public class RNDatePickerDialogFragment extends DialogFragment {
Expand Down Expand Up @@ -108,6 +109,11 @@ static DatePickerDialog createDialog(

final DatePicker datePicker = dialog.getDatePicker();

Integer timeZoneOffsetInMilliseconds = getTimeZoneOffset(args);
if (timeZoneOffsetInMilliseconds != null) {
c.setTimeZone(TimeZone.getTimeZone("GMT"));
}

if (args != null && args.containsKey(RNConstants.ARG_MINDATE)) {
// Set minDate to the beginning of the day. We need this because of clowniness in datepicker
// that causes it to throw an exception if minDate is greater than the internal timestamp
Expand All @@ -117,7 +123,7 @@ static DatePickerDialog createDialog(
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
datePicker.setMinDate(c.getTimeInMillis());
datePicker.setMinDate(c.getTimeInMillis() - getOffset(c, timeZoneOffsetInMilliseconds));
} else {
// This is to work around a bug in DatePickerDialog where it doesn't display a title showing
// the date under certain conditions.
Expand All @@ -130,12 +136,29 @@ static DatePickerDialog createDialog(
c.set(Calendar.MINUTE, 59);
c.set(Calendar.SECOND, 59);
c.set(Calendar.MILLISECOND, 999);
datePicker.setMaxDate(c.getTimeInMillis());
datePicker.setMaxDate(c.getTimeInMillis() - getOffset(c, timeZoneOffsetInMilliseconds));
}

return dialog;
}

private static Integer getTimeZoneOffset(Bundle args) {
if (args != null && args.containsKey(RNConstants.ARG_TZOFFSET_MINS)) {
long timeZoneOffsetInMinutesFallback = args.getLong(RNConstants.ARG_TZOFFSET_MINS);
int timeZoneOffsetInMinutes = args.getInt(RNConstants.ARG_TZOFFSET_MINS, (int) timeZoneOffsetInMinutesFallback);
return timeZoneOffsetInMinutes * 60000;
}

return null;
}

private static int getOffset(Calendar c, Integer timeZoneOffsetInMilliseconds) {
if (timeZoneOffsetInMilliseconds != null) {
return TimeZone.getDefault().getOffset(c.getTimeInMillis()) - timeZoneOffsetInMilliseconds;
}
return 0;
}

@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
Expand Down
28 changes: 27 additions & 1 deletion example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ const MINUTE_INTERVALS = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30];

export const App = () => {
// Sat, 13 Nov 2021 10:00:00 GMT (local: Saturday, November 13, 2021 11:00:00 AM GMT+01:00)
const sourceDate = moment.unix(1636797600).local().toDate();
const sourceMoment = moment.unix(1636797600);
const sourceDate = sourceMoment.local().toDate();
const [date, setDate] = useState(sourceDate);
const [tzOffsetInMinutes, setTzOffsetInMinutes] = useState(undefined);
const [mode, setMode] = useState(MODE_VALUES[0]);
Expand All @@ -72,6 +73,8 @@ export const App = () => {
const [interval, setMinInterval] = useState(1);
const [neutralButtonLabel, setNeutralButtonLabel] = useState(undefined);
const [disabled, setDisabled] = useState(false);
const [minimumDate, setMinimumDate] = useState();
const [maximumDate, setMaximumDate] = useState();

// Windows-specific
const [time, setTime] = useState(undefined);
Expand Down Expand Up @@ -111,6 +114,17 @@ export const App = () => {
backgroundColor: isDarkMode ? Colors.dark : Colors.lighter,
};

const toggleMinMaxDate = () => {
const startOfTodayUTC = sourceMoment.utc().startOf('day').toDate();
setMinimumDate(maximumDate ? undefined : startOfTodayUTC);
const endOfTomorrowUTC = sourceMoment
.utc()
.endOf('day')
.add(1, 'day')
.toDate();
setMaximumDate(minimumDate ? undefined : endOfTomorrowUTC);
};

if (Platform.OS !== 'windows') {
return (
<SafeAreaView style={[backgroundStyle, {flex: 1}]}>
Expand Down Expand Up @@ -267,11 +281,23 @@ export const App = () => {
title="setTzOffsetInMinutes to 120"
/>
</View>
<View style={styles.button}>
<Button
testID="setMinMax"
onPress={() => {
toggleMinMaxDate();
setShow(true);
}}
title="toggleMinMaxDate"
/>
</View>
{show && (
<DateTimePicker
testID="dateTimePicker"
timeZoneOffsetInMinutes={tzOffsetInMinutes}
minuteInterval={interval}
maximumDate={maximumDate}
minimumDate={minimumDate}
value={date}
mode={mode}
is24Hour
Expand Down
52 changes: 51 additions & 1 deletion example/e2e/detoxTest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ describe('Example', () => {
});

it('setTz should change time text when setTzOffsetInMinutes is 120 minutes', async () => {
await element(by.id('DateTimePickerScrollView')).scrollTo('bottom');
await elementById('DateTimePickerScrollView').scrollTo('bottom');
await userOpensPicker({
mode: 'time',
display: getPickerDisplay(),
Expand All @@ -186,6 +186,56 @@ describe('Example', () => {
}
await expect(getTimeText()).toHaveText('09:30');
});

it('should let you pick tomorrow but not yesterday when setting min/max', async () => {
await elementById('DateTimePickerScrollView').scrollTo('bottom');
await elementById('setTzOffsetToZero').tap();
await elementById('setMinMax').tap();

if (isIOS()) {
const testElement = getDateTimePickerControlIOS();

// Ensure you can't select yesterday (iOS)
await testElement.setDatePickerDate('2021-11-12', 'yyyy-MM-dd');
await expect(getDateText()).toHaveText('11/13/2021');

// Ensure you can select tomorrow (iOS)
await userOpensPicker({mode: 'date', display: getPickerDisplay()});
await testElement.setDatePickerDate('2021-11-14', 'yyyy-MM-dd');
} else {
const uiDevice = device.getUiDevice();

// Ensure you can't select yesterday (Android)
const focusTwelethOfNovemberInCalendar = async () => {
for (var i = 0; i < 4; i++) {
await uiDevice.pressDPadDown();
}
for (var i = 0; i < 3; i++) {
await uiDevice.pressDPadLeft();
}
};
await focusTwelethOfNovemberInCalendar();
await uiDevice.pressEnter();
await userTapsOkButtonAndroid();
await expect(getDateText()).toHaveText('11/13/2021');

// Ensure you can select tomorrow (Android)
await userOpensPicker({mode: 'date', display: getPickerDisplay()});
const focusFourteenthOfNovemberInCalendar = async () => {
for (var i = 0; i < 5; i++) {
await uiDevice.pressDPadDown();
}
for (var i = 0; i < 2; i++) {
await uiDevice.pressDPadLeft();
}
};
await focusFourteenthOfNovemberInCalendar();
await uiDevice.pressEnter();
await userTapsOkButtonAndroid();
}

await expect(getDateText()).toHaveText('11/14/2021');
});
});

it(':android: given we specify neutralButtonLabel, tapping the corresponding button sets date to the beginning of the unix time epoch', async () => {
Expand Down