diff --git a/.gitignore b/.gitignore
index 2145c4cf..964f0b81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,3 +59,5 @@ buck-out/
# CocoaPods
example/ios/Pods/
+
+.xcode.env
\ No newline at end of file
diff --git a/README.md b/README.md
index 179829af..c26fd2a6 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ React Native date & time picker component for iOS, Android and Windows.
- [`value` (`required`)](#value-required)
- [`maximumDate` (`optional`)](#maximumdate-optional)
- [`minimumDate` (`optional`)](#minimumdate-optional)
+ - [`timeZoneName` (`optional`, `iOS or Android only`)](#timeZoneName-optional-ios-and-android-only)
- [`timeZoneOffsetInMinutes` (`optional`, `iOS or Android only`)](#timezoneoffsetinminutes-optional-ios-and-android-only)
- [`timeZoneOffsetInSeconds` (`optional`, `Windows only`)](#timezoneoffsetinsecond-optional-windows-only)
- [`dayOfWeekFormat` (`optional`, `Windows only`)](#dayOfWeekFormat-optional-windows-only)
@@ -309,11 +310,13 @@ This is called when the user changes the date or time in the UI. It receives the
It is also called when user dismisses the picker, which you can detect by checking the `event.type` property.
The values can be: `'set' | 'dismissed' | 'neutralButtonPressed'`. (`neutralButtonPressed` is only available on Android).
+The `utcOffset` field is only available on Android and iOS. It is the offset in minutes between the selected date and UTC time.
+
```js
const setDate = (event: DateTimePickerEvent, date: Date) => {
const {
type,
- nativeEvent: {timestamp},
+ nativeEvent: {timestamp, utcOffset},
} = event;
};
@@ -344,10 +347,21 @@ Defines the minimum date that can be selected. Note that on Android, this only w
```
+#### `timeZoneName` (`optional`, `iOS and Android only`)
+
+Allows changing of the time zone of the date picker. By default, it uses the device's time zone.
+Use the time zone name from the IANA (TZDB) database name in https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
+
+```js
+
+```
+
#### `timeZoneOffsetInMinutes` (`optional`, `iOS and Android only`)
-Allows changing of the timeZone of the date picker. By default, it uses the device's time zone.
-We strongly recommend avoiding this prop on android because of known issues in the implementation (eg. [#528](https://github.com/react-native-datetimepicker/datetimepicker/issues/528)).
+Allows changing of the time zone of the date picker. By default, it uses the device's time zone.
+We **strongly** recommend using `timeZoneName` prop instead; this prop has known issues in the android implementation (eg. [#528](https://github.com/react-native-datetimepicker/datetimepicker/issues/528)).
+
+This prop will be removed in a future release.
```js
// GMT+1
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java
index b8c7acb3..9effd097 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java
@@ -18,8 +18,15 @@
import androidx.fragment.app.FragmentManager;
import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.util.RNLog;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashSet;
import java.util.Locale;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
public class Common {
@@ -63,21 +70,18 @@ public static int getDefaultDialogButtonTextColor(@NonNull Context activity) {
@NonNull
public static DialogInterface.OnShowListener setButtonTextColor(@NonNull final Context activityContext, final AlertDialog dialog, final Bundle args, final boolean needsColorOverride) {
- return new DialogInterface.OnShowListener() {
- @Override
- public void onShow(DialogInterface dialogInterface) {
- // change text color only if custom color is set or if spinner mode is set
- // because spinner suffers from https://github.com/react-native-datetimepicker/datetimepicker/issues/543
-
- Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
- Button negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
- Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
-
- int textColorPrimary = getDefaultDialogButtonTextColor(activityContext);
- setTextColor(positiveButton, POSITIVE, args, needsColorOverride, textColorPrimary);
- setTextColor(negativeButton, NEGATIVE, args, needsColorOverride, textColorPrimary);
- setTextColor(neutralButton, NEUTRAL, args, needsColorOverride, textColorPrimary);
- }
+ return dialogInterface -> {
+ // change text color only if custom color is set or if spinner mode is set
+ // because spinner suffers from https://github.com/react-native-datetimepicker/datetimepicker/issues/543
+
+ Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ Button negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
+ Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
+
+ int textColorPrimary = getDefaultDialogButtonTextColor(activityContext);
+ setTextColor(positiveButton, POSITIVE, args, needsColorOverride, textColorPrimary);
+ setTextColor(negativeButton, NEGATIVE, args, needsColorOverride, textColorPrimary);
+ setTextColor(neutralButton, NEUTRAL, args, needsColorOverride, textColorPrimary);
};
}
@@ -139,4 +143,63 @@ private static void setButtonLabel(Bundle buttonConfig, AlertDialog dialog, int
}
dialog.setButton(whichButton, buttonConfig.getString(LABEL), listener);
}
+
+ public static TimeZone getTimeZone(Bundle args) {
+ if (args != null && args.containsKey(RNConstants.ARG_TZOFFSET_MINS)) {
+ return new SimpleTimeZone((int)args.getLong(RNConstants.ARG_TZOFFSET_MINS) * 60 * 1000, "GMT");
+ }
+
+ if (args != null && args.containsKey(RNConstants.ARG_TZ_NAME)) {
+ String timeZoneName = args.getString(RNConstants.ARG_TZ_NAME);
+ if ("GMT".equals(timeZoneName)) {
+ return TimeZone.getTimeZone("GMT");
+ } else if (!"GMT".equals(TimeZone.getTimeZone(timeZoneName).getID())) {
+ return TimeZone.getTimeZone(timeZoneName);
+ }
+ RNLog.w(null, "'" + timeZoneName + "' does not exist in TimeZone.getAvailableIDs(). Falling back to TimeZone.getDefault()=" + TimeZone.getDefault().getID());
+ }
+
+ return TimeZone.getDefault();
+ }
+
+ public static long maxDateWithTimeZone(Bundle args) {
+ if (!args.containsKey(RNConstants.ARG_MAXDATE)) {
+ return Long.MAX_VALUE;
+ }
+
+ Calendar maxDate = Calendar.getInstance(getTimeZone(args));
+ maxDate.setTimeInMillis(args.getLong(RNConstants.ARG_MAXDATE));
+ maxDate.set(Calendar.HOUR_OF_DAY, 23);
+ maxDate.set(Calendar.MINUTE, 59);
+ maxDate.set(Calendar.SECOND, 59);
+ maxDate.set(Calendar.MILLISECOND, 999);
+ return maxDate.getTimeInMillis();
+ }
+
+ public static long minDateWithTimeZone(Bundle args) {
+ if (!args.containsKey(RNConstants.ARG_MINDATE)) {
+ return 0;
+ }
+
+ Calendar minDate = Calendar.getInstance(getTimeZone(args));
+ minDate.setTimeInMillis(args.getLong(RNConstants.ARG_MINDATE));
+ minDate.set(Calendar.HOUR_OF_DAY, 0);
+ minDate.set(Calendar.MINUTE, 0);
+ minDate.set(Calendar.SECOND, 0);
+ minDate.set(Calendar.MILLISECOND, 0);
+ return minDate.getTimeInMillis();
+ }
+
+ public static Bundle createFragmentArguments(ReadableMap options) {
+ final Bundle args = new Bundle();
+
+ if (options.hasKey(RNConstants.ARG_VALUE) && !options.isNull(RNConstants.ARG_VALUE)) {
+ args.putLong(RNConstants.ARG_VALUE, (long) options.getDouble(RNConstants.ARG_VALUE));
+ }
+ if (options.hasKey(RNConstants.ARG_TZ_NAME) && !options.isNull(RNConstants.ARG_TZ_NAME)) {
+ args.putString(RNConstants.ARG_TZ_NAME, options.getString(RNConstants.ARG_TZ_NAME));
+ }
+
+ return args;
+ }
}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java
index 881ffa4d..441f258c 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java
@@ -22,8 +22,6 @@
import com.facebook.react.module.annotations.ReactModule;
import static com.reactcommunity.rndatetimepicker.Common.dismissDialog;
-import static com.reactcommunity.rndatetimepicker.KeepDateInRangeListener.isDateAfterMaxDate;
-import static com.reactcommunity.rndatetimepicker.KeepDateInRangeListener.isDateBeforeMinDate;
import java.util.Calendar;
@@ -41,8 +39,9 @@ public DatePickerModule(ReactApplicationContext reactContext) {
super(reactContext);
}
+ @NonNull
@Override
- public @NonNull String getName() {
+ public String getName() {
return NAME;
}
@@ -60,31 +59,15 @@ public DatePickerDialogListener(final Promise promise, Bundle arguments) {
@Override
public void onDateSet(DatePicker view, int year, int month, int day) {
if (!mPromiseResolved && getReactApplicationContext().hasActiveReactInstance()) {
+ final RNDate date = new RNDate(mArgs);
+ Calendar calendar = Calendar.getInstance(Common.getTimeZone(mArgs));
+ calendar.set(year, month, day, date.hour(), date.minute(), 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+
WritableMap result = new WritableNativeMap();
result.putString("action", RNConstants.ACTION_DATE_SET);
- result.putInt("year", year);
- result.putInt("month", month);
- result.putInt("day", day);
-
- // https://issuetracker.google.com/issues/169602180
- // TODO revisit day, month, year with timezoneoffset fixes
- if (isDateAfterMaxDate(mArgs, year, month, day)) {
- Calendar maxDate = Calendar.getInstance();
- maxDate.setTimeInMillis(mArgs.getLong(RNConstants.ARG_MAXDATE));
-
- result.putInt("year", maxDate.get(Calendar.YEAR));
- result.putInt("month", maxDate.get(Calendar.MONTH) );
- result.putInt("day", maxDate.get(Calendar.DAY_OF_MONTH));
- }
-
- if (isDateBeforeMinDate(mArgs, year, month, day)) {
- Calendar minDate = Calendar.getInstance();
- minDate.setTimeInMillis(mArgs.getLong(RNConstants.ARG_MINDATE));
-
- result.putInt("year", minDate.get(Calendar.YEAR));
- result.putInt("month", minDate.get(Calendar.MONTH) );
- result.putInt("day", minDate.get(Calendar.DAY_OF_MONTH));
- }
+ result.putDouble("timestamp", calendar.getTimeInMillis());
+ result.putDouble("utcOffset", calendar.getTimeZone().getOffset(calendar.getTimeInMillis()) / 1000 / 60);
mPromise.resolve(result);
mPromiseResolved = true;
@@ -157,35 +140,32 @@ public void open(final ReadableMap options, final Promise promise) {
final FragmentManager fragmentManager = activity.getSupportFragmentManager();
- UiThreadUtil.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- RNDatePickerDialogFragment oldFragment =
- (RNDatePickerDialogFragment) fragmentManager.findFragmentByTag(NAME);
+ UiThreadUtil.runOnUiThread(() -> {
+ RNDatePickerDialogFragment oldFragment =
+ (RNDatePickerDialogFragment) fragmentManager.findFragmentByTag(NAME);
- if (oldFragment != null) {
- oldFragment.update(createFragmentArguments(options));
- return;
- }
+ Bundle arguments = createFragmentArguments(options);
- RNDatePickerDialogFragment fragment = new RNDatePickerDialogFragment();
+ if (oldFragment != null) {
+ oldFragment.update(arguments);
+ return;
+ }
- fragment.setArguments(createFragmentArguments(options));
+ RNDatePickerDialogFragment fragment = new RNDatePickerDialogFragment();
- final DatePickerDialogListener listener = new DatePickerDialogListener(promise, createFragmentArguments(options));
- fragment.setOnDismissListener(listener);
- fragment.setOnDateSetListener(listener);
- fragment.setOnNeutralButtonActionListener(listener);
- fragment.show(fragmentManager, NAME);
- }
+ fragment.setArguments(arguments);
+
+ final DatePickerDialogListener listener = new DatePickerDialogListener(promise, arguments);
+ fragment.setOnDismissListener(listener);
+ fragment.setOnDateSetListener(listener);
+ fragment.setOnNeutralButtonActionListener(listener);
+ fragment.show(fragmentManager, NAME);
});
}
private Bundle createFragmentArguments(ReadableMap options) {
- final Bundle args = new Bundle();
- if (options.hasKey(RNConstants.ARG_VALUE) && !options.isNull(RNConstants.ARG_VALUE)) {
- args.putLong(RNConstants.ARG_VALUE, (long) options.getDouble(RNConstants.ARG_VALUE));
- }
+ final Bundle args = Common.createFragmentArguments(options);
+
if (options.hasKey(RNConstants.ARG_MINDATE) && !options.isNull(RNConstants.ARG_MINDATE)) {
args.putLong(RNConstants.ARG_MINDATE, (long) options.getDouble(RNConstants.ARG_MINDATE));
}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/KeepDateInRangeListener.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/KeepDateInRangeListener.java
deleted file mode 100644
index 0da88bfe..00000000
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/KeepDateInRangeListener.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.reactcommunity.rndatetimepicker;
-
-import android.os.Bundle;
-import android.widget.DatePicker;
-
-import androidx.annotation.NonNull;
-
-import java.util.Calendar;
-
-// fix for https://issuetracker.google.com/issues/169602180
-// TODO revisit day, month, year with timezoneoffset fixes
-public class KeepDateInRangeListener implements DatePicker.OnDateChangedListener {
-
- private final Bundle args;
-
- public KeepDateInRangeListener(@NonNull Bundle args) {
- this.args = args;
- }
-
- @Override
- public void onDateChanged(DatePicker view, int year, int month, int day) {
- fixPotentialMaxDateBug(view, year, month, day);
- fixPotentialMinDateBug(view, year, month, day);
- }
-
- private void fixPotentialMaxDateBug(DatePicker datePicker, int year, int month, int day) {
- if (!isDateAfterMaxDate(args, year, month, day)) {
- return;
- }
- Calendar maxDate = Calendar.getInstance();
- maxDate.setTimeInMillis(args.getLong(RNConstants.ARG_MAXDATE));
- datePicker.updateDate(maxDate.get(Calendar.YEAR), maxDate.get(Calendar.MONTH), maxDate.get(Calendar.DAY_OF_MONTH));
- }
-
- private void fixPotentialMinDateBug(DatePicker datePicker, int year, int month, int day) {
- if (!isDateBeforeMinDate(args, year, month, day)) {
- return;
- }
- Calendar c = Calendar.getInstance();
- c.setTimeInMillis(args.getLong(RNConstants.ARG_MINDATE));
- datePicker.updateDate(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH));
- }
-
- public static boolean isDateAfterMaxDate(Bundle args, int year, int month, int day) {
- if (!args.containsKey(RNConstants.ARG_MAXDATE)) {
- return false;
- }
- Calendar maxDate = Calendar.getInstance();
- maxDate.setTimeInMillis(args.getLong(RNConstants.ARG_MAXDATE));
- return (year > maxDate.get(Calendar.YEAR) ||
- (year == maxDate.get(Calendar.YEAR) && month > maxDate.get(Calendar.MONTH)) ||
- (year == maxDate.get(Calendar.YEAR) && month == maxDate.get(Calendar.MONTH) && day > maxDate.get(Calendar.DAY_OF_MONTH)));
- }
-
- public static boolean isDateBeforeMinDate(Bundle args, int year, int month, int day) {
- if (!args.containsKey(RNConstants.ARG_MINDATE)) {
- return false;
- }
- Calendar minDate = Calendar.getInstance();
- minDate.setTimeInMillis(args.getLong(RNConstants.ARG_MINDATE));
- return (year < minDate.get(Calendar.YEAR) ||
- (year == minDate.get(Calendar.YEAR) && month < minDate.get(Calendar.MONTH)) ||
- (year == minDate.get(Calendar.YEAR) && month == minDate.get(Calendar.MONTH) && day < minDate.get(Calendar.DAY_OF_MONTH)));
- }
-}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java
index 291cb1ab..99530d51 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java
@@ -12,6 +12,7 @@ public final class RNConstants {
public static final String ARG_DISPLAY = "display";
public static final String ARG_DIALOG_BUTTONS = "dialogButtons";
public static final String ARG_TZOFFSET_MINS = "timeZoneOffsetInMinutes";
+ public static final String ARG_TZ_NAME = "timeZoneName";
public static final String ARG_TESTID = "testID";
public static final String ACTION_DATE_SET = "dateSetAction";
public static final String ACTION_TIME_SET = "timeSetAction";
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDate.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDate.java
index 7cbdac72..137869a9 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDate.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDate.java
@@ -11,19 +11,11 @@ public RNDate(Bundle args) {
now = Calendar.getInstance();
if (args != null && args.containsKey(RNConstants.ARG_VALUE)) {
- set(args.getLong(RNConstants.ARG_VALUE));
+ now.setTimeInMillis((args.getLong(RNConstants.ARG_VALUE)));
}
- if (args != null && args.containsKey(RNConstants.ARG_TZOFFSET_MINS)) {
- now.setTimeZone(TimeZone.getTimeZone("GMT"));
- Long timeZoneOffsetInMinutesFallback = args.getLong(RNConstants.ARG_TZOFFSET_MINS);
- Integer timeZoneOffsetInMinutes = args.getInt(RNConstants.ARG_TZOFFSET_MINS, timeZoneOffsetInMinutesFallback.intValue());
- now.add(Calendar.MILLISECOND, timeZoneOffsetInMinutes * 60000);
- }
- }
- public void set(long value) {
- now.setTimeInMillis(value);
+ now.setTimeZone(Common.getTimeZone(args));
}
public int year() { return now.get(Calendar.YEAR); }
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java
index 735e578c..72de7cbb 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java
@@ -30,7 +30,6 @@
import java.util.Calendar;
import java.util.Locale;
-import java.util.TimeZone;
@SuppressLint("ValidFragment")
public class RNDatePickerDialogFragment extends DialogFragment {
@@ -95,8 +94,6 @@ DatePickerDialog getDialog(
private DatePickerDialog createDialog(Bundle args) {
Context activityContext = getActivity();
- final Calendar c = Calendar.getInstance();
-
DatePickerDialog dialog = getDialog(args, activityContext, mOnDateSetListener);
if (args != null) {
@@ -109,66 +106,39 @@ private DatePickerDialog createDialog(Bundle args) {
}
final DatePicker datePicker = dialog.getDatePicker();
+ final long minDate = Common.minDateWithTimeZone(args);
+ final long maxDate = Common.maxDateWithTimeZone(args);
- 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
- // that it generates from the y/m/d passed in the constructor.
- c.setTimeInMillis(args.getLong(RNConstants.ARG_MINDATE));
- c.set(Calendar.HOUR_OF_DAY, 0);
- c.set(Calendar.MINUTE, 0);
- c.set(Calendar.SECOND, 0);
- c.set(Calendar.MILLISECOND, 0);
- datePicker.setMinDate(c.getTimeInMillis() - getOffset(c, timeZoneOffsetInMilliseconds));
+ if (args.containsKey(RNConstants.ARG_MINDATE)) {
+ datePicker.setMinDate(minDate);
} else {
// This is to work around a bug in DatePickerDialog where it doesn't display a title showing
// the date under certain conditions.
datePicker.setMinDate(RNConstants.DEFAULT_MIN_DATE);
}
- if (args != null && args.containsKey(RNConstants.ARG_MAXDATE)) {
- // Set maxDate to the end of the day, same reason as for minDate.
- c.setTimeInMillis(args.getLong(RNConstants.ARG_MAXDATE));
- c.set(Calendar.HOUR_OF_DAY, 23);
- c.set(Calendar.MINUTE, 59);
- c.set(Calendar.SECOND, 59);
- c.set(Calendar.MILLISECOND, 999);
- datePicker.setMaxDate(c.getTimeInMillis() - getOffset(c, timeZoneOffsetInMilliseconds));
+ if (args.containsKey(RNConstants.ARG_MAXDATE)) {
+ datePicker.setMaxDate(maxDate);
}
- if (args != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
- && (args.containsKey(RNConstants.ARG_MAXDATE) || args.containsKey(RNConstants.ARG_MINDATE))) {
- datePicker.setOnDateChangedListener(new KeepDateInRangeListener(args));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (args.containsKey(RNConstants.ARG_MAXDATE) || args.containsKey(RNConstants.ARG_MINDATE))) {
+ datePicker.setOnDateChangedListener((view, year, monthOfYear, dayOfMonth) -> {
+ Calendar calendar = Calendar.getInstance(Common.getTimeZone(args));
+ calendar.set(year, monthOfYear, dayOfMonth, 0, 0, 0);
+ long timestamp = Math.min(Math.max(calendar.getTimeInMillis(), minDate), maxDate);
+ calendar.setTimeInMillis(timestamp);
+ if (datePicker.getYear() != calendar.get(Calendar.YEAR) || datePicker.getMonth() != calendar.get(Calendar.MONTH) || datePicker.getDayOfMonth() != calendar.get(Calendar.DAY_OF_MONTH)) {
+ datePicker.updateDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
+ }
+ });
}
- if (args != null && args.containsKey(RNConstants.ARG_TESTID)) {
+ if (args.containsKey(RNConstants.ARG_TESTID)) {
datePicker.setTag(args.getString(RNConstants.ARG_TESTID));
}
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(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/TimePickerModule.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/TimePickerModule.java
index f948d7ad..034df515 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/TimePickerModule.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/TimePickerModule.java
@@ -25,6 +25,8 @@
import static com.reactcommunity.rndatetimepicker.Common.dismissDialog;
+import java.util.Calendar;
+
/**
* {@link NativeModule} that allows JS to show a native time picker dialog and get called back when
* the user selects a time.
@@ -47,19 +49,27 @@ public String getName() {
private class TimePickerDialogListener implements OnTimeSetListener, OnDismissListener, OnClickListener {
private final Promise mPromise;
+ private final Bundle mArgs;
private boolean mPromiseResolved = false;
- public TimePickerDialogListener(Promise promise) {
+ public TimePickerDialogListener(Promise promise, Bundle arguments) {
mPromise = promise;
+ mArgs = arguments;
}
@Override
public void onTimeSet(TimePicker view, int hour, int minute) {
if (!mPromiseResolved && getReactApplicationContext().hasActiveReactInstance()) {
+ final RNDate date = new RNDate(mArgs);
+ Calendar calendar = Calendar.getInstance(Common.getTimeZone(mArgs));
+ calendar.set(date.year(), date.month(), date.day(), hour, minute, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+
WritableMap result = new WritableNativeMap();
result.putString("action", RNConstants.ACTION_TIME_SET);
- result.putInt("hour", hour);
- result.putInt("minute", minute);
+ result.putDouble("timestamp", calendar.getTimeInMillis());
+ result.putDouble("utcOffset", calendar.getTimeZone().getOffset(calendar.getTimeInMillis()) / 1000 / 60);
+
mPromise.resolve(result);
mPromiseResolved = true;
}
@@ -105,35 +115,32 @@ public void open(final ReadableMap options, final Promise promise) {
// (for apps that use it for legacy reasons). This unfortunately leads to some code duplication.
final FragmentManager fragmentManager = activity.getSupportFragmentManager();
- UiThreadUtil.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- RNTimePickerDialogFragment oldFragment =
- (RNTimePickerDialogFragment) fragmentManager.findFragmentByTag(NAME);
+ UiThreadUtil.runOnUiThread(() -> {
+ RNTimePickerDialogFragment oldFragment =
+ (RNTimePickerDialogFragment) fragmentManager.findFragmentByTag(NAME);
+
+ Bundle arguments = createFragmentArguments(options);
- if (oldFragment != null) {
- oldFragment.update(createFragmentArguments(options));
- return;
- }
+ if (oldFragment != null) {
+ oldFragment.update(arguments);
+ return;
+ }
- RNTimePickerDialogFragment fragment = new RNTimePickerDialogFragment();
+ RNTimePickerDialogFragment fragment = new RNTimePickerDialogFragment();
- fragment.setArguments(createFragmentArguments(options));
+ fragment.setArguments(arguments);
- final TimePickerDialogListener listener = new TimePickerDialogListener(promise);
- fragment.setOnDismissListener(listener);
- fragment.setOnTimeSetListener(listener);
- fragment.setOnNeutralButtonActionListener(listener);
- fragment.show(fragmentManager, NAME);
- }
+ final TimePickerDialogListener listener = new TimePickerDialogListener(promise, arguments);
+ fragment.setOnDismissListener(listener);
+ fragment.setOnTimeSetListener(listener);
+ fragment.setOnNeutralButtonActionListener(listener);
+ fragment.show(fragmentManager, NAME);
});
}
private Bundle createFragmentArguments(ReadableMap options) {
- final Bundle args = new Bundle();
- if (options.hasKey(RNConstants.ARG_VALUE) && !options.isNull(RNConstants.ARG_VALUE)) {
- args.putLong(RNConstants.ARG_VALUE, (long) options.getDouble(RNConstants.ARG_VALUE));
- }
+ final Bundle args = Common.createFragmentArguments(options);
+
if (options.hasKey(RNConstants.ARG_IS24HOUR) && !options.isNull(RNConstants.ARG_IS24HOUR)) {
args.putBoolean(RNConstants.ARG_IS24HOUR, options.getBoolean(RNConstants.ARG_IS24HOUR));
}
@@ -147,7 +154,7 @@ private Bundle createFragmentArguments(ReadableMap options) {
args.putInt(RNConstants.ARG_INTERVAL, options.getInt(RNConstants.ARG_INTERVAL));
}
if (options.hasKey(RNConstants.ARG_TZOFFSET_MINS) && !options.isNull(RNConstants.ARG_TZOFFSET_MINS)) {
- args.putInt(RNConstants.ARG_TZOFFSET_MINS, options.getInt(RNConstants.ARG_TZOFFSET_MINS));
+ args.putLong(RNConstants.ARG_TZOFFSET_MINS, (long) options.getDouble(RNConstants.ARG_TZOFFSET_MINS));
}
return args;
}
diff --git a/example/App.js b/example/App.js
index 95ce3de6..2a591ac0 100644
--- a/example/App.js
+++ b/example/App.js
@@ -11,13 +11,14 @@ import {
useColorScheme,
Switch,
Alert,
+ FlatList,
} from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';
import SegmentedControl from './SegmentedControl';
import {Colors} from 'react-native/Libraries/NewAppScreen';
import React, {useRef, useState} from 'react';
import {Picker} from 'react-native-windows';
-import moment from 'moment';
+import moment from 'moment-timezone';
import {
ANDROID_MODE,
DAY_OF_WEEK,
@@ -25,7 +26,22 @@ import {
ANDROID_DISPLAY,
IOS_DISPLAY,
} from '../src/constants';
-// import * as RNLocalize from 'react-native-localize';
+import * as RNLocalize from 'react-native-localize';
+
+const timezone = [
+ 120,
+ 0,
+ -120,
+ undefined,
+ 'America/New_York',
+ 'America/Vancouver',
+ 'Europe/London',
+ 'Europe/Istanbul',
+ 'Asia/Hong_Kong',
+ 'Australia/Brisbane',
+ 'Australia/Sydney',
+ 'Australia/Adelaide',
+];
const ThemedText = (props) => {
const isDarkMode = useColorScheme() === 'dark';
@@ -49,6 +65,17 @@ const ThemedTextInput = (props) => {
});
};
+const Info = ({testID, title, body}) => {
+ return (
+
+ {title}
+
+ {body}
+
+
+ );
+};
+
const MODE_VALUES = Platform.select({
ios: Object.values(IOS_MODE),
android: Object.values(ANDROID_MODE),
@@ -63,10 +90,11 @@ 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 sourceMoment = moment.unix(1636797600);
+ const sourceMoment = moment.unix(1636765200);
const sourceDate = sourceMoment.local().toDate();
const [date, setDate] = useState(sourceDate);
const [tzOffsetInMinutes, setTzOffsetInMinutes] = useState(undefined);
+ const [tzName, setTzName] = useState(RNLocalize.getTimeZone());
const [mode, setMode] = useState(MODE_VALUES[0]);
const [show, setShow] = useState(false);
const [textColor, setTextColor] = useState();
@@ -80,8 +108,8 @@ export const App = () => {
// Windows-specific
const [time, setTime] = useState(undefined);
- const [maxDate, setMinDate] = useState(new Date('2021'));
- const [minDate, setMaxDate] = useState(new Date('2018'));
+ const [maxDate] = useState(new Date('2021'));
+ const [minDate] = useState(new Date('2018'));
const [is24Hours, set24Hours] = useState(false);
const [firstDayOfWeek, setFirstDayOfWeek] = useState(DAY_OF_WEEK.Monday);
const [dateFormat, setDateFormat] = useState('longdate');
@@ -132,8 +160,29 @@ export const App = () => {
backgroundColor: isDarkMode ? Colors.dark : Colors.lighter,
};
+ const renderItem = ({item}) => {
+ const isNumber = typeof item === 'number';
+ const title = isNumber
+ ? item > 0
+ ? `+${item} mins`
+ : `${item} mins`
+ : item;
+ return (
+
+
+ );
+ };
+
const toggleMinMaxDateInUTC = () => {
setTzOffsetInMinutes(0);
+ setTzName(undefined);
const startOfTodayUTC = sourceMoment.utc().startOf('day').toDate();
setMinimumDate(maximumDate ? undefined : startOfTodayUTC);
@@ -147,8 +196,65 @@ export const App = () => {
if (Platform.OS !== 'windows') {
return (
-
-
+
+
+
+
+ Example DateTime Picker
+
+
+
+
+
+ {(tzName || !isNaN(tzOffsetInMinutes)) && (
+ <>
+ {
+ if (tzName) {
+ return moment(date).tz(tzName).format();
+ }
+ if (tzOffsetInMinutes !== undefined) {
+ return moment(date).utcOffset(tzOffsetInMinutes).format();
+ }
+ return '';
+ })()}
+ />
+ {
+ if (tzName) {
+ return 'Overridden TzName:';
+ }
+ if (tzOffsetInMinutes !== undefined) {
+ return 'Overridden TzOffset:';
+ }
+ return '';
+ })()}
+ body={tzName || `${tzOffsetInMinutes} mins`}
+ />
+ >
+ )}
+
+
{
scrollRef.current?.scrollToEnd({animated: true});
}
}}>
- {global.HermesInternal != null && (
-
-
- Engine: Hermes
-
-
- )}
-
-
-
- Example DateTime Picker
-
-
-
-
- {/*TZ: {RNLocalize.getTimeZone()}, original:{' '}*/}
- {moment(sourceDate).format('MM/DD/YYYY HH:mm')}
-
-
- , TZOffset:{new Date().getTimezoneOffset() / 60}
-
+ mode prop:
+ {
+ setMode(MODE_VALUES[event.nativeEvent.selectedSegmentIndex]);
+ }}
+ />
+ display prop:
+ {
+ setDisplay(
+ DISPLAY_VALUES[event.nativeEvent.selectedSegmentIndex],
+ );
+ }}
+ />
+ minute interval prop:
+ {
+ setMinInterval(
+ MINUTE_INTERVALS[event.nativeEvent.selectedSegmentIndex],
+ );
+ }}
+ />
+
+
+ text color (iOS only)
+
+ {
+ setTextColor(text.toLowerCase());
+ }}
+ placeholder="textColor"
+ />
+
+
+
+ accent color (iOS only)
+
+ {
+ setAccentColor(text.toLowerCase());
+ }}
+ placeholder="accentColor"
+ />
+
+
+
+ disabled (iOS only)
+
+
+
- mode prop:
- {
- setMode(MODE_VALUES[event.nativeEvent.selectedSegmentIndex]);
+
+
+
+ neutralButtonLabel (android only)
+
+
+
+
+
+ [android] show and dismiss picker after 3 secs
+
+
+
+
+
+
+
+
+ {
+ toggleMinMaxDateInUTC();
+ setShow(true);
}}
+ title="toggleMinMaxDate"
/>
-
-
- text color (iOS only)
-
- {
- setTextColor(text.toLowerCase());
- }}
- placeholder="textColor"
- />
-
-
-
- accent color (iOS only)
-
- {
- setAccentColor(text.toLowerCase());
- }}
- placeholder="accentColor"
- />
-
-
-
- disabled (iOS only)
-
-
-
-
-
- neutralButtonLabel (android only)
-
-
-
-
-
- [android] show and dismiss picker after 3 secs
-
-
-
- {
- setShow(true);
- setTimeout(() => {
- setShow(false);
- }, 6000);
- }}
- title="Show and dismiss picker!"
- />
-
-
- {
- setShow(true);
- }}
- title="Show picker!"
- />
- setShow(false)}
- title="Hide picker!"
- />
-
-
-
- {moment(date).format('MM/DD/YYYY')}
-
-
-
- {moment(date).format('HH:mm')}
-
-
-
- tzOffset: {tzOffsetInMinutes ?? 'auto'}
-
-
-
- {
- setTzOffsetInMinutes(0);
- }}
- title="setTzOffsetInMinutes to 0"
- />
-
-
- {
- setTzOffsetInMinutes(120);
- }}
- title="setTzOffsetInMinutes to 120"
- />
-
-
- {
- toggleMinMaxDateInUTC();
- setShow(true);
- }}
- title="toggleMinMaxDate"
+
+
+ {/* This label ensures there is no regression in this former bug: https://github.com/react-native-datetimepicker/datetimepicker/issues/409 */}
+
+ This is a very very very very very very long text to showcase
+ behavior
+
+ {show && (
+
-
-
- {/* This label ensures there is no regression in this former bug: https://github.com/react-native-datetimepicker/datetimepicker/issues/409 */}
-
- This is a very very very very very very long text to showcase
- behavior
-
- {show && (
-
- )}
-
+ )}
diff --git a/example/e2e/detoxTest.spec.js b/example/e2e/detoxTest.spec.js
index 2dd494c0..3c6b63b1 100644
--- a/example/e2e/detoxTest.spec.js
+++ b/example/e2e/detoxTest.spec.js
@@ -1,6 +1,4 @@
const {
- getTimeText,
- getDateText,
elementById,
elementByText,
getDateTimePickerIOS,
@@ -15,9 +13,13 @@ const {
userTapsOkButtonAndroid,
userDismissesCompactDatePicker,
} = require('./utils/actions');
-const {isIOS, wait, Platform} = require('./utils/utils');
+const {isIOS, isAndroid, wait, Platform} = require('./utils/utils');
const {device} = require('detox');
const {describe} = require('jest-circus');
+const {
+ assertTimeLabels,
+ assertInitialTimeLabels,
+} = require('./utils/assertions');
describe('e2e tests', () => {
const getPickerDisplay = () => {
@@ -41,10 +43,10 @@ describe('e2e tests', () => {
.withTimeout(5000);
});
- it.skip('timeInfo heading has expected content', async () => {
- await expect(elementById('timeInfo')).toHaveText(
- 'TZ: Europe/Prague, original: 11/13/2021 11:00',
- );
+ it('timeInfo heading has expected content', async () => {
+ await assertInitialTimeLabels();
+ await expect(elementById('deviceTzName')).toHaveText('Europe/Prague');
+ await expect(elementById('overriddenTzName')).toHaveText('Europe/Prague');
});
it('should show date picker after tapping datePicker button', async () => {
@@ -57,7 +59,7 @@ describe('e2e tests', () => {
}
});
- it('nothing should happen if picker is dismissed / cancelled', async () => {
+ it('nothing should happen if date picker is dismissed / cancelled', async () => {
await userOpensPicker({mode: 'date', display: 'default'});
if (isIOS()) {
@@ -82,15 +84,16 @@ describe('e2e tests', () => {
}
await elementByText('great').tap();
- await expect(getDateText()).toHaveText('11/13/2021');
+ await assertInitialTimeLabels();
});
it('should update dateTimeText when date changes', async () => {
await userOpensPicker({mode: 'date', display: getPickerDisplay()});
+ const targetDate = '2021-11-02T01:00:00Z';
if (isIOS()) {
const testElement = getDateTimePickerControlIOS();
- await testElement.setDatePickerDate('2021-11-02', 'yyyy-MM-dd');
+ await testElement.setDatePickerDate(targetDate, 'ISO8601');
} else {
const uiDevice = device.getUiDevice();
const focusSecondOfNovemberInCalendar = async () => {
@@ -104,11 +107,15 @@ describe('e2e tests', () => {
await userTapsOkButtonAndroid();
}
- await expect(getDateText()).toHaveText('11/02/2021');
+
+ await assertTimeLabels({
+ utcTime: targetDate,
+ deviceTime: '2021-11-02T02:00:00+01:00',
+ });
});
it('should show time picker after tapping timePicker button', async () => {
- const display = await Platform.select({
+ const display = Platform.select({
ios: 'inline',
android: 'default',
});
@@ -131,7 +138,7 @@ describe('e2e tests', () => {
await userTapsCancelButtonAndroid();
await elementByText('great').tap();
}
- await expect(getTimeText()).toHaveText('11:00');
+ await assertInitialTimeLabels();
});
it('should change time text when time changes', async () => {
@@ -140,47 +147,138 @@ describe('e2e tests', () => {
if (isIOS()) {
const testElement = getDateTimePickerControlIOS();
// TODO
- await testElement.setDatePickerDate('15:44', 'HH:mm');
+ await testElement.setDatePickerDate('2021-11-13T14:44:00Z', 'ISO8601');
} else {
await userChangesTimeValue({hours: 15, minutes: 44});
await userTapsOkButtonAndroid();
}
- await expect(getTimeText()).toHaveText('15:44');
+
+ await assertTimeLabels({
+ utcTime: '2021-11-13T14:44:00Z',
+ deviceTime: '2021-11-13T15:44:00+01:00',
+ });
+ });
+
+ describe('IANA time zone', () => {
+ it('should show utcTime, deviceTime, overriddenTime correctly', async () => {
+ await assertInitialTimeLabels();
+
+ await expect(elementById('overriddenTzName')).toHaveText('Europe/Prague');
+
+ await elementById('timezone').swipe('left', 'fast', 0.5);
+
+ let timeZone = 'America/Vancouver';
+ if (isAndroid()) {
+ timeZone = timeZone.toUpperCase();
+ }
+
+ await waitFor(elementByText(timeZone)).toBeVisible().withTimeout(1000);
+
+ await elementByText(timeZone).tap();
+
+ await assertTimeLabels({
+ utcTime: '2021-11-13T01:00:00Z',
+ deviceTime: '2021-11-13T02:00:00+01:00',
+ overriddenTime: '2021-11-12T17:00:00-08:00',
+ });
+
+ await expect(elementById('overriddenTzName')).toHaveText(
+ 'America/Vancouver',
+ );
+ });
+
+ it('daylight saving should work properly', async () => {
+ await elementById('timezone').swipe('left', 'fast', 0.5);
+
+ let timeZone = 'America/Vancouver';
+ if (isAndroid()) {
+ timeZone = timeZone.toUpperCase();
+ }
+
+ await waitFor(elementByText(timeZone)).toBeVisible().withTimeout(1000);
+
+ await elementByText(timeZone).tap();
+
+ await userOpensPicker({mode: 'date', display: getPickerDisplay()});
+
+ if (isIOS()) {
+ const testElement = getDateTimePickerControlIOS();
+
+ await testElement.setDatePickerDate('2021-03-14T10:00:00Z', 'ISO8601');
+ } else {
+ const uiDevice = device.getUiDevice();
+
+ // Ensure you can't select yesterday (Android)
+ const focusFourteenthOfMarchInCalendar = async () => {
+ for (let i = 0; i < 3; i++) {
+ await uiDevice.pressDPadDown();
+ }
+
+ await uiDevice.pressDPadUp();
+
+ for (let i = 0; i < 8; i++) {
+ await uiDevice.pressEnter();
+ }
+
+ for (let i = 0; i < 2; i++) {
+ await uiDevice.pressDPadDown();
+ }
+ };
+ await focusFourteenthOfMarchInCalendar();
+ await uiDevice.pressEnter();
+ await userTapsOkButtonAndroid();
+
+ await userOpensPicker({mode: 'time', display: getPickerDisplay()});
+ await userChangesTimeValue({hours: '2', minutes: '0'});
+ await userTapsOkButtonAndroid();
+ }
+
+ await assertTimeLabels({
+ utcTime: '2021-03-14T10:00:00Z',
+ deviceTime: '2021-03-14T11:00:00+01:00',
+ overriddenTime: '2021-03-14T03:00:00-07:00',
+ });
+ });
});
describe('time zone offset', () => {
- it.skip('should update dateTimeText when date changes and set setTzOffsetInMinutes to 0', async () => {
- // skip for now, there is a bug on android https://github.com/react-native-datetimepicker/datetimepicker/issues/528
- await expect(getDateText()).toHaveText('11/13/2021');
- await expect(getTimeText()).toHaveText('11:00');
+ it('should update dateTimeText when date changes and set setTzOffsetInMinutes to 0', async () => {
+ await assertInitialTimeLabels();
+
+ let tzOffsetPreset = '0 mins';
+
if (isIOS()) {
await userOpensPicker({
mode: 'date',
display: 'spinner',
- tzOffsetPreset: 'setTzOffsetToZero',
+ tzOffsetPreset,
});
- const testElement = getDateTimePickerIOS();
- await testElement.setColumnToValue(0, 'November');
- await testElement.setColumnToValue(1, '14');
- await testElement.setColumnToValue(2, '2021');
} else {
+ tzOffsetPreset = tzOffsetPreset.toUpperCase();
await userOpensPicker({
mode: 'date',
display: 'default',
- tzOffsetPreset: 'setTzOffsetToZero',
+ tzOffsetPreset,
});
await userTapsOkButtonAndroid();
}
- await expect(getDateText()).toHaveText('11/14/2021');
- await expect(getTimeText()).toHaveText('11:00');
+ await expect(elementById('overriddenTime')).toHaveText(
+ '2021-11-13T01:00:00Z',
+ );
});
it('setTz should change time text when setTzOffsetInMinutes is 120 minutes', async () => {
await elementById('DateTimePickerScrollView').scrollTo('bottom');
+
+ let tzOffsetPreset = '+120 mins';
+ if (isAndroid()) {
+ tzOffsetPreset = tzOffsetPreset.toUpperCase();
+ }
+
await userOpensPicker({
mode: 'time',
display: getPickerDisplay(),
- tzOffsetPreset: 'setTzOffset',
+ tzOffsetPreset,
});
if (isIOS()) {
@@ -192,7 +290,11 @@ describe('e2e tests', () => {
await userChangesTimeValue({hours: '7', minutes: '30'});
await userTapsOkButtonAndroid();
}
- await expect(getTimeText()).toHaveText('06:30');
+ await assertTimeLabels({
+ utcTime: '2021-11-13T05:30:00Z',
+ deviceTime: '2021-11-13T06:30:00+01:00',
+ overriddenTime: '2021-11-13T07:30:00+02:00',
+ });
});
it('should let you pick tomorrow but not yesterday when setting min/max', async () => {
@@ -203,28 +305,34 @@ describe('e2e tests', () => {
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');
+ await testElement.setDatePickerDate('2021-11-12T01:00:00Z', 'ISO8601');
+
+ await expect(elementById('utcTime')).toHaveText('2021-11-13T00:00:00Z');
// Ensure you can select tomorrow (iOS)
await userOpensPicker({mode: 'date', display: getPickerDisplay()});
- await testElement.setDatePickerDate('2021-11-14', 'yyyy-MM-dd');
+ await testElement.setDatePickerDate('2021-11-14T01:00:00Z', 'ISO8601');
} else {
const uiDevice = device.getUiDevice();
// Ensure you can't select yesterday (Android)
- const focusTwelethOfNovemberInCalendar = async () => {
+ const focusTwelveOfNovemberInCalendar = async () => {
for (let i = 0; i < 4; i++) {
await uiDevice.pressDPadDown();
}
for (let i = 0; i < 3; i++) {
- await uiDevice.pressDPadLeft();
+ await uiDevice.pressDPadRight();
}
};
- await focusTwelethOfNovemberInCalendar();
+ await focusTwelveOfNovemberInCalendar();
await uiDevice.pressEnter();
await userTapsOkButtonAndroid();
- await expect(getDateText()).toHaveText('11/13/2021');
+
+ await assertTimeLabels({
+ utcTime: '2021-11-13T01:00:00Z',
+ deviceTime: '2021-11-13T02:00:00+01:00',
+ overriddenTime: '2021-11-13T01:00:00Z',
+ });
// Ensure you can select tomorrow (Android)
await userOpensPicker({mode: 'date', display: getPickerDisplay()});
@@ -241,7 +349,11 @@ describe('e2e tests', () => {
await userTapsOkButtonAndroid();
}
- await expect(getDateText()).toHaveText('11/14/2021');
+ await assertTimeLabels({
+ utcTime: '2021-11-14T01:00:00Z',
+ deviceTime: '2021-11-14T02:00:00+01:00',
+ overriddenTime: '2021-11-14T01:00:00Z',
+ });
});
});
@@ -250,8 +362,10 @@ describe('e2e tests', () => {
await userOpensPicker({mode: 'time', display: 'default'});
await elementByText('clear').tap();
- const dateText = getDateText();
- await expect(dateText).toHaveText('01/01/1970');
+ await assertTimeLabels({
+ utcTime: '1970-01-01T00:00:00Z',
+ deviceTime: '1970-01-01T01:00:00+01:00',
+ });
});
it(':android: when component unmounts, dialog is dismissed', async () => {
@@ -264,23 +378,23 @@ describe('e2e tests', () => {
describe('given 5-minute interval', () => {
it(':android: clock picker should correct 18-minute selection to 20-minute one', async () => {
- try {
- await userOpensPicker({mode: 'time', display: 'clock', interval: 5});
+ await userOpensPicker({mode: 'time', display: 'clock', interval: 5});
- await userChangesTimeValue({hours: '23', minutes: '18'});
+ await userChangesTimeValue({hours: '23', minutes: '18'});
- await userTapsOkButtonAndroid();
+ await userTapsOkButtonAndroid();
- await expect(getTimeText()).toHaveText('23:20');
- } catch (err) {
- console.error(err);
- }
+ await assertTimeLabels({
+ utcTime: '2021-11-13T22:20:00Z',
+ deviceTime: '2021-11-13T23:20:00+01:00',
+ });
});
it(':android: when the picker is shown as "spinner", swiping it down changes selected time', async () => {
- const timeText = getTimeText();
-
- await expect(timeText).toHaveText('11:00');
+ await assertTimeLabels({
+ utcTime: '2021-11-13T01:00:00Z',
+ deviceTime: '2021-11-13T02:00:00+01:00',
+ });
await userOpensPicker({mode: 'time', display: 'spinner', interval: 5});
@@ -290,7 +404,10 @@ describe('e2e tests', () => {
await minutePicker.swipe('up', 'slow', 0.33);
await userTapsOkButtonAndroid();
- await expect(timeText).toHaveText('11:15');
+ await assertTimeLabels({
+ utcTime: '2021-11-13T01:15:00Z',
+ deviceTime: '2021-11-13T02:15:00+01:00',
+ });
});
it(':ios: picker should offer only options divisible by 5 (0, 5, 10,...)', async () => {
@@ -300,9 +417,11 @@ describe('e2e tests', () => {
await testElement.setColumnToValue(0, '2');
await testElement.setColumnToValue(1, '15');
await testElement.setColumnToValue(2, 'PM');
- const timeText = getTimeText();
- await expect(timeText).toHaveText('14:15');
+ await assertTimeLabels({
+ utcTime: '2021-11-13T13:15:00Z',
+ deviceTime: '2021-11-13T14:15:00+01:00',
+ });
const valueThatShouldNotBePresented = '18';
try {
@@ -320,7 +439,10 @@ describe('e2e tests', () => {
}
}
- await expect(timeText).toHaveText('14:45');
+ await assertTimeLabels({
+ utcTime: '2021-11-13T13:45:00Z',
+ deviceTime: '2021-11-13T14:45:00+01:00',
+ });
});
});
});
diff --git a/example/e2e/utils/actions.js b/example/e2e/utils/actions.js
index 0203d50e..cca2b178 100644
--- a/example/e2e/utils/actions.js
+++ b/example/e2e/utils/actions.js
@@ -30,7 +30,7 @@ async function userOpensPicker({mode, display, interval, tzOffsetPreset}) {
await element(by.text(String(interval))).tap();
}
if (tzOffsetPreset) {
- await element(by.id(tzOffsetPreset)).tap();
+ await element(by.text(tzOffsetPreset)).tap();
}
await element(by.id('showPickerButton')).tap();
}
diff --git a/example/e2e/utils/assertions.js b/example/e2e/utils/assertions.js
new file mode 100644
index 00000000..5cfab683
--- /dev/null
+++ b/example/e2e/utils/assertions.js
@@ -0,0 +1,18 @@
+const {elementById} = require('./matchers');
+
+async function assertTimeLabels({utcTime, deviceTime, overriddenTime}) {
+ await expect(elementById('utcTime')).toHaveText(utcTime);
+ await expect(elementById('deviceTime')).toHaveText(deviceTime);
+ await expect(elementById('overriddenTime')).toHaveText(
+ overriddenTime ?? deviceTime,
+ );
+}
+
+async function assertInitialTimeLabels() {
+ return await assertTimeLabels({
+ utcTime: '2021-11-13T01:00:00Z',
+ deviceTime: '2021-11-13T02:00:00+01:00',
+ overriddenTime: '2021-11-13T02:00:00+01:00',
+ });
+}
+module.exports = {assertTimeLabels, assertInitialTimeLabels};
diff --git a/example/e2e/utils/matchers.js b/example/e2e/utils/matchers.js
index 239f9d45..02cf20c0 100644
--- a/example/e2e/utils/matchers.js
+++ b/example/e2e/utils/matchers.js
@@ -1,5 +1,3 @@
-const getTimeText = () => element(by.id('timeText'));
-const getDateText = () => element(by.id('dateText'));
const elementById = (id) => element(by.id(id));
const elementByText = (text) => element(by.text(text));
@@ -13,8 +11,6 @@ const getDateTimePickerControlIOS = () => element(by.type('UIDatePicker'));
const getDatePickerAndroid = () => element(by.id('dateTimePicker'));
module.exports = {
- getTimeText,
- getDateText,
elementById,
elementByText,
getDateTimePickerIOS,
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 86b783d3..18de3802 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -655,7 +655,7 @@ PODS:
- React-Core
- React-jsi
- ReactTestApp-Resources (1.0.0-dev)
- - RNDateTimePicker (7.2.0):
+ - RNDateTimePicker (7.4.0):
- RCT-Folly (= 2021.07.22.00)
- RCTRequired
- RCTTypeSafety
@@ -853,10 +853,10 @@ SPEC CHECKSUMS:
ReactCommon: 86289421205f793f8b50106aabca54f0b4abb574
ReactTestApp-DevSupport: 1fa43e1284fd97b62238fb34c8bb349a80721b0d
ReactTestApp-Resources: ff5f151e465e890010b417ce65ca6c5de6aeccbb
- RNDateTimePicker: df9e3decb899aa74ae200cc1e892c25b4e0f08de
+ RNDateTimePicker: 7ba754319710d777f8ed14e83296f87d113b2b53
RNLocalize: 9c4950ae13d7bfaeaac010ecad97abbe96abdfff
Yoga: e7ea9e590e27460d28911403b894722354d73479
PODFILE CHECKSUM: 8d21f6b1c802d4b966f0ed4d84584d57bd07fc86
-COCOAPODS: 1.12.0
+COCOAPODS: 1.12.1
diff --git a/ios/RNDateTimePicker.m b/ios/RNDateTimePicker.m
index c2a70e73..9c73c8ef 100644
--- a/ios/RNDateTimePicker.m
+++ b/ios/RNDateTimePicker.m
@@ -41,7 +41,7 @@ - (instancetype)initWithFrame:(CGRect)frame
- (void)didChange
{
if (_onChange) {
- _onChange(@{ @"timestamp": @(self.date.timeIntervalSince1970 * 1000.0) });
+ _onChange(@{ @"timestamp": @(self.date.timeIntervalSince1970 * 1000.0), @"utcOffset": @([self.timeZone secondsFromGMTForDate:self.date] / 60 )});
}
}
diff --git a/ios/RNDateTimePickerManager.m b/ios/RNDateTimePickerManager.m
index dbf3a626..686fb534 100644
--- a/ios/RNDateTimePickerManager.m
+++ b/ios/RNDateTimePickerManager.m
@@ -78,7 +78,7 @@ - (UIView *)view
- (RCTShadowView *)shadowView
{
- RNDateTimePickerShadowView* shadowView = [RNDateTimePickerShadowView new];
+ RNDateTimePickerShadowView* shadowView = [RNDateTimePickerShadowView new];
shadowView.picker = _picker;
return shadowView;
}
@@ -104,6 +104,7 @@ + (NSString*) datepickerStyleToString: (UIDatePickerStyle) style API_AVAILABLE(
RCT_EXPORT_SHADOW_PROPERTY(mode, UIDatePickerMode)
RCT_EXPORT_SHADOW_PROPERTY(locale, NSLocale)
RCT_EXPORT_SHADOW_PROPERTY(displayIOS, RNCUIDatePickerStyle)
+RCT_EXPORT_SHADOW_PROPERTY(timeZoneName, NSString)
RCT_EXPORT_VIEW_PROPERTY(date, NSDate)
RCT_EXPORT_VIEW_PROPERTY(locale, NSLocale)
@@ -178,4 +179,19 @@ + (NSString*) datepickerStyleToString: (UIDatePickerStyle) style API_AVAILABLE(
}
}
+RCT_CUSTOM_VIEW_PROPERTY(timeZoneName, NSString, RNDateTimePicker)
+{
+ if (json) {
+ NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:json];
+ if (timeZone == nil) {
+ RCTLogWarn(@"'%@' does not exist in NSTimeZone.knownTimeZoneNames fallback to localTimeZone=%@", json, NSTimeZone.localTimeZone.name);
+ view.timeZone = NSTimeZone.localTimeZone;
+ } else {
+ view.timeZone = timeZone;
+ }
+ } else {
+ view.timeZone = NSTimeZone.localTimeZone;
+ }
+}
+
@end
diff --git a/ios/RNDateTimePickerShadowView.h b/ios/RNDateTimePickerShadowView.h
index 04578ee8..4ce9175d 100644
--- a/ios/RNDateTimePickerShadowView.h
+++ b/ios/RNDateTimePickerShadowView.h
@@ -7,6 +7,8 @@
@property (nonatomic) UIDatePickerMode mode;
@property (nullable, nonatomic, strong) NSDate *date;
@property (nullable, nonatomic, strong) NSLocale *locale;
+@property (nonatomic, assign) NSInteger timeZoneOffsetInMinutes;
+@property (nullable, nonatomic, strong) NSString *timeZoneName;
@property (nonatomic, assign) UIDatePickerStyle displayIOS API_AVAILABLE(ios(13.4));
@end
diff --git a/ios/RNDateTimePickerShadowView.m b/ios/RNDateTimePickerShadowView.m
index ba0273bc..c139440c 100644
--- a/ios/RNDateTimePickerShadowView.m
+++ b/ios/RNDateTimePickerShadowView.m
@@ -31,6 +31,16 @@ - (void)setDisplayIOS:(UIDatePickerStyle)displayIOS {
YGNodeMarkDirty(self.yogaNode);
}
+- (void)setTimeZoneOffsetInMinutes:(NSInteger)timeZoneOffsetInMinutes {
+ _timeZoneOffsetInMinutes = timeZoneOffsetInMinutes;
+ YGNodeMarkDirty(self.yogaNode);
+}
+
+- (void)setTimeZoneName:(NSString *)timeZoneName {
+ _timeZoneName = timeZoneName;
+ YGNodeMarkDirty(self.yogaNode);
+}
+
static YGSize RNDateTimePickerShadowViewMeasure(YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
{
RNDateTimePickerShadowView *shadowPickerView = (__bridge RNDateTimePickerShadowView *)YGNodeGetContext(node);
@@ -40,13 +50,28 @@ static YGSize RNDateTimePickerShadowViewMeasure(YGNodeRef node, float width, YGM
[shadowPickerView.picker setDate:shadowPickerView.date];
[shadowPickerView.picker setDatePickerMode:shadowPickerView.mode];
[shadowPickerView.picker setLocale:shadowPickerView.locale];
+ [shadowPickerView.picker setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:shadowPickerView.timeZoneOffsetInMinutes * 60]];
+
+ if (shadowPickerView.timeZoneName) {
+ NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:shadowPickerView.timeZoneName];
+ if (timeZone != nil) {
+ [shadowPickerView.picker setTimeZone:timeZone];
+ } else {
+ RCTLogWarn(@"'%@' does not exist in NSTimeZone.knownTimeZoneNames. Falling back to localTimeZone=%@", shadowPickerView.timeZoneName, NSTimeZone.localTimeZone.name);
+ [shadowPickerView.picker setTimeZone:NSTimeZone.localTimeZone];
+ }
+ } else {
+ [shadowPickerView.picker setTimeZone:NSTimeZone.localTimeZone];
+ }
+
if (@available(iOS 14.0, *)) {
[shadowPickerView.picker setPreferredDatePickerStyle:shadowPickerView.displayIOS];
}
- size = [shadowPickerView.picker sizeThatFits:UILayoutFittingCompressedSize];
- size.width += 10;
+
+ size = [shadowPickerView.picker sizeThatFits:UILayoutFittingCompressedSize];
+ size.width += 10;
});
-
+
return (YGSize){
RCTYogaFloatFromCoreGraphicsFloat(size.width),
RCTYogaFloatFromCoreGraphicsFloat(size.height)
diff --git a/ios/fabric/RNDateTimePickerComponentView.mm b/ios/fabric/RNDateTimePickerComponentView.mm
index 4d2ac597..be1762d2 100644
--- a/ios/fabric/RNDateTimePickerComponentView.mm
+++ b/ios/fabric/RNDateTimePickerComponentView.mm
@@ -36,20 +36,20 @@ - (instancetype)initWithFrame:(CGRect)frame
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared();
_props = defaultProps;
-
+
_picker = [RNDateTimePicker new];
_dummyPicker = [RNDateTimePicker new];
-
+
[_picker addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged];
[_picker addTarget:self action:@selector(onDismiss:) forControlEvents:UIControlEventEditingDidEnd];
-
+
// Default Picker mode
_picker.datePickerMode = UIDatePickerModeDate;
_dummyPicker.datePickerMode = UIDatePickerModeDate;
-
+
self.contentView = _picker;
}
-
+
return self;
}
@@ -68,13 +68,13 @@ -(void)onChange:(RNDateTimePicker *)sender
if (!_eventEmitter) {
return;
}
-
+
NSTimeInterval timestamp = [sender.date timeIntervalSince1970];
RNDateTimePickerEventEmitter::OnChange event = {
// Sending time in milliseconds
.timestamp = timestamp * 1000
};
-
+
std::dynamic_pointer_cast(_eventEmitter)
->onChange(event);
}
@@ -135,7 +135,7 @@ -(void)updateTextColorForPicker:(UIDatePicker *)picker color:(UIColor *)color
*/
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
_state = std::static_pointer_cast(state);
-
+
if (oldState == nullptr) {
// Calculate the initial picker measurements
[self updateMeasurements];
@@ -149,24 +149,24 @@ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &
* Props that will to update measurements: date, locale, mode, displayIOS.
*/
- (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
-
+
const auto &oldPickerProps = *std::static_pointer_cast(_props);
const auto &newPickerProps = *std::static_pointer_cast(props);
Boolean needsToUpdateMeasurements = false;
-
+
if (oldPickerProps.date != newPickerProps.date) {
picker.date = convertJSTimeToDate(newPickerProps.date);
needsToUpdateMeasurements = true;
}
-
+
if (oldPickerProps.minimumDate != newPickerProps.minimumDate) {
picker.minimumDate = convertJSTimeToDate(newPickerProps.minimumDate);
}
-
+
if (oldPickerProps.maximumDate != newPickerProps.maximumDate) {
picker.maximumDate = convertJSTimeToDate(newPickerProps.maximumDate);
}
-
+
if (oldPickerProps.locale != newPickerProps.locale) {
NSString *convertedLocale = RCTNSStringFromString(newPickerProps.locale);
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:convertedLocale];
@@ -174,7 +174,7 @@ - (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared cons
picker.locale = locale;
needsToUpdateMeasurements = true;
}
-
+
if (oldPickerProps.mode != newPickerProps.mode) {
switch(newPickerProps.mode) {
case RNDateTimePickerMode::Time:
@@ -191,7 +191,7 @@ - (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared cons
}
needsToUpdateMeasurements = true;
}
-
+
if (@available(iOS 14.0, *)) {
if (oldPickerProps.displayIOS != newPickerProps.displayIOS) {
switch(newPickerProps.displayIOS) {
@@ -210,19 +210,36 @@ - (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared cons
needsToUpdateMeasurements = true;
}
}
-
+
if (oldPickerProps.minuteInterval != newPickerProps.minuteInterval) {
picker.minuteInterval = newPickerProps.minuteInterval;
}
-
+
if (oldPickerProps.timeZoneOffsetInMinutes != newPickerProps.timeZoneOffsetInMinutes) {
// JS standard for time zones is minutes.
picker.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:newPickerProps.timeZoneOffsetInMinutes * 60.0];
+ needsToUpdateMeasurements = true;
}
-
+
+ if (oldPickerProps.timeZoneName != newPickerProps.timeZoneName) {
+ NSString *timeZoneName = [NSString stringWithUTF8String:newPickerProps.timeZoneName.c_str()];
+ if ([@"" isEqualToString:timeZoneName]) {
+ picker.timeZone = NSTimeZone.localTimeZone;
+ } else {
+ NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:timeZoneName];
+ if (timeZone != nil) {
+ picker.timeZone = timeZone;
+ } else {
+ RCTLogWarn(@"'%@' does not exist in NSTimeZone.knownTimeZoneNames. Falling back to localTimeZone=%@", timeZoneName, NSTimeZone.localTimeZone.name);
+ picker.timeZone = NSTimeZone.localTimeZone;
+ }
+ }
+ needsToUpdateMeasurements = true;
+ }
+
if (oldPickerProps.accentColor != newPickerProps.accentColor) {
UIColor *color = RCTUIColorFromSharedColor(newPickerProps.accentColor);
-
+
if (color != nil) {
[picker setTintColor:color];
} else {
@@ -233,11 +250,11 @@ - (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared cons
}
}
}
-
+
if (oldPickerProps.textColor != newPickerProps.textColor) {
[self updateTextColorForPicker:picker color:RCTUIColorFromSharedColor(newPickerProps.textColor)];
}
-
+
if (@available(iOS 13.0, *)) {
if (oldPickerProps.themeVariant != newPickerProps.themeVariant) {
switch (newPickerProps.themeVariant) {
@@ -252,11 +269,11 @@ - (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared cons
}
}
}
-
+
if (oldPickerProps.enabled != newPickerProps.enabled) {
picker.enabled = newPickerProps.enabled;
}
-
+
return needsToUpdateMeasurements;
}
@@ -264,13 +281,13 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
{
// Updating the dummy first to check if we need to update measurements
Boolean needsToUpdateMeasurements = [self updatePropsForPicker:_dummyPicker props:props oldProps:oldProps];
-
+
if (needsToUpdateMeasurements) {
[self updateMeasurements];
}
-
+
[self updatePropsForPicker:_picker props:props oldProps:oldProps];
-
+
[super updateProps:props oldProps:oldProps];
}
diff --git a/jest/index.js b/jest/index.js
index 1e44a6df..78e2821c 100644
--- a/jest/index.js
+++ b/jest/index.js
@@ -12,19 +12,12 @@ export const mockAndroidDialogDateChange = (datePickedByUser: Date) => {
value: timestampFromPickerValueProp,
}) {
const pickedDate = new Date(timestampFromPickerValueProp);
- pickedDate.setFullYear(
- datePickedByUser.getFullYear(),
- datePickedByUser.getMonth(),
- datePickedByUser.getDate(),
- );
+ pickedDate.setTime(datePickedByUser.getTime());
return {
action: DATE_SET_ACTION,
- year: pickedDate.getFullYear(),
- month: pickedDate.getMonth(),
- day: pickedDate.getDate(),
- hour: pickedDate.getHours(),
- minute: pickedDate.getMinutes(),
+ timestamp: pickedDate.getTime(),
+ utcOffset: 0,
};
}
return (fakeDateTimePickerAndroidOpener: PresentPickerCallback);
diff --git a/package.json b/package.json
index 6b8e6af4..d14cdfb6 100644
--- a/package.json
+++ b/package.json
@@ -81,6 +81,7 @@
"jest": "^29.5.0",
"metro-react-native-babel-preset": "0.73.9",
"moment": "^2.24.0",
+ "moment-timezone": "^0.5.41",
"patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.8",
diff --git a/src/DateTimePickerAndroid.android.js b/src/DateTimePickerAndroid.android.js
index 9f720521..9b495114 100644
--- a/src/DateTimePickerAndroid.android.js
+++ b/src/DateTimePickerAndroid.android.js
@@ -13,11 +13,7 @@ import {
import invariant from 'invariant';
import type {AndroidNativeProps} from './types';
-import {
- getOpenPicker,
- timeZoneOffsetDateSetter,
- validateAndroidProps,
-} from './androidUtils';
+import {getOpenPicker, validateAndroidProps} from './androidUtils';
import pickers from './picker';
import {
createDateTimeSetEvtParams,
@@ -36,6 +32,7 @@ function open(props: AndroidNativeProps) {
maximumDate,
minuteInterval,
timeZoneOffsetInMinutes,
+ timeZoneName,
onChange,
onError,
positiveButton,
@@ -76,7 +73,7 @@ function open(props: AndroidNativeProps) {
display === ANDROID_DISPLAY.spinner
? ANDROID_DISPLAY.spinner
: ANDROID_DISPLAY.default;
- const {action, day, month, year, minute, hour} = await openPicker({
+ const {action, timestamp, utcOffset} = await openPicker({
value: valueTimestamp,
display: displayOverride,
is24Hour,
@@ -84,37 +81,28 @@ function open(props: AndroidNativeProps) {
maximumDate,
minuteInterval,
timeZoneOffsetInMinutes,
+ timeZoneName,
dialogButtons,
testID,
});
switch (action) {
- case DATE_SET_ACTION: {
- let date = new Date(valueTimestamp);
- date.setFullYear(year, month, day);
- date = timeZoneOffsetDateSetter(date, timeZoneOffsetInMinutes);
- const [event] = createDateTimeSetEvtParams(date);
- onChange?.(event, date);
- break;
- }
-
+ case DATE_SET_ACTION:
case TIME_SET_ACTION: {
- let date = new Date(valueTimestamp);
- date.setHours(hour, minute);
- date = timeZoneOffsetDateSetter(date, timeZoneOffsetInMinutes);
- const [event] = createDateTimeSetEvtParams(date);
+ const date = new Date(timestamp);
+ const [event] = createDateTimeSetEvtParams(date, utcOffset);
onChange?.(event, date);
break;
}
case NEUTRAL_BUTTON_ACTION: {
- const [event] = createNeutralEvtParams(originalValue);
+ const [event] = createNeutralEvtParams(originalValue, utcOffset);
onChange?.(event, originalValue);
break;
}
case DISMISS_ACTION:
default: {
- const [event] = createDismissEvtParams(originalValue);
+ const [event] = createDismissEvtParams(originalValue, utcOffset);
onChange?.(event, originalValue);
break;
}
diff --git a/src/androidUtils.js b/src/androidUtils.js
index 2e422507..9442cf6f 100644
--- a/src/androidUtils.js
+++ b/src/androidUtils.js
@@ -2,7 +2,7 @@
* @format
* @flow strict-local
*/
-import {ANDROID_DISPLAY, ANDROID_MODE, MIN_MS} from './constants';
+import {ANDROID_DISPLAY, ANDROID_MODE} from './constants';
import pickers from './picker';
import type {AndroidNativeProps, DateTimePickerResult} from './types';
import {sharedPropsValidation} from './utils';
@@ -24,6 +24,7 @@ type OpenParams = {
maximumDate: AndroidNativeProps['maximumDate'],
minuteInterval: AndroidNativeProps['minuteInterval'],
timeZoneOffsetInMinutes: AndroidNativeProps['timeZoneOffsetInMinutes'],
+ timeZoneName: AndroidNativeProps['timeZoneName'],
testID: AndroidNativeProps['testID'],
dialogButtons: {
positive: ProcessedButton,
@@ -46,6 +47,7 @@ function getOpenPicker(
is24Hour,
minuteInterval,
timeZoneOffsetInMinutes,
+ timeZoneName,
dialogButtons,
}: OpenParams) =>
// $FlowFixMe - `AbstractComponent` [1] is not an instance type.
@@ -55,6 +57,7 @@ function getOpenPicker(
minuteInterval,
is24Hour,
timeZoneOffsetInMinutes,
+ timeZoneName,
dialogButtons,
});
default:
@@ -64,6 +67,7 @@ function getOpenPicker(
minimumDate,
maximumDate,
timeZoneOffsetInMinutes,
+ timeZoneName,
dialogButtons,
testID,
}: OpenParams) =>
@@ -74,26 +78,13 @@ function getOpenPicker(
minimumDate,
maximumDate,
timeZoneOffsetInMinutes,
+ timeZoneName,
dialogButtons,
testID,
});
}
}
-function timeZoneOffsetDateSetter(
- date: Date,
- timeZoneOffsetInMinutes: ?number,
-): Date {
- if (typeof timeZoneOffsetInMinutes === 'number') {
- // FIXME this causes a bug. repro: set tz offset to zero, and then keep opening and closing the calendar picker
- // https://github.com/react-native-datetimepicker/datetimepicker/issues/528
- const offset = date.getTimezoneOffset() + timeZoneOffsetInMinutes;
- const shiftedDate = new Date(date.getTime() - offset * MIN_MS);
- return shiftedDate;
- }
- return date;
-}
-
function validateAndroidProps(props: AndroidNativeProps) {
sharedPropsValidation({value: props?.value});
const {mode, display} = props;
@@ -113,4 +104,4 @@ function validateAndroidProps(props: AndroidNativeProps) {
);
}
}
-export {getOpenPicker, timeZoneOffsetDateSetter, validateAndroidProps};
+export {getOpenPicker, validateAndroidProps};
diff --git a/src/datetimepicker.android.js b/src/datetimepicker.android.js
index 5fc83f19..c530d70e 100644
--- a/src/datetimepicker.android.js
+++ b/src/datetimepicker.android.js
@@ -24,6 +24,7 @@ export default function RNDateTimePickerAndroid(
minuteInterval,
onError,
timeZoneOffsetInMinutes,
+ timeZoneName,
positiveButton,
negativeButton,
neutralButton,
@@ -51,6 +52,7 @@ export default function RNDateTimePickerAndroid(
maximumDate,
minuteInterval,
timeZoneOffsetInMinutes,
+ timeZoneName,
onError,
onChange,
positiveButton,
diff --git a/src/datetimepicker.ios.js b/src/datetimepicker.ios.js
index af363e2b..e040136b 100644
--- a/src/datetimepicker.ios.js
+++ b/src/datetimepicker.ios.js
@@ -50,6 +50,7 @@ export default function Picker({
minimumDate,
minuteInterval,
timeZoneOffsetInMinutes,
+ timeZoneName,
textColor,
accentColor,
themeVariant,
@@ -59,7 +60,7 @@ export default function Picker({
disabled = false,
...other
}: IOSNativeProps): React.Node {
- sharedPropsValidation({value});
+ sharedPropsValidation({value, timeZoneOffsetInMinutes, timeZoneName});
const display = getDisplaySafe(providedDisplay);
@@ -83,6 +84,7 @@ export default function Picker({
type: EVENT_TYPE_DISMISSED,
nativeEvent: {
timestamp: value.getTime(),
+ utcOffset: 0, // TODO vonovak - the dismiss event should not carry any date information
},
},
value,
@@ -99,6 +101,7 @@ export default function Picker({
mode={mode}
minuteInterval={minuteInterval}
timeZoneOffsetInMinutes={timeZoneOffsetInMinutes}
+ timeZoneName={timeZoneName}
onChange={_onChange}
onPickerDismiss={onDismiss}
textColor={textColor}
diff --git a/src/datetimepicker.windows.js b/src/datetimepicker.windows.js
index 6bc29b22..3a0e7782 100644
--- a/src/datetimepicker.windows.js
+++ b/src/datetimepicker.windows.js
@@ -54,7 +54,11 @@ export default function RNDateTimePickerQWE(
const {onChange} = props;
const unifiedEvent: DateTimePickerEvent = {
...event,
- nativeEvent: {...event.nativeEvent, timestamp: event.nativeEvent.newDate},
+ nativeEvent: {
+ ...event.nativeEvent,
+ timestamp: event.nativeEvent.newDate,
+ utcOffset: 0,
+ },
type: EVENT_TYPE_SET,
};
diff --git a/src/eventCreators.js b/src/eventCreators.js
index 121ee0f2..cd16fbd9 100644
--- a/src/eventCreators.js
+++ b/src/eventCreators.js
@@ -6,12 +6,14 @@ import {ANDROID_EVT_TYPE, EVENT_TYPE_SET} from './constants';
export const createDateTimeSetEvtParams = (
date: Date,
+ utcOffset: number,
): [DateTimePickerEvent, Date] => {
return [
{
type: EVENT_TYPE_SET,
nativeEvent: {
timestamp: date.getTime(),
+ utcOffset,
},
},
date,
@@ -20,12 +22,14 @@ export const createDateTimeSetEvtParams = (
export const createDismissEvtParams = (
date: Date,
+ utcOffset: number,
): [DateTimePickerEvent, Date] => {
return [
{
type: ANDROID_EVT_TYPE.dismissed,
nativeEvent: {
timestamp: date.getTime(),
+ utcOffset,
},
},
date,
@@ -34,12 +38,14 @@ export const createDismissEvtParams = (
export const createNeutralEvtParams = (
date: Date,
+ utcOffset: number,
): [DateTimePickerEvent, Date] => {
return [
{
type: ANDROID_EVT_TYPE.neutralButtonPressed,
nativeEvent: {
timestamp: date.getTime(),
+ utcOffset,
},
},
date,
diff --git a/src/index.d.ts b/src/index.d.ts
index c421c3a1..850868b0 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -19,7 +19,8 @@ export type EvtTypes = 'set' | 'neutralButtonPressed' | 'dismissed';
export type DateTimePickerEvent = {
type: EvtTypes;
nativeEvent: {
- timestamp?: number;
+ timestamp: number;
+ utcOffset: number;
};
};
@@ -63,7 +64,15 @@ type TimeOptions = Readonly<
}
>;
-export type BaseProps = Readonly & DateOptions>;
+export type BaseProps = Readonly<
+ Omit &
+ DateOptions & {
+ /**
+ * The tz database name in https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+ */
+ timeZoneName?: string;
+ }
+>;
export type IOSNativeProps = Readonly<
BaseProps & {
diff --git a/src/specs/NativeComponentDateTimePicker.js b/src/specs/NativeComponentDateTimePicker.js
index ce28e22a..526dbc8f 100644
--- a/src/specs/NativeComponentDateTimePicker.js
+++ b/src/specs/NativeComponentDateTimePicker.js
@@ -13,6 +13,7 @@ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNati
type DateTimePickerEvent = $ReadOnly<{|
timestamp: Double,
+ utcOffset: Int32,
|}>;
type NativeProps = $ReadOnly<{|
@@ -26,6 +27,7 @@ type NativeProps = $ReadOnly<{|
minuteInterval?: ?Int32,
mode?: WithDefault<'date' | 'time' | 'datetime' | 'countdown', 'date'>,
timeZoneOffsetInMinutes?: ?Double,
+ timeZoneName?: ?string,
textColor?: ?ColorValue,
accentColor?: ?ColorValue,
themeVariant?: WithDefault<'dark' | 'light' | 'unspecified', 'unspecified'>,
diff --git a/src/types.js b/src/types.js
index aff89406..d2630ed3 100644
--- a/src/types.js
+++ b/src/types.js
@@ -29,13 +29,15 @@ type MinuteInterval = ?(1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 15 | 20 | 30);
export type NativeEventIOS = SyntheticEvent<
$ReadOnly<{|
timestamp: number,
+ utcOffset: number,
|}>,
>;
export type DateTimePickerEvent = {
type: AndroidEvtTypes,
nativeEvent: $ReadOnly<{
- timestamp?: number,
+ timestamp: number,
+ utcOffset: number,
...
}>,
...
@@ -92,6 +94,13 @@ type ViewPropsWithoutChildren = $Diff<
export type BaseProps = $ReadOnly<{|
...ViewPropsWithoutChildren,
...DateOptions,
+ /**
+ * Timezone in database name.
+ *
+ * By default, the date picker will use the device's timezone. With this
+ * parameter, it is possible to force a certain timezone based on IANA
+ */
+ timeZoneName?: ?string,
|}>;
export type IOSNativeProps = $ReadOnly<{|
@@ -175,6 +184,7 @@ export type AndroidNativeProps = $ReadOnly<{|
* instance, to show times in Pacific Standard Time, pass -7 * 60.
*/
timeZoneOffsetInMinutes?: ?number,
+
/**
* The interval at which minutes can be selected.
*/
@@ -211,11 +221,8 @@ export type TimePickerOptions = {|
export type DateTimePickerResult = $ReadOnly<{|
action: 'timeSetAction' | 'dateSetAction' | 'dismissedAction',
- year: number,
- month: number,
- day: number,
- hour: number,
- minute: number,
+ timestamp: number,
+ utcOffset: number,
|}>;
export type RCTDateTimePickerNative = Class>;
diff --git a/src/utils.js b/src/utils.js
index 4df40d25..a4dee188 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -30,10 +30,27 @@ export function dateToMilliseconds(date: ?Date): ?number {
return date.getTime();
}
-export function sharedPropsValidation({value}: {value: ?Date}) {
+export function sharedPropsValidation({
+ value,
+ timeZoneName,
+ timeZoneOffsetInMinutes,
+}: {
+ value: Date,
+ timeZoneName?: ?string,
+ timeZoneOffsetInMinutes?: ?number,
+}) {
invariant(value, 'A date or time must be specified as `value` prop');
invariant(
value instanceof Date,
'`value` prop must be an instance of Date object',
);
+ invariant(
+ timeZoneName == null || timeZoneOffsetInMinutes == null,
+ '`timeZoneName` and `timeZoneOffsetInMinutes` cannot be specified at the same time',
+ );
+ if (timeZoneOffsetInMinutes !== undefined) {
+ console.warn(
+ '`timeZoneOffsetInMinutes` is deprecated and will be removed in a future release. Use `timeZoneName` instead.',
+ );
+ }
}
diff --git a/test/userlandTestExamples.test.js b/test/userlandTestExamples.test.js
index 9b247a8c..ddcf26dd 100644
--- a/test/userlandTestExamples.test.js
+++ b/test/userlandTestExamples.test.js
@@ -77,7 +77,7 @@ describe('userland tests', () => {
fireEvent(
UNSAFE_getByType(DateTimePicker),
'onChange',
- ...createDateTimeSetEvtParams(date),
+ ...createDateTimeSetEvtParams(date, 0),
);
getByText('1560000000');
});
diff --git a/yarn.lock b/yarn.lock
index 8b0df076..2331f18b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7615,7 +7615,14 @@ module-details-from-path@^1.0.3:
resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b"
integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==
-moment@^2.19.3, moment@^2.24.0:
+moment-timezone@^0.5.41:
+ version "0.5.43"
+ resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790"
+ integrity sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==
+ dependencies:
+ moment "^2.29.4"
+
+moment@^2.19.3, moment@^2.24.0, moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==