Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: react-native-datetimepicker/datetimepicker
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: b11edd9f9068dddff31f6aa686eaf52bd09ab8c4
Choose a base ref
..
head repository: react-native-datetimepicker/datetimepicker
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 23708f4d3491d64551ef8fc34a7c17b49b25eb13
Choose a head ref
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# CHANGELOG

### 2.4.3

- Fix TS typings [#197](https://github.com/react-native-community/datetimepicker/pull/197)
- document working with dark mode [#204](https://github.com/react-native-community/datetimepicker/pull/204)

### 2.4.2

- Make react-native-windows optional [#191](https://github.com/react-native-community/datetimepicker/pull/191)

### 2.4.1

- allow compiling with xcode 10 [#186](https://github.com/react-native-community/datetimepicker/pull/186)

### 2.4.0

- Add Windows date picker [#157](https://github.com/react-native-community/datetimepicker/pull/157)
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ React Native date & time picker component for iOS and Android
- [`dayOfWeekFormat` (`optional`, `Windows only`)](#dayOfWeekFormat-optional-windows-only)
- [`dateFormat` (`optional`, `Windows only`)](#dateFormat-optional-windows-only)
- [`firstDayOfWeek` (`optional`, `Windows only`)](#firstDayOfWeek-optional-windows-only)
- [`textColor` (`optional`, `iOS only`)](#textColor-optional-ios-only)
- [`locale` (`optional`, `iOS only`)](#locale-optional-ios-only)
- [`is24Hour` (`optional`, `Android only`)](#is24hour-optional-android-only)
- [`neutralButtonLabel` (`optional`, `Android only`)](#neutralbuttonlabel-optional-android-only)
@@ -114,7 +115,7 @@ import React, {useState} from 'react';
import {View, Button, Platform} from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';

const App = () => {
export const App = () => {
const [date, setDate] = useState(new Date(1598051730000));
const [mode, setMode] = useState('date');
const [show, setShow] = useState(false);
@@ -159,15 +160,13 @@ const App = () => {
</View>
);
};

export default App;
```

## Props

> Please note that this library currently exposes functionality from [`UIDatePicker`](https://developer.apple.com/documentation/uikit/uidatepicker?language=objc) on iOS and [DatePickerDialog](https://developer.android.com/reference/android/app/DatePickerDialog) + [TimePickerDialog](https://developer.android.com/reference/android/app/TimePickerDialog) on Android, and [`CalendarDatePicker`](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/calendar-date-picker) on Windows.
>
> These native classes offer only limited configuration, while there are dozens of possible options you as a developer may need. It follows that if your requirement is not supported by the backing native views, this libray will _not_ be able to implement your requirement. When you open an issue with a feature request, please document if (or how) the feature can be implemented using the aforementioned native views. If those views do not support what you need, such feature requests will be closed as not actionable.
> These native classes offer only limited configuration, while there are dozens of possible options you as a developer may need. It follows that if your requirement is not supported by the backing native views, this library will _not_ be able to implement your requirement. When you open an issue with a feature request, please document if (or how) the feature can be implemented using the aforementioned native views. If those views do not support what you need, such feature requests will be closed as not actionable.
#### `mode` (`optional`)

@@ -190,7 +189,7 @@ Defines the visual display of the picker for Android and will be ignored for iOS

List of possible values:

- `"default"`
- `"default"` - Show a default date picker (spinner/calendar/clock) based on `mode` and android version.
- `"spinner"`
- `"calendar"` (only for `date` mode)
- `"clock"` (only for `time` mode)
@@ -324,8 +323,11 @@ Possible values are: `1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30`

#### `style` (`optional`, `iOS only`)

Sets style directly on picker component.
By default height of picker is fixed to 216px.
Sets style directly on picker component. By default, the picker height is fixed to 216px.

Please note that by default, picker's text color is controlled by the application theme (light / dark mode). In dark mode, text is white and in light mode, text is black.

This means that eg. if the device has dark mode turned on, and your screen background color is white, you will not see the picker. Please use the `Appearance` api to adjust the picker's background color so that it is visible, as we do in the [example App](/example/App.js) or [opt-out from dark mode](https://stackoverflow.com/a/56546554/2070942).

```js
<RNDateTimePicker style={{flex: 1}} />
@@ -668,7 +670,7 @@ Add `PackageProviders().Append(winrt::DateTimePicker::ReactPackageProvider());`

## Running the example app

1. Install required pods in `example/ios` by running `pods install`
1. Install required pods in `example/ios` by running `npx pod-install`
1. Run `npm start` to start Metro Bundler
1. Run `npm run start:ios` or `npm run start:android` or `npm run start:windows` (or `yarn run start:windows`)

2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ android {
buildToolsVersion safeExtGet('buildToolsVersion', '26.0.3')

defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 18)
minSdkVersion safeExtGet('minSdkVersion', 16)
targetSdkVersion safeExtGet('targetSdkVersion', 26)
versionCode 1
versionName "1.0"
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
import java.util.ArrayList;
import java.util.List;

import android.annotation.SuppressLint;
import android.app.TimePickerDialog;
import android.content.DialogInterface;
import android.content.Context;
@@ -54,43 +55,63 @@ public CustomTimePickerDialog(
mContext = context;
}

private boolean timePickerHasCustomMinuteInterval() {
return mTimePickerInterval != RNConstants.DEFAULT_TIME_PICKER_INTERVAL;
}

private boolean isSpinner() {
return mDisplay == RNTimePickerDisplay.SPINNER;
}

/**
* Converts values returned from picker to actual minutes
*
* @param minute the internal value of what the user had selected on picker
* @param minutesOrSpinnerIndex the internal value of what the user had selected
* @return returns 'real' minutes (0-59)
*/
private int getRealMinutes(int minute) {
private int getRealMinutes(int minutesOrSpinnerIndex) {
if (mDisplay == RNTimePickerDisplay.SPINNER) {
return minute * mTimePickerInterval;
return minutesOrSpinnerIndex * mTimePickerInterval;
}

return minute;
return minutesOrSpinnerIndex;
}

private int getRealMinutes() {
int minute = mTimePicker.getCurrentMinute();
return getRealMinutes(minute);
}

/**
* 'Snaps' real minutes to nearest valid value
* 'Snaps' real minutes or spinner value index to nearest valid value
* in spinner mode you need to make sure to transform the picked value (which is an index)
* to a real value before passing!
*
* @param realMinutes 'real' minutes (0-59)
* @return nearest valid value
* @return nearest valid real minute
*/
private int snapMinutesToInterval(int realMinutes) {
private int snapRealMinutesToInterval(int realMinutes) {
float stepsInMinutes = (float) realMinutes / (float) mTimePickerInterval;

if (mDisplay == RNTimePickerDisplay.SPINNER) {
return Math.round(stepsInMinutes);
}

return Math.round(stepsInMinutes) * mTimePickerInterval;
int rounded = Math.round(stepsInMinutes) * mTimePickerInterval;
return rounded == 60 ? rounded - mTimePickerInterval : rounded;
}

/**
* Determines if real minutes align setted minuteInterval
* Determines if picked real minutes are ok with the minuteInterval
*
* @param realMinutes 'real' minutes (0-59)
*/
private boolean minutesAreInvalid(int realMinutes) {
return realMinutes % mTimePickerInterval != 0;
private boolean minutesNeedCorrection(int realMinutes) {
assertNotSpinner("minutesNeedCorrection is not intended to be used with spinner, spinner won't allow picking invalid values");

return timePickerHasCustomMinuteInterval() && realMinutes != snapRealMinutesToInterval(realMinutes);
}

private void assertNotSpinner(String s) {
if (isSpinner()) {
throw new RuntimeException(s);
}
}

/**
@@ -106,86 +127,84 @@ private boolean pickerIsInTextInputMode() {
/**
* Corrects minute values if they don't align with minuteInterval
* <p>
* If the picker is in text input mode, correction will be postponed slightly to let the user
* finish the input
* in text input mode, correction will be postponed slightly to let the user finish the input
* in clock mode we also delay it to give user visual cue about the correction
* <p>
* If the picker is not in text input mode, correction will take place immidiately
*
* @param view the picker's view
* @param hourOfDay the picker's selected hours
* @param realMinutes 'real' minutes (0-59)
* @param correctedMinutes 'real' minutes (0-59) aligned to minute interval
*/
private void correctEnteredMinutes(final TimePicker view, final int hourOfDay, final int realMinutes) {
if (pickerIsInTextInputMode()) {
final EditText textInput = (EditText) view.findFocus();

// 'correction' callback
runnable = new Runnable() {
@Override
public void run() {
private void correctEnteredMinutes(final TimePicker view, final int hourOfDay, final int correctedMinutes) {
assertNotSpinner("spinner never needs to be corrected because wrong values are not offered to user (both in scrolling and textInput mode)!");
final EditText textInput = (EditText) view.findFocus();

// 'correction' callback
runnable = new Runnable() {
@Override
public void run() {
if (pickerIsInTextInputMode()) {
// set valid minutes && move caret to the end of input
view.setCurrentMinute(snapMinutesToInterval(realMinutes));
view.setCurrentHour(hourOfDay);
view.setCurrentMinute(correctedMinutes);
textInput.setSelection(textInput.getText().length());
} else {
view.setCurrentHour(hourOfDay);
// we need to set minutes to 0 for this to work on older android devices
view.setCurrentMinute(0);
view.setCurrentMinute(correctedMinutes);
}
};

handler.postDelayed(runnable, 500);
return;
}
}
};

view.setCurrentMinute(snapMinutesToInterval(realMinutes));
view.setCurrentHour(hourOfDay);
handler.postDelayed(runnable, 500);
}

@Override
public void onTimeChanged(final TimePicker view, final int hourOfDay, final int minute) {
final int realMinutes = getRealMinutes(minute);

// remove pending 'validation' callbacks if any
// *always* remove pending 'validation' callbacks, otherwise a valid value might be rewritten
handler.removeCallbacks(runnable);

if (minutesAreInvalid(realMinutes)) {
correctEnteredMinutes(view, hourOfDay, realMinutes);
return;
}
if (!isSpinner() && minutesNeedCorrection(realMinutes)) {
int correctedMinutes = snapRealMinutesToInterval(realMinutes);

super.onTimeChanged(view, hourOfDay, realMinutes);
// will fire another onTimeChanged
correctEnteredMinutes(view, hourOfDay, correctedMinutes);
} else {
super.onTimeChanged(view, hourOfDay, minute);
}
}

@Override
public void onClick(DialogInterface dialog, int which) {
if (mTimePickerInterval == RNConstants.DEFAULT_TIME_PICKER_INTERVAL) {
super.onClick(dialog, which);
} else {
switch (which) {
case BUTTON_POSITIVE:
final int hours = mTimePicker.getCurrentHour();
int minutes = mTimePicker.getCurrentMinute();

if (mTimePickerInterval > RNConstants.DEFAULT_TIME_PICKER_INTERVAL) {
final int realMinutes = getRealMinutes(minutes);
minutes = realMinutes;

if (pickerIsInTextInputMode() && minutesAreInvalid(realMinutes)) {
minutes = snapMinutesToInterval(realMinutes);
}
}

if (mTimeSetListener != null) {
mTimeSetListener.onTimeSet(mTimePicker, hours, minutes);
}
break;
case BUTTON_NEGATIVE:
cancel();
break;
if (mTimePicker != null && which == BUTTON_POSITIVE && timePickerHasCustomMinuteInterval()) {
final int hours = mTimePicker.getCurrentHour();

final int realMinutes = getRealMinutes();
int validMinutes = isSpinner() ? realMinutes : snapRealMinutesToInterval(realMinutes);

if (mTimeSetListener != null) {
mTimeSetListener.onTimeSet(mTimePicker, hours, validMinutes);
}
} else {
super.onClick(dialog, which);
}
}

@Override
public void updateTime(int hourOfDay, int minuteOfHour) {
mTimePicker.setCurrentMinute(snapMinutesToInterval(minuteOfHour));
if (timePickerHasCustomMinuteInterval()) {
if (isSpinner()) {
final int realMinutes = getRealMinutes();
int selectedIndex = snapRealMinutesToInterval(realMinutes) / mTimePickerInterval;
super.updateTime(hourOfDay, selectedIndex);
} else {
super.updateTime(hourOfDay, snapRealMinutesToInterval(minuteOfHour));
}
} else {
super.updateTime(hourOfDay, minuteOfHour);
}
}

/**
@@ -196,28 +215,42 @@ public void updateTime(int hourOfDay, int minuteOfHour) {
public void onAttachedToWindow() {
super.onAttachedToWindow();

if (mTimePickerInterval > RNConstants.DEFAULT_TIME_PICKER_INTERVAL) {
if (timePickerHasCustomMinuteInterval()) {
int timePickerId = mContext.getResources().getIdentifier("timePicker", "id", "android");

mTimePicker = this.findViewById(timePickerId);
int currentMinute = mTimePicker.getCurrentMinute();

if (mDisplay == RNTimePickerDisplay.SPINNER) {
int minutePickerId = mContext.getResources().getIdentifier("minute", "id", "android");
NumberPicker minutePicker = this.findViewById(minutePickerId);
int realMinuteBackup = mTimePicker.getCurrentMinute();

minutePicker.setMinValue(0);
minutePicker.setMaxValue((60 / mTimePickerInterval) - 1);
if (isSpinner()) {
setSpinnerDisplayedValues();
int selectedIndex = snapRealMinutesToInterval(realMinuteBackup) / mTimePickerInterval;
mTimePicker.setCurrentMinute(selectedIndex);
} else {
int snappedRealMinute = snapRealMinutesToInterval(realMinuteBackup);
mTimePicker.setCurrentMinute(snappedRealMinute);
}
}
}

List<String> displayedValues = new ArrayList<>();
for (int i = 0; i < 60; i += mTimePickerInterval) {
displayedValues.add(String.format("%02d", i));
}
@SuppressLint("DefaultLocale")
private void setSpinnerDisplayedValues() {
int minutePickerId = mContext.getResources().getIdentifier("minute", "id", "android");
NumberPicker minutePicker = this.findViewById(minutePickerId);

minutePicker.setDisplayedValues(displayedValues.toArray(new String[0]));
}
minutePicker.setMinValue(0);
minutePicker.setMaxValue((60 / mTimePickerInterval) - 1);

mTimePicker.setCurrentMinute(snapMinutesToInterval(currentMinute));
List<String> displayedValues = new ArrayList<>(60 / mTimePickerInterval);
for (int displayedMinute = 0; displayedMinute < 60; displayedMinute += mTimePickerInterval) {
displayedValues.add(String.format("%02d", displayedMinute));
}

minutePicker.setDisplayedValues(displayedValues.toArray(new String[0]));
}

@Override
public void onDetachedFromWindow() {
handler.removeCallbacks(runnable);
super.onDetachedFromWindow();
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package com.reactcommunity.rndatetimepicker;

import java.util.Arrays;

/**
* Time picker minutes' intervals.
*/
public final class RNMinuteIntervals {
private final static Integer[] MinuteIntervals = new Integer[]{1, 5, 10, 15, 20, 30};

public static boolean isValid(Integer interval) {
return Arrays.asList(MinuteIntervals).contains(interval);
public static boolean isValid(int interval) {
return 60 % interval == 0 && interval <= 30;
}
}
Loading