Skip to content

Commit d136216

Browse files
wood1986vonovak
andauthored
feat: add timeZoneName prop (#744)
* 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 <[email protected]>
1 parent d81632b commit d136216

34 files changed

+809
-565
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,5 @@ buck-out/
5959

6060
# CocoaPods
6161
example/ios/Pods/
62+
63+
.xcode.env

README.md

+17-3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ React Native date & time picker component for iOS, Android and Windows.
6666
- [`value` (`required`)](#value-required)
6767
- [`maximumDate` (`optional`)](#maximumdate-optional)
6868
- [`minimumDate` (`optional`)](#minimumdate-optional)
69+
- [`timeZoneName` (`optional`, `iOS or Android only`)](#timeZoneName-optional-ios-and-android-only)
6970
- [`timeZoneOffsetInMinutes` (`optional`, `iOS or Android only`)](#timezoneoffsetinminutes-optional-ios-and-android-only)
7071
- [`timeZoneOffsetInSeconds` (`optional`, `Windows only`)](#timezoneoffsetinsecond-optional-windows-only)
7172
- [`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
309310
It is also called when user dismisses the picker, which you can detect by checking the `event.type` property.
310311
The values can be: `'set' | 'dismissed' | 'neutralButtonPressed'`. (`neutralButtonPressed` is only available on Android).
311312

313+
The `utcOffset` field is only available on Android and iOS. It is the offset in minutes between the selected date and UTC time.
314+
312315
```js
313316
const setDate = (event: DateTimePickerEvent, date: Date) => {
314317
const {
315318
type,
316-
nativeEvent: {timestamp},
319+
nativeEvent: {timestamp, utcOffset},
317320
} = event;
318321
};
319322

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

350+
#### `timeZoneName` (`optional`, `iOS and Android only`)
351+
352+
Allows changing of the time zone of the date picker. By default, it uses the device's time zone.
353+
Use the time zone name from the IANA (TZDB) database name in https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
354+
355+
```js
356+
<RNDateTimePicker timeZoneName={'Europe/Prague'} />
357+
```
358+
347359
#### `timeZoneOffsetInMinutes` (`optional`, `iOS and Android only`)
348360

349-
Allows changing of the timeZone of the date picker. By default, it uses the device's time zone.
350-
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)).
361+
Allows changing of the time zone of the date picker. By default, it uses the device's time zone.
362+
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)).
363+
364+
This prop will be removed in a future release.
351365

352366
```js
353367
// GMT+1

android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java

+78-15
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@
1818
import androidx.fragment.app.FragmentManager;
1919

2020
import com.facebook.react.bridge.Promise;
21+
import com.facebook.react.bridge.ReadableMap;
22+
import com.facebook.react.util.RNLog;
2123

24+
import java.util.Arrays;
25+
import java.util.Calendar;
26+
import java.util.HashSet;
2227
import java.util.Locale;
28+
import java.util.SimpleTimeZone;
29+
import java.util.TimeZone;
2330

2431
public class Common {
2532

@@ -63,21 +70,18 @@ public static int getDefaultDialogButtonTextColor(@NonNull Context activity) {
6370

6471
@NonNull
6572
public static DialogInterface.OnShowListener setButtonTextColor(@NonNull final Context activityContext, final AlertDialog dialog, final Bundle args, final boolean needsColorOverride) {
66-
return new DialogInterface.OnShowListener() {
67-
@Override
68-
public void onShow(DialogInterface dialogInterface) {
69-
// change text color only if custom color is set or if spinner mode is set
70-
// because spinner suffers from https://github.com/react-native-datetimepicker/datetimepicker/issues/543
71-
72-
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
73-
Button negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
74-
Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
75-
76-
int textColorPrimary = getDefaultDialogButtonTextColor(activityContext);
77-
setTextColor(positiveButton, POSITIVE, args, needsColorOverride, textColorPrimary);
78-
setTextColor(negativeButton, NEGATIVE, args, needsColorOverride, textColorPrimary);
79-
setTextColor(neutralButton, NEUTRAL, args, needsColorOverride, textColorPrimary);
80-
}
73+
return dialogInterface -> {
74+
// change text color only if custom color is set or if spinner mode is set
75+
// because spinner suffers from https://github.com/react-native-datetimepicker/datetimepicker/issues/543
76+
77+
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
78+
Button negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
79+
Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
80+
81+
int textColorPrimary = getDefaultDialogButtonTextColor(activityContext);
82+
setTextColor(positiveButton, POSITIVE, args, needsColorOverride, textColorPrimary);
83+
setTextColor(negativeButton, NEGATIVE, args, needsColorOverride, textColorPrimary);
84+
setTextColor(neutralButton, NEUTRAL, args, needsColorOverride, textColorPrimary);
8185
};
8286
}
8387

@@ -139,4 +143,63 @@ private static void setButtonLabel(Bundle buttonConfig, AlertDialog dialog, int
139143
}
140144
dialog.setButton(whichButton, buttonConfig.getString(LABEL), listener);
141145
}
146+
147+
public static TimeZone getTimeZone(Bundle args) {
148+
if (args != null && args.containsKey(RNConstants.ARG_TZOFFSET_MINS)) {
149+
return new SimpleTimeZone((int)args.getLong(RNConstants.ARG_TZOFFSET_MINS) * 60 * 1000, "GMT");
150+
}
151+
152+
if (args != null && args.containsKey(RNConstants.ARG_TZ_NAME)) {
153+
String timeZoneName = args.getString(RNConstants.ARG_TZ_NAME);
154+
if ("GMT".equals(timeZoneName)) {
155+
return TimeZone.getTimeZone("GMT");
156+
} else if (!"GMT".equals(TimeZone.getTimeZone(timeZoneName).getID())) {
157+
return TimeZone.getTimeZone(timeZoneName);
158+
}
159+
RNLog.w(null, "'" + timeZoneName + "' does not exist in TimeZone.getAvailableIDs(). Falling back to TimeZone.getDefault()=" + TimeZone.getDefault().getID());
160+
}
161+
162+
return TimeZone.getDefault();
163+
}
164+
165+
public static long maxDateWithTimeZone(Bundle args) {
166+
if (!args.containsKey(RNConstants.ARG_MAXDATE)) {
167+
return Long.MAX_VALUE;
168+
}
169+
170+
Calendar maxDate = Calendar.getInstance(getTimeZone(args));
171+
maxDate.setTimeInMillis(args.getLong(RNConstants.ARG_MAXDATE));
172+
maxDate.set(Calendar.HOUR_OF_DAY, 23);
173+
maxDate.set(Calendar.MINUTE, 59);
174+
maxDate.set(Calendar.SECOND, 59);
175+
maxDate.set(Calendar.MILLISECOND, 999);
176+
return maxDate.getTimeInMillis();
177+
}
178+
179+
public static long minDateWithTimeZone(Bundle args) {
180+
if (!args.containsKey(RNConstants.ARG_MINDATE)) {
181+
return 0;
182+
}
183+
184+
Calendar minDate = Calendar.getInstance(getTimeZone(args));
185+
minDate.setTimeInMillis(args.getLong(RNConstants.ARG_MINDATE));
186+
minDate.set(Calendar.HOUR_OF_DAY, 0);
187+
minDate.set(Calendar.MINUTE, 0);
188+
minDate.set(Calendar.SECOND, 0);
189+
minDate.set(Calendar.MILLISECOND, 0);
190+
return minDate.getTimeInMillis();
191+
}
192+
193+
public static Bundle createFragmentArguments(ReadableMap options) {
194+
final Bundle args = new Bundle();
195+
196+
if (options.hasKey(RNConstants.ARG_VALUE) && !options.isNull(RNConstants.ARG_VALUE)) {
197+
args.putLong(RNConstants.ARG_VALUE, (long) options.getDouble(RNConstants.ARG_VALUE));
198+
}
199+
if (options.hasKey(RNConstants.ARG_TZ_NAME) && !options.isNull(RNConstants.ARG_TZ_NAME)) {
200+
args.putString(RNConstants.ARG_TZ_NAME, options.getString(RNConstants.ARG_TZ_NAME));
201+
}
202+
203+
return args;
204+
}
142205
}

android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java

+27-47
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@
2222
import com.facebook.react.module.annotations.ReactModule;
2323

2424
import static com.reactcommunity.rndatetimepicker.Common.dismissDialog;
25-
import static com.reactcommunity.rndatetimepicker.KeepDateInRangeListener.isDateAfterMaxDate;
26-
import static com.reactcommunity.rndatetimepicker.KeepDateInRangeListener.isDateBeforeMinDate;
2725

2826
import java.util.Calendar;
2927

@@ -41,8 +39,9 @@ public DatePickerModule(ReactApplicationContext reactContext) {
4139
super(reactContext);
4240
}
4341

42+
@NonNull
4443
@Override
45-
public @NonNull String getName() {
44+
public String getName() {
4645
return NAME;
4746
}
4847

@@ -60,31 +59,15 @@ public DatePickerDialogListener(final Promise promise, Bundle arguments) {
6059
@Override
6160
public void onDateSet(DatePicker view, int year, int month, int day) {
6261
if (!mPromiseResolved && getReactApplicationContext().hasActiveReactInstance()) {
62+
final RNDate date = new RNDate(mArgs);
63+
Calendar calendar = Calendar.getInstance(Common.getTimeZone(mArgs));
64+
calendar.set(year, month, day, date.hour(), date.minute(), 0);
65+
calendar.set(Calendar.MILLISECOND, 0);
66+
6367
WritableMap result = new WritableNativeMap();
6468
result.putString("action", RNConstants.ACTION_DATE_SET);
65-
result.putInt("year", year);
66-
result.putInt("month", month);
67-
result.putInt("day", day);
68-
69-
// https://issuetracker.google.com/issues/169602180
70-
// TODO revisit day, month, year with timezoneoffset fixes
71-
if (isDateAfterMaxDate(mArgs, year, month, day)) {
72-
Calendar maxDate = Calendar.getInstance();
73-
maxDate.setTimeInMillis(mArgs.getLong(RNConstants.ARG_MAXDATE));
74-
75-
result.putInt("year", maxDate.get(Calendar.YEAR));
76-
result.putInt("month", maxDate.get(Calendar.MONTH) );
77-
result.putInt("day", maxDate.get(Calendar.DAY_OF_MONTH));
78-
}
79-
80-
if (isDateBeforeMinDate(mArgs, year, month, day)) {
81-
Calendar minDate = Calendar.getInstance();
82-
minDate.setTimeInMillis(mArgs.getLong(RNConstants.ARG_MINDATE));
83-
84-
result.putInt("year", minDate.get(Calendar.YEAR));
85-
result.putInt("month", minDate.get(Calendar.MONTH) );
86-
result.putInt("day", minDate.get(Calendar.DAY_OF_MONTH));
87-
}
69+
result.putDouble("timestamp", calendar.getTimeInMillis());
70+
result.putDouble("utcOffset", calendar.getTimeZone().getOffset(calendar.getTimeInMillis()) / 1000 / 60);
8871

8972
mPromise.resolve(result);
9073
mPromiseResolved = true;
@@ -157,35 +140,32 @@ public void open(final ReadableMap options, final Promise promise) {
157140

158141
final FragmentManager fragmentManager = activity.getSupportFragmentManager();
159142

160-
UiThreadUtil.runOnUiThread(new Runnable() {
161-
@Override
162-
public void run() {
163-
RNDatePickerDialogFragment oldFragment =
164-
(RNDatePickerDialogFragment) fragmentManager.findFragmentByTag(NAME);
143+
UiThreadUtil.runOnUiThread(() -> {
144+
RNDatePickerDialogFragment oldFragment =
145+
(RNDatePickerDialogFragment) fragmentManager.findFragmentByTag(NAME);
165146

166-
if (oldFragment != null) {
167-
oldFragment.update(createFragmentArguments(options));
168-
return;
169-
}
147+
Bundle arguments = createFragmentArguments(options);
170148

171-
RNDatePickerDialogFragment fragment = new RNDatePickerDialogFragment();
149+
if (oldFragment != null) {
150+
oldFragment.update(arguments);
151+
return;
152+
}
172153

173-
fragment.setArguments(createFragmentArguments(options));
154+
RNDatePickerDialogFragment fragment = new RNDatePickerDialogFragment();
174155

175-
final DatePickerDialogListener listener = new DatePickerDialogListener(promise, createFragmentArguments(options));
176-
fragment.setOnDismissListener(listener);
177-
fragment.setOnDateSetListener(listener);
178-
fragment.setOnNeutralButtonActionListener(listener);
179-
fragment.show(fragmentManager, NAME);
180-
}
156+
fragment.setArguments(arguments);
157+
158+
final DatePickerDialogListener listener = new DatePickerDialogListener(promise, arguments);
159+
fragment.setOnDismissListener(listener);
160+
fragment.setOnDateSetListener(listener);
161+
fragment.setOnNeutralButtonActionListener(listener);
162+
fragment.show(fragmentManager, NAME);
181163
});
182164
}
183165

184166
private Bundle createFragmentArguments(ReadableMap options) {
185-
final Bundle args = new Bundle();
186-
if (options.hasKey(RNConstants.ARG_VALUE) && !options.isNull(RNConstants.ARG_VALUE)) {
187-
args.putLong(RNConstants.ARG_VALUE, (long) options.getDouble(RNConstants.ARG_VALUE));
188-
}
167+
final Bundle args = Common.createFragmentArguments(options);
168+
189169
if (options.hasKey(RNConstants.ARG_MINDATE) && !options.isNull(RNConstants.ARG_MINDATE)) {
190170
args.putLong(RNConstants.ARG_MINDATE, (long) options.getDouble(RNConstants.ARG_MINDATE));
191171
}

android/src/main/java/com/reactcommunity/rndatetimepicker/KeepDateInRangeListener.java

-65
This file was deleted.

android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public final class RNConstants {
1212
public static final String ARG_DISPLAY = "display";
1313
public static final String ARG_DIALOG_BUTTONS = "dialogButtons";
1414
public static final String ARG_TZOFFSET_MINS = "timeZoneOffsetInMinutes";
15+
public static final String ARG_TZ_NAME = "timeZoneName";
1516
public static final String ARG_TESTID = "testID";
1617
public static final String ACTION_DATE_SET = "dateSetAction";
1718
public static final String ACTION_TIME_SET = "timeSetAction";

android/src/main/java/com/reactcommunity/rndatetimepicker/RNDate.java

+2-10
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,11 @@ public RNDate(Bundle args) {
1111
now = Calendar.getInstance();
1212

1313
if (args != null && args.containsKey(RNConstants.ARG_VALUE)) {
14-
set(args.getLong(RNConstants.ARG_VALUE));
14+
now.setTimeInMillis((args.getLong(RNConstants.ARG_VALUE)));
1515
}
1616

17-
if (args != null && args.containsKey(RNConstants.ARG_TZOFFSET_MINS)) {
18-
now.setTimeZone(TimeZone.getTimeZone("GMT"));
19-
Long timeZoneOffsetInMinutesFallback = args.getLong(RNConstants.ARG_TZOFFSET_MINS);
20-
Integer timeZoneOffsetInMinutes = args.getInt(RNConstants.ARG_TZOFFSET_MINS, timeZoneOffsetInMinutesFallback.intValue());
21-
now.add(Calendar.MILLISECOND, timeZoneOffsetInMinutes * 60000);
22-
}
23-
}
2417

25-
public void set(long value) {
26-
now.setTimeInMillis(value);
18+
now.setTimeZone(Common.getTimeZone(args));
2719
}
2820

2921
public int year() { return now.get(Calendar.YEAR); }

0 commit comments

Comments
 (0)