Skip to content

feat: add timeZoneName props #744

New issue

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

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

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading