diff --git a/README.md b/README.md
index 19f7d7c9..9a6542cc 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,10 @@ React Native date & time picker component for iOS, Android and Windows (please n

|

|
+
+ 
|
+ 
|
+
Windows |

|
@@ -77,6 +81,10 @@ React Native date & time picker component for iOS, Android and Windows (please n
- [Props / params](#component-props--params-of-the-android-imperative-api)
- [`mode` (`optional`)](#mode-optional)
- [`display` (`optional`)](#display-optional)
+ - [`design` (`optional`, `Android only`)](#design-optional)
+ - [`initialInputMode` (`optional`, `Android only`)](#initialinputmode-optional-android-only)
+ - [`title` (`optional`, `Android only`)](#title-optional-android-only)
+ - [`fullscreen` (`optional`, `Android only`)](#fullscreen-optional-android-only)
- [`onChange` (`optional`)](#onchange-optional)
- [`value` (`required`)](#value-required)
- [`maximumDate` (`optional`)](#maximumdate-optional)
@@ -287,6 +295,8 @@ The reason we recommend the imperative API is: on Android, the date/time picker
### Android styling
+If you'd like to use the Material pickers, your app theme will need to inherit from `Theme.Material3.DayNight.NoActionBar` in `styles.xml`.
+
Styling of the dialogs on Android can be easily customized by using the provided config plugin, provided that you use a [Expo development build](https://docs.expo.dev/develop/development-builds/introduction/). The plugin allows you to configure color properties that cannot be set at runtime and requires building a new app binary to take effect.
Refer to this documentation for more information: [android-styling.md](/docs/android-styling.md).
@@ -334,6 +344,19 @@ List of possible values for iOS (maps to [preferredDatePickerStyle](https://deve
```
+#### `design` (`optional`, `Android only`)
+
+Defines if the picker should use Material 3 components or the default picker. The default value is `"default"`.
+
+List of possible values
+
+- `"default"`
+- `"material"`
+
+```js
+
+```
+
#### `onChange` (`optional`)
Date change handler.
@@ -482,6 +505,35 @@ Allows changing of the time picker to a 24-hour format. By default, this value i
```
+#### `initialInputMode` (`optional`, `Android only`)
+
+:warning: Has effect only when `design` is "material". Allows setting the initial input mode of the picker.
+
+List of possible values:
+
+- `"default"` - Recommended. Date pickers will show the calendar view by default, and time pickers will show the clock view by default.
+- `"keyboard"` - Both pickers will show an input where the user can type the date or time.
+
+```js
+
+```
+
+#### `title` (`optional`, `Android only`)
+
+:warning: Has effect only when `design` is "material". Allows setting the title of the dialog for the pickers.
+
+```js
+
+```
+
+#### `fullscreen` (`optional`, `Android only`)
+
+:warning: Has effect only when `design` is "material". Allows setting the date picker dialog to be fullscreen.
+
+```js
+
+```
+
#### `positiveButton` (`optional`, `Android only`)
Set the positive button label and text color.
diff --git a/android/build.gradle b/android/build.gradle
index 20cc6954..af810e8e 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -18,6 +18,7 @@ def isNewArchitectureEnabled() {
}
apply plugin: 'com.android.library'
+apply plugin: 'org.jetbrains.kotlin.android'
if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
}
@@ -53,6 +54,9 @@ android {
}
}
}
+ kotlinOptions {
+ jvmTarget = '17'
+ }
}
repositories {
@@ -64,4 +68,6 @@ repositories {
dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+'
+ implementation 'com.google.android.material:material:1.12.0'
+ implementation 'androidx.core:core-ktx:1.13.1'
}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java
index 0bfc4c21..ca6b2e06 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java
@@ -17,6 +17,7 @@
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
+import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.util.RNLog;
@@ -205,6 +206,66 @@ public static Bundle createFragmentArguments(ReadableMap options) {
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));
}
+ if (options.hasKey(RNConstants.ARG_TITLE) && !options.isNull(RNConstants.ARG_TITLE)) {
+ args.putString(RNConstants.ARG_TITLE, options.getString(RNConstants.ARG_TITLE));
+ }
+ if (options.hasKey(RNConstants.ARG_INITIAL_INPUT_MODE) && !options.isNull(RNConstants.ARG_INITIAL_INPUT_MODE)) {
+ args.putString(RNConstants.ARG_INITIAL_INPUT_MODE, options.getString(RNConstants.ARG_INITIAL_INPUT_MODE));
+ }
+
+ return args;
+ }
+
+ public static Bundle createDatePickerArguments(ReadableMap options) {
+ 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));
+ }
+ if (options.hasKey(RNConstants.ARG_MAXDATE) && !options.isNull(RNConstants.ARG_MAXDATE)) {
+ args.putLong(RNConstants.ARG_MAXDATE, (long) options.getDouble(RNConstants.ARG_MAXDATE));
+ }
+ if (options.hasKey(RNConstants.ARG_DISPLAY) && !options.isNull(RNConstants.ARG_DISPLAY)) {
+ args.putString(RNConstants.ARG_DISPLAY, options.getString(RNConstants.ARG_DISPLAY));
+ }
+ if (options.hasKey(RNConstants.ARG_DIALOG_BUTTONS) && !options.isNull(RNConstants.ARG_DIALOG_BUTTONS)) {
+ args.putBundle(RNConstants.ARG_DIALOG_BUTTONS, Arguments.toBundle(options.getMap(RNConstants.ARG_DIALOG_BUTTONS)));
+ }
+ if (options.hasKey(RNConstants.ARG_TZOFFSET_MINS) && !options.isNull(RNConstants.ARG_TZOFFSET_MINS)) {
+ args.putLong(RNConstants.ARG_TZOFFSET_MINS, (long) options.getDouble(RNConstants.ARG_TZOFFSET_MINS));
+ }
+ if (options.hasKey(RNConstants.ARG_TESTID) && !options.isNull(RNConstants.ARG_TESTID)) {
+ args.putString(RNConstants.ARG_TESTID, options.getString(RNConstants.ARG_TESTID));
+ }
+ if (options.hasKey(RNConstants.ARG_FULLSCREEN) && !options.isNull(RNConstants.ARG_FULLSCREEN)) {
+ args.putBoolean(RNConstants.ARG_FULLSCREEN, options.getBoolean(RNConstants.ARG_FULLSCREEN));
+ }
+ if (options.hasKey(RNConstants.FIRST_DAY_OF_WEEK) && !options.isNull(RNConstants.FIRST_DAY_OF_WEEK)) {
+ // FIRST_DAY_OF_WEEK is 0-indexed, since it uses the same constants DAY_OF_WEEK used in the Windows implementation
+ // Android DatePicker uses 1-indexed values, SUNDAY being 1 and SATURDAY being 7, so the +1 is necessary in this case
+ args.putInt(RNConstants.FIRST_DAY_OF_WEEK, options.getInt(RNConstants.FIRST_DAY_OF_WEEK)+1);
+ }
+ return args;
+ }
+
+ public static Bundle createTimePickerArguments(ReadableMap options) {
+ 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));
+ }
+ if (options.hasKey(RNConstants.ARG_DISPLAY) && !options.isNull(RNConstants.ARG_DISPLAY)) {
+ args.putString(RNConstants.ARG_DISPLAY, options.getString(RNConstants.ARG_DISPLAY));
+ }
+ if (options.hasKey(RNConstants.ARG_DIALOG_BUTTONS) && !options.isNull(RNConstants.ARG_DIALOG_BUTTONS)) {
+ args.putBundle(RNConstants.ARG_DIALOG_BUTTONS, Arguments.toBundle(options.getMap(RNConstants.ARG_DIALOG_BUTTONS)));
+ }
+ if (options.hasKey(RNConstants.ARG_INTERVAL) && !options.isNull(RNConstants.ARG_INTERVAL)) {
+ args.putInt(RNConstants.ARG_INTERVAL, options.getInt(RNConstants.ARG_INTERVAL));
+ }
+ if (options.hasKey(RNConstants.ARG_TZOFFSET_MINS) && !options.isNull(RNConstants.ARG_TZOFFSET_MINS)) {
+ args.putLong(RNConstants.ARG_TZOFFSET_MINS, (long) options.getDouble(RNConstants.ARG_TZOFFSET_MINS));
+ }
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 6eec370b..d043e4f9 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java
@@ -21,6 +21,7 @@
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.module.annotations.ReactModule;
+import static com.reactcommunity.rndatetimepicker.Common.createDatePickerArguments;
import static com.reactcommunity.rndatetimepicker.Common.dismissDialog;
import java.util.Calendar;
@@ -144,7 +145,7 @@ public void open(final ReadableMap options, final Promise promise) {
RNDatePickerDialogFragment oldFragment =
(RNDatePickerDialogFragment) fragmentManager.findFragmentByTag(NAME);
- Bundle arguments = createFragmentArguments(options);
+ Bundle arguments = createDatePickerArguments(options);
if (oldFragment != null) {
oldFragment.update(arguments);
@@ -162,33 +163,4 @@ public void open(final ReadableMap options, final Promise promise) {
fragment.show(fragmentManager, NAME);
});
}
-
- private Bundle createFragmentArguments(ReadableMap options) {
- 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));
- }
- if (options.hasKey(RNConstants.ARG_MAXDATE) && !options.isNull(RNConstants.ARG_MAXDATE)) {
- args.putLong(RNConstants.ARG_MAXDATE, (long) options.getDouble(RNConstants.ARG_MAXDATE));
- }
- if (options.hasKey(RNConstants.ARG_DISPLAY) && !options.isNull(RNConstants.ARG_DISPLAY)) {
- args.putString(RNConstants.ARG_DISPLAY, options.getString(RNConstants.ARG_DISPLAY));
- }
- if (options.hasKey(RNConstants.ARG_DIALOG_BUTTONS) && !options.isNull(RNConstants.ARG_DIALOG_BUTTONS)) {
- args.putBundle(RNConstants.ARG_DIALOG_BUTTONS, Arguments.toBundle(options.getMap(RNConstants.ARG_DIALOG_BUTTONS)));
- }
- if (options.hasKey(RNConstants.ARG_TZOFFSET_MINS) && !options.isNull(RNConstants.ARG_TZOFFSET_MINS)) {
- args.putLong(RNConstants.ARG_TZOFFSET_MINS, (long) options.getDouble(RNConstants.ARG_TZOFFSET_MINS));
- }
- if (options.hasKey(RNConstants.ARG_TESTID) && !options.isNull(RNConstants.ARG_TESTID)) {
- args.putString(RNConstants.ARG_TESTID, options.getString(RNConstants.ARG_TESTID));
- }
- if (options.hasKey(RNConstants.FIRST_DAY_OF_WEEK) && !options.isNull(RNConstants.FIRST_DAY_OF_WEEK)) {
- // FIRST_DAY_OF_WEEK is 0-indexed, since it uses the same constants DAY_OF_WEEK used in the Windows implementation
- // Android DatePicker uses 1-indexed values, SUNDAY being 1 and SATURDAY being 7, so the +1 is necessary in this case
- args.putInt(RNConstants.FIRST_DAY_OF_WEEK, options.getInt(RNConstants.FIRST_DAY_OF_WEEK)+1);
- }
- return args;
- }
}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialDatePickerModule.kt b/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialDatePickerModule.kt
new file mode 100644
index 00000000..2cb28ade
--- /dev/null
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialDatePickerModule.kt
@@ -0,0 +1,44 @@
+package com.reactcommunity.rndatetimepicker
+
+import androidx.fragment.app.FragmentActivity
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.UiThreadUtil
+import com.reactcommunity.rndatetimepicker.Common.createDatePickerArguments
+import com.reactcommunity.rndatetimepicker.Common.dismissDialog
+
+class MaterialDatePickerModule(reactContext: ReactApplicationContext): NativeModuleMaterialDatePickerSpec(reactContext) {
+ companion object {
+ const val NAME = "RNCMaterialDatePicker"
+ }
+
+ override fun getName(): String {
+ return NAME
+ }
+
+ override fun dismiss(promise: Promise?) {
+ val activity = currentActivity as FragmentActivity?
+ dismissDialog(activity, NAME, promise)
+ }
+
+ override fun open(params: ReadableMap, promise: Promise) {
+ val activity = currentActivity as FragmentActivity?
+ if (activity == null) {
+ promise.reject(
+ RNConstants.ERROR_NO_ACTIVITY,
+ "Tried to open a MaterialDatePicker dialog while not attached to an Activity"
+ )
+ return
+ }
+
+ val fragmentManager = activity.supportFragmentManager
+
+ UiThreadUtil.runOnUiThread {
+ val arguments = createDatePickerArguments(params)
+ val datePicker =
+ RNMaterialDatePicker(arguments, promise, fragmentManager, reactApplicationContext)
+ datePicker.open()
+ }
+ }
+}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialTimePickerModule.kt b/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialTimePickerModule.kt
new file mode 100644
index 00000000..b3e75669
--- /dev/null
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialTimePickerModule.kt
@@ -0,0 +1,49 @@
+package com.reactcommunity.rndatetimepicker
+
+import androidx.fragment.app.FragmentActivity
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.UiThreadUtil
+import com.reactcommunity.rndatetimepicker.Common.createTimePickerArguments
+import com.reactcommunity.rndatetimepicker.Common.dismissDialog
+
+class MaterialTimePickerModule(reactContext: ReactApplicationContext) :
+ NativeModuleMaterialTimePickerSpec(reactContext) {
+ companion object {
+ const val NAME = "RNCMaterialTimePicker"
+ }
+
+ override fun getName(): String {
+ return NAME
+ }
+
+ override fun dismiss(promise: Promise?) {
+ val activity = currentActivity as FragmentActivity?
+ dismissDialog(activity, NAME, promise)
+ }
+
+ override fun open(params: ReadableMap, promise: Promise) {
+ val activity = currentActivity as FragmentActivity?
+ if (activity == null) {
+ promise.reject(
+ RNConstants.ERROR_NO_ACTIVITY,
+ "Tried to open a MaterialTimePicker dialog while not attached to an Activity"
+ )
+ }
+
+ val fragmentManager = activity!!.supportFragmentManager
+
+ UiThreadUtil.runOnUiThread {
+ val arguments =
+ createTimePickerArguments(params)
+ val materialPicker = RNMaterialTimePicker(
+ arguments,
+ promise,
+ fragmentManager,
+ reactApplicationContext
+ )
+ materialPicker.open()
+ }
+ }
+}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java
index acb56cb9..07220b79 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java
@@ -14,6 +14,9 @@ public final class RNConstants {
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 ARG_TITLE = "title";
+ public static final String ARG_INITIAL_INPUT_MODE = "initialInputMode";
+ public static final String ARG_FULLSCREEN = "fullscreen";
public static final String ACTION_DATE_SET = "dateSetAction";
public static final String ACTION_TIME_SET = "timeSetAction";
public static final String ACTION_DISMISSED = "dismissedAction";
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDate.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDate.java
index 137869a9..f5dc0f64 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDate.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDate.java
@@ -23,4 +23,5 @@ public RNDate(Bundle args) {
public int day() { return now.get(Calendar.DAY_OF_MONTH); }
public int hour() { return now.get(Calendar.HOUR_OF_DAY); }
public int minute() { return now.get(Calendar.MINUTE); }
+ public Long timestamp() { return now.getTimeInMillis(); }
}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDateTimePickerPackage.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDateTimePickerPackage.java
index ceafa6ba..417183e4 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDateTimePickerPackage.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDateTimePickerPackage.java
@@ -20,6 +20,10 @@ public NativeModule getModule(String name, ReactApplicationContext reactContext)
return new DatePickerModule(reactContext);
} else if (name.equals(TimePickerModule.NAME)) {
return new TimePickerModule(reactContext);
+ } else if (name.equals(MaterialDatePickerModule.NAME)) {
+ return new MaterialDatePickerModule(reactContext);
+ } else if (name.equals(MaterialTimePickerModule.NAME)) {
+ return new MaterialTimePickerModule(reactContext);
} else {
return null;
}
@@ -52,6 +56,28 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() {
false, // isCxxModule
isTurboModule // isTurboModule
));
+ moduleInfos.put(
+ MaterialDatePickerModule.NAME,
+ new ReactModuleInfo(
+ MaterialDatePickerModule.NAME,
+ MaterialDatePickerModule.NAME,
+ false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // hasConstants
+ false, // isCxxModule
+ isTurboModule // isTurboModule
+ ));
+ moduleInfos.put(
+ MaterialTimePickerModule.NAME,
+ new ReactModuleInfo(
+ MaterialTimePickerModule.NAME,
+ MaterialTimePickerModule.NAME,
+ false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // hasConstants
+ false, // isCxxModule
+ isTurboModule // isTurboModule
+ ));
return moduleInfos;
};
}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt
new file mode 100644
index 00000000..3e0d88b7
--- /dev/null
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt
@@ -0,0 +1,179 @@
+package com.reactcommunity.rndatetimepicker
+
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.fragment.app.FragmentManager
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.WritableNativeMap
+import com.google.android.material.datepicker.CalendarConstraints
+import com.google.android.material.datepicker.CalendarConstraints.DateValidator
+import com.google.android.material.datepicker.CompositeDateValidator
+import com.google.android.material.datepicker.DateValidatorPointBackward
+import com.google.android.material.datepicker.DateValidatorPointForward
+import com.google.android.material.datepicker.MaterialDatePicker
+import com.google.android.material.datepicker.MaterialPickerOnPositiveButtonClickListener
+import java.util.Calendar
+
+class RNMaterialDatePicker(
+ private val args: Bundle,
+ private val promise: Promise,
+ private val fragmentManager: FragmentManager,
+ private val reactContext: ReactApplicationContext
+) {
+ private var promiseResolved = false
+ private var datePicker: MaterialDatePicker? = null
+ private var builder = MaterialDatePicker.Builder.datePicker()
+
+ fun open() {
+ createDatePicker()
+ addListeners()
+ show()
+ }
+
+ private fun createDatePicker() {
+ setInitialDate()
+ setTitle()
+ setInputMode()
+ setButtons()
+ setConstraints()
+ setFullscreen()
+
+ datePicker = builder.build()
+ }
+
+ private fun setInitialDate() {
+ val initialDate = RNDate(args)
+ builder.setSelection(initialDate.timestamp())
+ }
+
+ private fun setTitle() {
+ val title = args.getString(RNConstants.ARG_TITLE)
+ if (!title.isNullOrEmpty()) {
+ builder.setTitleText(args.getString(RNConstants.ARG_TITLE))
+ }
+ }
+
+ private fun setInputMode() {
+ if (args.getString(RNConstants.ARG_INITIAL_INPUT_MODE).isNullOrEmpty()) {
+ builder.setInputMode(MaterialDatePicker.INPUT_MODE_CALENDAR)
+ return
+ }
+
+ val inputMode =
+ RNMaterialInputMode.valueOf(
+ args.getString(RNConstants.ARG_INITIAL_INPUT_MODE)!!.uppercase()
+ )
+
+ if (inputMode == RNMaterialInputMode.KEYBOARD) {
+ builder.setInputMode(MaterialDatePicker.INPUT_MODE_TEXT)
+ } else {
+ builder.setInputMode(MaterialDatePicker.INPUT_MODE_CALENDAR)
+ }
+ }
+
+ private fun setConstraints() {
+ val constraintsBuilder = CalendarConstraints.Builder()
+
+ if (args.containsKey(RNConstants.FIRST_DAY_OF_WEEK)) {
+ constraintsBuilder.setFirstDayOfWeek(args.getInt(RNConstants.FIRST_DAY_OF_WEEK))
+ }
+
+ val validators = mutableListOf()
+
+ if (args.containsKey(RNConstants.ARG_MINDATE)) {
+ val minDate = Common.minDateWithTimeZone(args)
+ validators.add(DateValidatorPointForward.from(minDate))
+ }
+
+ if (args.containsKey(RNConstants.ARG_MAXDATE)) {
+ val maxDate = Common.maxDateWithTimeZone(args)
+ validators.add(DateValidatorPointBackward.before(maxDate))
+ }
+
+ constraintsBuilder.setValidator(CompositeDateValidator.allOf(validators))
+ builder.setCalendarConstraints(constraintsBuilder.build())
+ }
+
+ private fun setFullscreen() {
+ val isFullscreen = args.getBoolean(RNConstants.ARG_FULLSCREEN)
+
+ if (isFullscreen) {
+ builder.setTheme(com.google.android.material.R.style.ThemeOverlay_Material3_MaterialCalendar_Fullscreen)
+ } else {
+ builder.setTheme(com.google.android.material.R.style.ThemeOverlay_Material3_MaterialCalendar)
+ }
+ }
+
+ private fun addListeners() {
+ val listeners = Listeners()
+ datePicker!!.addOnPositiveButtonClickListener(listeners)
+ datePicker!!.addOnDismissListener(listeners)
+ }
+
+ private fun show() {
+ datePicker!!.show(fragmentManager, MaterialDatePickerModule.NAME)
+ }
+
+ private fun setButtons() {
+ val buttons = args.getBundle(RNConstants.ARG_DIALOG_BUTTONS) ?: return
+
+ val negativeButton = buttons.getBundle(Common.NEGATIVE)
+ val positiveButton = buttons.getBundle(Common.POSITIVE)
+
+ if (negativeButton != null) {
+ builder.setNegativeButtonText(negativeButton.getString(Common.LABEL))
+ }
+
+ if (positiveButton != null) {
+ builder.setPositiveButtonText(positiveButton.getString(Common.LABEL))
+ }
+ }
+
+ private inner class Listeners : MaterialPickerOnPositiveButtonClickListener,
+ DialogInterface.OnDismissListener {
+ override fun onDismiss(dialog: DialogInterface) {
+ if (promiseResolved || !reactContext.hasActiveReactInstance()) return
+
+ val result = WritableNativeMap()
+ result.putString("action", RNConstants.ACTION_DISMISSED)
+ promise.resolve(result)
+ promiseResolved = true
+ }
+
+ override fun onPositiveButtonClick(selection: Long) {
+ if (promiseResolved || !reactContext.hasActiveReactInstance()) return
+
+ val newCalendar = createNewCalendar(selection)
+ val result = WritableNativeMap()
+
+ result.putString("action", RNConstants.ACTION_DATE_SET)
+ result.putDouble("timestamp", newCalendar.timeInMillis.toDouble())
+ result.putDouble(
+ "utcOffset",
+ newCalendar.timeZone.getOffset(newCalendar.timeInMillis).toDouble() / 1000 / 60
+ )
+
+ promise.resolve(result)
+ promiseResolved = true
+ }
+
+ private fun createNewCalendar(selection: Long): Calendar {
+ val initialDate = RNDate(args)
+ val newCalendar = Calendar.getInstance(
+ Common.getTimeZone(
+ args
+ )
+ )
+
+ newCalendar.timeInMillis = selection
+
+ newCalendar[Calendar.HOUR_OF_DAY] = initialDate.hour()
+ newCalendar[Calendar.MINUTE] = initialDate.minute()
+ newCalendar[Calendar.SECOND] = 0
+ newCalendar[Calendar.MILLISECOND] = 0
+
+ return newCalendar
+ }
+ }
+}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialInputMode.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialInputMode.java
new file mode 100644
index 00000000..5952b0a2
--- /dev/null
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialInputMode.java
@@ -0,0 +1,6 @@
+package com.reactcommunity.rndatetimepicker;
+
+public enum RNMaterialInputMode {
+ DEFAULT,
+ KEYBOARD
+}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialTimePicker.kt b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialTimePicker.kt
new file mode 100644
index 00000000..9ff384bf
--- /dev/null
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialTimePicker.kt
@@ -0,0 +1,155 @@
+package com.reactcommunity.rndatetimepicker
+
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.format.DateFormat
+import android.view.View
+import androidx.fragment.app.FragmentManager
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.bridge.WritableNativeMap
+import com.google.android.material.timepicker.MaterialTimePicker
+import com.google.android.material.timepicker.TimeFormat
+import java.util.Calendar
+
+class RNMaterialTimePicker(
+ private val args: Bundle,
+ private val promise: Promise,
+ private val fragmentManager: FragmentManager,
+ private val reactContext: ReactApplicationContext
+) {
+ private var promiseResolved = false
+ private var timePicker: MaterialTimePicker? = null
+ private var builder = MaterialTimePicker.Builder()
+
+ fun open() {
+ createTimePicker()
+ addListeners()
+ show()
+ }
+
+ private fun createTimePicker() {
+ setInitialDate()
+ setTitle()
+ setInputMode()
+ setButtons()
+ setTimeFormat()
+
+ timePicker = builder.build()
+ }
+
+ private fun setInitialDate() {
+ val initialDate = RNDate(args)
+
+ builder.setHour(initialDate.hour())
+ .setMinute(initialDate.minute())
+ }
+
+ private fun setTitle() {
+ val title = args.getString(RNConstants.ARG_TITLE)
+ if (!title.isNullOrEmpty()) {
+ builder.setTitleText(args.getString(RNConstants.ARG_TITLE))
+ }
+ }
+
+ private fun setInputMode() {
+ if (args.getString(RNConstants.ARG_INITIAL_INPUT_MODE).isNullOrEmpty()) {
+ builder.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
+ return
+ }
+
+ val inputMode =
+ RNMaterialInputMode.valueOf(
+ args.getString(RNConstants.ARG_INITIAL_INPUT_MODE)!!.uppercase()
+ )
+
+ if (inputMode == RNMaterialInputMode.KEYBOARD) {
+ builder.setInputMode(MaterialTimePicker.INPUT_MODE_KEYBOARD)
+ } else {
+ builder.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
+ }
+ }
+
+ private fun setButtons() {
+ val buttons = args.getBundle(RNConstants.ARG_DIALOG_BUTTONS) ?: return
+
+ val negativeButton = buttons.getBundle(Common.NEGATIVE)
+ val positiveButton = buttons.getBundle(Common.POSITIVE)
+
+ if (negativeButton != null) {
+ builder.setNegativeButtonText(negativeButton.getString(Common.LABEL))
+ }
+
+ if (positiveButton != null) {
+ builder.setPositiveButtonText(positiveButton.getString(Common.LABEL))
+ }
+ }
+
+ private fun setTimeFormat() {
+ if (args.getBoolean(RNConstants.ARG_IS24HOUR)) {
+ builder.setTimeFormat(TimeFormat.CLOCK_24H)
+ return
+ }
+
+ if (DateFormat.is24HourFormat(reactContext)) {
+ builder.setTimeFormat(TimeFormat.CLOCK_24H)
+ } else {
+ builder.setTimeFormat(TimeFormat.CLOCK_12H)
+ }
+ }
+
+ private fun addListeners() {
+ val listeners = Listeners()
+ timePicker!!.addOnPositiveButtonClickListener(listeners)
+ timePicker!!.addOnDismissListener(listeners)
+ }
+
+ private fun show() {
+ timePicker!!.show(fragmentManager, MaterialTimePickerModule.NAME)
+ }
+
+ private inner class Listeners : View.OnClickListener, DialogInterface.OnDismissListener {
+ override fun onDismiss(dialog: DialogInterface) {
+ if (promiseResolved || !reactContext.hasActiveReactInstance()) return
+
+ val result: WritableMap = WritableNativeMap()
+ result.putString("action", RNConstants.ACTION_DISMISSED)
+ promise.resolve(result)
+ promiseResolved = true
+ }
+
+ override fun onClick(v: View) {
+ if (promiseResolved || !reactContext.hasActiveReactInstance()) return
+
+ val newCalendar = createNewCalendar()
+ val result = WritableNativeMap()
+
+ result.putString("action", RNConstants.ACTION_DATE_SET)
+ result.putDouble("timestamp", newCalendar.timeInMillis.toDouble())
+ result.putDouble(
+ "utcOffset",
+ newCalendar.timeZone.getOffset(newCalendar.timeInMillis).toDouble() / 1000 / 60
+ )
+
+ promise.resolve(result)
+ promiseResolved = true
+ }
+
+ private fun createNewCalendar(): Calendar {
+ val initialDate = RNDate(args)
+ val calendar = Calendar.getInstance(
+ Common.getTimeZone(
+ args
+ )
+ )
+
+ calendar[initialDate.year(), initialDate.month(), initialDate.day(), timePicker!!.hour, timePicker!!.minute] =
+ 0
+
+ calendar[Calendar.MILLISECOND] = 0
+
+ return calendar
+ }
+ }
+}
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/TimePickerModule.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/TimePickerModule.java
index 877b37b1..46dd5e41 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/TimePickerModule.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/TimePickerModule.java
@@ -23,6 +23,7 @@
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
+import static com.reactcommunity.rndatetimepicker.Common.createTimePickerArguments;
import static com.reactcommunity.rndatetimepicker.Common.dismissDialog;
import java.util.Calendar;
@@ -119,7 +120,7 @@ public void open(final ReadableMap options, final Promise promise) {
RNTimePickerDialogFragment oldFragment =
(RNTimePickerDialogFragment) fragmentManager.findFragmentByTag(NAME);
- Bundle arguments = createFragmentArguments(options);
+ Bundle arguments = createTimePickerArguments(options);
if (oldFragment != null) {
oldFragment.update(arguments);
@@ -137,25 +138,4 @@ public void open(final ReadableMap options, final Promise promise) {
fragment.show(fragmentManager, NAME);
});
}
-
- private Bundle createFragmentArguments(ReadableMap options) {
- 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));
- }
- if (options.hasKey(RNConstants.ARG_DISPLAY) && !options.isNull(RNConstants.ARG_DISPLAY)) {
- args.putString(RNConstants.ARG_DISPLAY, options.getString(RNConstants.ARG_DISPLAY));
- }
- if (options.hasKey(RNConstants.ARG_DIALOG_BUTTONS) && !options.isNull(RNConstants.ARG_DIALOG_BUTTONS)) {
- args.putBundle(RNConstants.ARG_DIALOG_BUTTONS, Arguments.toBundle(options.getMap(RNConstants.ARG_DIALOG_BUTTONS)));
- }
- if (options.hasKey(RNConstants.ARG_INTERVAL) && !options.isNull(RNConstants.ARG_INTERVAL)) {
- args.putInt(RNConstants.ARG_INTERVAL, options.getInt(RNConstants.ARG_INTERVAL));
- }
- if (options.hasKey(RNConstants.ARG_TZOFFSET_MINS) && !options.isNull(RNConstants.ARG_TZOFFSET_MINS)) {
- args.putLong(RNConstants.ARG_TZOFFSET_MINS, (long) options.getDouble(RNConstants.ARG_TZOFFSET_MINS));
- }
- return args;
- }
}
diff --git a/android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialDatePickerSpec.java b/android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialDatePickerSpec.java
new file mode 100644
index 00000000..4db46bff
--- /dev/null
+++ b/android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialDatePickerSpec.java
@@ -0,0 +1,36 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Then it was commited. It is here to support the old architecture.
+ * If you use the new architecture, this file won't be included and instead will be generated by the codegen.
+ *
+ * @generated by codegen project: GenerateModuleJavaSpec.js
+ *
+ * @nolint
+ */
+
+package com.reactcommunity.rndatetimepicker;
+
+import com.facebook.proguard.annotations.DoNotStrip;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReactModuleWithSpec;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.turbomodule.core.interfaces.TurboModule;
+
+public abstract class NativeModuleMaterialDatePickerSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule {
+ public NativeModuleMaterialDatePickerSpec(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @ReactMethod
+ @DoNotStrip
+ public abstract void dismiss(Promise promise);
+
+ @ReactMethod
+ @DoNotStrip
+ public abstract void open(ReadableMap params, Promise promise);
+}
diff --git a/android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialTimePickerSpec.java b/android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialTimePickerSpec.java
new file mode 100644
index 00000000..a59a8cda
--- /dev/null
+++ b/android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialTimePickerSpec.java
@@ -0,0 +1,36 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Then it was commited. It is here to support the old architecture.
+ * If you use the new architecture, this file won't be included and instead will be generated by the codegen.
+ *
+ * @generated by codegen project: GenerateModuleJavaSpec.js
+ *
+ * @nolint
+ */
+
+package com.reactcommunity.rndatetimepicker;
+
+import com.facebook.proguard.annotations.DoNotStrip;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReactModuleWithSpec;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.turbomodule.core.interfaces.TurboModule;
+
+public abstract class NativeModuleMaterialTimePickerSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule {
+ public NativeModuleMaterialTimePickerSpec(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @ReactMethod
+ @DoNotStrip
+ public abstract void dismiss(Promise promise);
+
+ @ReactMethod
+ @DoNotStrip
+ public abstract void open(ReadableMap params, Promise promise);
+}
diff --git a/docs/images/android_material_date.jpg b/docs/images/android_material_date.jpg
new file mode 100644
index 00000000..8930cb10
Binary files /dev/null and b/docs/images/android_material_date.jpg differ
diff --git a/docs/images/android_material_time.jpg b/docs/images/android_material_time.jpg
new file mode 100644
index 00000000..b1f3aaca
Binary files /dev/null and b/docs/images/android_material_time.jpg differ
diff --git a/example/App.js b/example/App.js
index 7e00c069..ae542c80 100644
--- a/example/App.js
+++ b/example/App.js
@@ -87,6 +87,8 @@ const DISPLAY_VALUES = Platform.select({
windows: [],
});
const MINUTE_INTERVALS = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30];
+const DESIGNS = ['default', 'material'];
+const INPUT_MODES = ['default', 'keyboard'];
export const App = () => {
// Sat, 13 Nov 2021 10:00:00 GMT (local: Saturday, November 13, 2021 11:00:00 AM GMT+01:00)
@@ -103,8 +105,14 @@ export const App = () => {
const [interval, setMinInterval] = useState(1);
const [neutralButtonLabel, setNeutralButtonLabel] = useState(undefined);
const [disabled, setDisabled] = useState(false);
+ const [isFullscreen, setIsFullscreen] = useState(false);
const [minimumDate, setMinimumDate] = useState();
const [maximumDate, setMaximumDate] = useState();
+ const [design, setDesign] = useState(DESIGNS[0]);
+ const [inputMode, setInputMode] = useState(INPUT_MODES[0]);
+ const [title, setTitle] = useState('');
+
+ const isMaterialDesign = design === 'material';
// Windows-specific
const [time, setTime] = useState(undefined);
@@ -312,6 +320,32 @@ export const App = () => {
);
}}
/>
+ Design (android only):
+ {
+ setDesign(DESIGNS[event.nativeEvent.selectedSegmentIndex]);
+ }}
+ />
+ Input mode (android only):
+ {
+ setInputMode(INPUT_MODES[event.nativeEvent.selectedSegmentIndex]);
+ }}
+ />
+
+
+ title (android only)
+
+
+
text color (iOS only)
@@ -344,6 +378,14 @@ export const App = () => {
+
+
+ fullscreen (android only)
+
+
+
+
+
neutralButtonLabel (android only)
@@ -455,6 +497,10 @@ export const App = () => {
negativeButton={{label: 'Cancel', textColor: 'red'}}
disabled={disabled}
firstDayOfWeek={firstDayOfWeek}
+ title={isMaterialDesign ? title : undefined}
+ initialInputMode={isMaterialDesign ? inputMode : undefined}
+ design={design}
+ fullscreen={isMaterialDesign ? isFullscreen : undefined}
/>
)}
diff --git a/example/e2e/detoxTest.spec.js b/example/e2e/detoxTest.spec.js
index 3ea6dbf2..8961720a 100644
--- a/example/e2e/detoxTest.spec.js
+++ b/example/e2e/detoxTest.spec.js
@@ -168,6 +168,7 @@ describe('e2e tests', () => {
await expect(elementById('overriddenTzName')).toHaveText('Europe/Prague');
let timeZone = 'America/Vancouver';
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
await waitFor(elementById('timezone')).toBeVisible().withTimeout(1000);
await userSwipesTimezoneListUntilDesiredIsVisible(timeZone);
@@ -192,6 +193,7 @@ describe('e2e tests', () => {
it('daylight saving should work properly', async () => {
let timeZone = 'America/Vancouver';
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
await waitFor(elementById('timezone')).toBeVisible().withTimeout(1000);
await userSwipesTimezoneListUntilDesiredIsVisible(timeZone);
@@ -373,6 +375,7 @@ describe('e2e tests', () => {
});
it(':android: when component unmounts, dialog is dismissed', async () => {
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
await elementById('showAndDismissPickerButton').tap();
await waitFor(getDatePickerAndroid()).toExist().withTimeout(4000);
await wait(6000);
diff --git a/example/e2e/utils/actions.js b/example/e2e/utils/actions.js
index 4a70cbdd..6e18b360 100644
--- a/example/e2e/utils/actions.js
+++ b/example/e2e/utils/actions.js
@@ -35,7 +35,7 @@ async function userOpensPicker({
await elementById('DateTimePickerScrollView').scrollTo('top');
await element(by.text(mode)).tap();
- await element(by.text(display)).tap();
+ await element(by.text(display)).atIndex(0).tap();
if (interval) {
await element(by.text(String(interval))).tap();
}
@@ -47,6 +47,7 @@ async function userOpensPicker({
if (firstDayOfWeek) {
await element(by.id(firstDayOfWeek)).tap();
}
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
await element(by.id('showPickerButton')).tap();
}
diff --git a/package.json b/package.json
index eccb0a73..9802aba7 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
"access": "public"
},
"scripts": {
- "start": "react-native start",
+ "start": "patch-package && react-native start",
"start:android": "react-native run-android",
"start:ios": "react-native run-ios",
"start:windows": "react-native run-windows --sln example/windows/date-time-picker-example.sln",
@@ -91,6 +91,8 @@
"jest": "^29.5.0",
"moment": "^2.24.0",
"moment-timezone": "^0.5.41",
+ "patch-package": "^8.0.0",
+ "postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.8",
"react": "18.3.1",
"react-native": "^0.75.4",
diff --git a/patches/react-native-test-app+4.0.7.patch b/patches/react-native-test-app+4.0.7.patch
new file mode 100644
index 00000000..eff131ce
--- /dev/null
+++ b/patches/react-native-test-app+4.0.7.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/react-native-test-app/android/app/src/main/res/values/styles.xml b/node_modules/react-native-test-app/android/app/src/main/res/values/styles.xml
+index e58cbf3..ba878cb 100644
+--- a/node_modules/react-native-test-app/android/app/src/main/res/values/styles.xml
++++ b/node_modules/react-native-test-app/android/app/src/main/res/values/styles.xml
+@@ -1,7 +1,7 @@
+
+
+
+-