Skip to content

Commit

Permalink
feat: add timeZoneName prop (#744)
Browse files Browse the repository at this point in the history
* feat: add timeZoneName props

* code review

* refactor: e2e review

* refactor: fix flow

* docs: readme

* docs: readme

* fix: tz prop handling

---------

Co-authored-by: Vojtech Novak <vonovak@gmail.com>
  • Loading branch information
wood1986 and vonovak committed Aug 29, 2023
1 parent d81632b commit d136216
Show file tree
Hide file tree
Showing 34 changed files with 809 additions and 565 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,5 @@ buck-out/

# CocoaPods
example/ios/Pods/

.xcode.env
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -344,10 +347,21 @@ Defines the minimum date that can be selected. Note that on Android, this only w
<RNDateTimePicker minimumDate={new Date(1950, 0, 1)} />
```

#### `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
<RNDateTimePicker timeZoneName={'Europe/Prague'} />
```

#### `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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
};
}

Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -41,8 +39,9 @@ public DatePickerModule(ReactApplicationContext reactContext) {
super(reactContext);
}

@NonNull
@Override
public @NonNull String getName() {
public String getName() {
return NAME;
}

Expand All @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down

0 comments on commit d136216

Please sign in to comment.