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 ( + +