From 401b2e325879715c3a78228b5740046f7180cc92 Mon Sep 17 00:00:00 2001 From: sidorchukandrew <36050911+sidorchukandrew@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:19:14 -0500 Subject: [PATCH 1/7] Set up module on Android side --- .../MaterialRangePickerModule.kt | 21 +++++++++++ .../RNDateTimePickerPackage.java | 13 +++++++ .../NativeModuleMaterialRangePickerSpec.java | 36 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialRangePickerModule.kt create mode 100644 android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialRangePickerSpec.java diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialRangePickerModule.kt b/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialRangePickerModule.kt new file mode 100644 index 00000000..75903327 --- /dev/null +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialRangePickerModule.kt @@ -0,0 +1,21 @@ +package com.reactcommunity.rndatetimepicker + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap + +class MaterialRangePickerModule(reactContext: ReactApplicationContext): NativeModuleMaterialRangePickerSpec(reactContext) { + companion object { + const val NAME = "RNCMaterialRangePicker" + } + + override fun getName(): String { + return NAME + } + + override fun dismiss(promise: Promise?) { + } + + override fun open(params: ReadableMap, promise: Promise) { + } +} diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDateTimePickerPackage.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDateTimePickerPackage.java index 417183e4..fb03cfd6 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDateTimePickerPackage.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDateTimePickerPackage.java @@ -24,6 +24,8 @@ public NativeModule getModule(String name, ReactApplicationContext reactContext) return new MaterialDatePickerModule(reactContext); } else if (name.equals(MaterialTimePickerModule.NAME)) { return new MaterialTimePickerModule(reactContext); + } else if (name.equals(MaterialRangePickerModule.NAME)) { + return new MaterialRangePickerModule(reactContext); } else { return null; } @@ -78,6 +80,17 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { false, // isCxxModule isTurboModule // isTurboModule )); + moduleInfos.put( + MaterialRangePickerModule.NAME, + new ReactModuleInfo( + MaterialRangePickerModule.NAME, + MaterialRangePickerModule.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // hasConstants + false, // isCxxModule + isTurboModule // isTurboModule + )); return moduleInfos; }; } diff --git a/android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialRangePickerSpec.java b/android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialRangePickerSpec.java new file mode 100644 index 00000000..33ffec46 --- /dev/null +++ b/android/src/paper/java/com/reactcommunity/rndatetimepicker/NativeModuleMaterialRangePickerSpec.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 NativeModuleMaterialRangePickerSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule { + public NativeModuleMaterialRangePickerSpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @ReactMethod + @DoNotStrip + public abstract void dismiss(Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void open(ReadableMap params, Promise promise); +} From 649ed9d33763d237a8e20cbc961f8786c8749722 Mon Sep 17 00:00:00 2001 From: sidorchukandrew <36050911+sidorchukandrew@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:22:45 -0500 Subject: [PATCH 2/7] Implement range picker This implementation models how the Material date picker was implemented. However, the range picker returns two values and accepts two values for the start and end of the range. --- .../rndatetimepicker/Common.java | 14 ++ .../MaterialRangePickerModule.kt | 24 ++ .../rndatetimepicker/RNConstants.java | 3 + .../rndatetimepicker/RNMaterialRangePicker.kt | 207 ++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100644 android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialRangePicker.kt diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java index ca6b2e06..9874ec1b 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java @@ -248,6 +248,20 @@ public static Bundle createDatePickerArguments(ReadableMap options) { return args; } + public static Bundle createRangePickerArguments(ReadableMap options) { + final Bundle args = createDatePickerArguments(options); + + if (options.hasKey(RNConstants.ARG_START_TIMESTAMP) && !options.isNull(RNConstants.ARG_START_TIMESTAMP)) { + args.putLong(RNConstants.ARG_START_TIMESTAMP, (long) options.getDouble(RNConstants.ARG_START_TIMESTAMP)); + } + + if (options.hasKey(RNConstants.ARG_END_TIMESTAMP) && !options.isNull(RNConstants.ARG_END_TIMESTAMP)) { + args.putLong(RNConstants.ARG_END_TIMESTAMP, (long) options.getDouble(RNConstants.ARG_END_TIMESTAMP)); + } + + return args; + } + public static Bundle createTimePickerArguments(ReadableMap options) { final Bundle args = Common.createFragmentArguments(options); diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialRangePickerModule.kt b/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialRangePickerModule.kt index 75903327..79d2f17c 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialRangePickerModule.kt +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/MaterialRangePickerModule.kt @@ -1,8 +1,13 @@ 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.createRangePickerArguments +import com.reactcommunity.rndatetimepicker.Common.dismissDialog class MaterialRangePickerModule(reactContext: ReactApplicationContext): NativeModuleMaterialRangePickerSpec(reactContext) { companion object { @@ -14,8 +19,27 @@ class MaterialRangePickerModule(reactContext: ReactApplicationContext): NativeMo } 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 MaterialRangePicker dialog while not attached to an Activity" + ) + return + } + + val fragmentManager = activity.supportFragmentManager + + UiThreadUtil.runOnUiThread { + val arguments = createRangePickerArguments(params) + val rangePicker = + RNMaterialRangePicker(arguments, promise, fragmentManager, reactApplicationContext) + rangePicker.open() + } } } diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java index 07220b79..eddc9412 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java @@ -5,6 +5,8 @@ public final class RNConstants { public static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY"; public static final String ARG_VALUE = "value"; + public static final String ARG_START_TIMESTAMP = "startTimestamp"; + public static final String ARG_END_TIMESTAMP = "endTimestamp"; public static final String ARG_MINDATE = "minimumDate"; public static final String ARG_MAXDATE = "maximumDate"; public static final String ARG_INTERVAL = "minuteInterval"; @@ -18,6 +20,7 @@ public final class RNConstants { 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_RANGE_SET = "rangeSetAction"; public static final String ACTION_TIME_SET = "timeSetAction"; public static final String ACTION_DISMISSED = "dismissedAction"; public static final String ACTION_NEUTRAL_BUTTON = "neutralButtonAction"; diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialRangePicker.kt b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialRangePicker.kt new file mode 100644 index 00000000..69bf9fff --- /dev/null +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialRangePicker.kt @@ -0,0 +1,207 @@ +package com.reactcommunity.rndatetimepicker + +import android.content.DialogInterface +import android.os.Bundle +import androidx.core.util.Pair +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 RNMaterialRangePicker( + private val args: Bundle, + private val promise: Promise, + private val fragmentManager: FragmentManager, + private val reactContext: ReactApplicationContext +) { + private var promiseResolved = false + private var rangePicker: MaterialDatePicker>? = null + private var builder = MaterialDatePicker.Builder.dateRangePicker() + + fun open() { + createRangePicker() + addListeners() + show() + } + + private fun createRangePicker() { + setInitialDates() + setTitle() + setInputMode() + setButtons() + setConstraints() + setFullscreen() + + rangePicker = builder.build() + } + + private fun setInitialDates() { + var start: Long? = null + var end: Long? = null + + if (args.containsKey(RNConstants.ARG_START_TIMESTAMP)) { + // override "value" so we can use the same constructor from RNDate + args.putLong(RNConstants.ARG_VALUE, args.getLong((RNConstants.ARG_START_TIMESTAMP))) + start = RNDate(args).timestamp() + } + + if (args.containsKey(RNConstants.ARG_END_TIMESTAMP)) { + // override "value" so we can use the same constructor from RNDate + args.putLong(RNConstants.ARG_VALUE, args.getLong((RNConstants.ARG_END_TIMESTAMP))) + end = RNDate(args).timestamp() + } + + val selection = Pair(start, end) + builder.setSelection(selection) + } + + 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 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 addListeners() { + val listeners = Listeners() + rangePicker!!.addOnPositiveButtonClickListener(listeners) + rangePicker!!.addOnDismissListener(listeners) + } + + private fun show() { + rangePicker!!.show(fragmentManager, MaterialRangePickerModule.NAME) + } + + 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: Pair) { + if (promiseResolved || !reactContext.hasActiveReactInstance()) return + + val result = WritableNativeMap() + + result.putString("action", RNConstants.ACTION_RANGE_SET) + result.putDouble("startTimestamp", getStartTimestamp(selection)) + result.putDouble("endTimestamp", getEndTimestamp(selection)) + result.putDouble( + "utcOffset", + getStartTimestamp(selection) / 1000 / 60 + ) + + promise.resolve(result) + promiseResolved = true + } + + private fun getStartTimestamp(selection: Pair): Double { + val newCalendar = Calendar.getInstance( + Common.getTimeZone( + args + ) + ) + + newCalendar.timeInMillis = selection.first + newCalendar[Calendar.HOUR_OF_DAY] = 0 + newCalendar[Calendar.MINUTE] = 0 + newCalendar[Calendar.SECOND] = 0 + + return newCalendar.timeInMillis.toDouble() + } + + private fun getEndTimestamp(selection: Pair): Double { + val newCalendar = Calendar.getInstance( + Common.getTimeZone( + args + ) + ) + + newCalendar.timeInMillis = selection.first + newCalendar[Calendar.HOUR_OF_DAY] = 23 + newCalendar[Calendar.MINUTE] = 59 + newCalendar[Calendar.SECOND] = 59 + + return newCalendar.timeInMillis.toDouble() + } + } +} From af8a7b7aefa316bd6813bf396ecdb28834cc483f Mon Sep 17 00:00:00 2001 From: sidorchukandrew <36050911+sidorchukandrew@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:23:58 -0500 Subject: [PATCH 3/7] Add spec on Javascript side --- src/specs/NativeModuleMaterialRangePicker.js | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/specs/NativeModuleMaterialRangePicker.js diff --git a/src/specs/NativeModuleMaterialRangePicker.js b/src/specs/NativeModuleMaterialRangePicker.js new file mode 100644 index 00000000..bf78a6ca --- /dev/null +++ b/src/specs/NativeModuleMaterialRangePicker.js @@ -0,0 +1,34 @@ +// @flow strict-local + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import {TurboModuleRegistry} from 'react-native'; + +export type RangePickerOpenParams = $ReadOnly<{ + dialogButtons?: $ReadOnly<{string: string}>, + initialInputMode?: string, + title?: string, + maximumDate?: number, + minimumDate?: number, + startTimestamp?: number, + endTimestamp?: number, + testID?: string, + timeZoneName?: number, + timeZoneOffsetInMinutes?: number, +}>; + +type RangeSetAction = 'rangeSetAction' | 'dismissedAction'; +type RangePickerResult = $ReadOnly<{ + action: RangeSetAction, + startTimestamp: number, + endTimestamp: number, + utcOffset: number, +}>; + +export interface Spec extends TurboModule { + +dismiss: () => Promise; + +open: (params: RangePickerOpenParams) => Promise; +} + +export default (TurboModuleRegistry.getEnforcing( + 'RNCMaterialRangePicker', +): ?Spec); From 10feb666f78d730401219c843762f33386a3e0b0 Mon Sep 17 00:00:00 2001 From: sidorchukandrew <36050911+sidorchukandrew@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:30:35 -0500 Subject: [PATCH 4/7] Implement imperative MaterialRangePicker Since this is a new module, I think we should only support the imperative API the same way we encourage developers to use the imperative API for the date and time pickers. --- src/MaterialRangePicker.android.js | 59 ++++++++++++++++++++++++++++++ src/MaterialRangePicker.js | 11 ++++++ src/constants.js | 1 + src/eventCreators.js | 33 ++++++++++++++++- src/index.d.ts | 28 ++++++++++++++ src/index.js | 1 + src/types.js | 33 +++++++++++++++++ 7 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/MaterialRangePicker.android.js create mode 100644 src/MaterialRangePicker.js diff --git a/src/MaterialRangePicker.android.js b/src/MaterialRangePicker.android.js new file mode 100644 index 00000000..eb64403c --- /dev/null +++ b/src/MaterialRangePicker.android.js @@ -0,0 +1,59 @@ +/** + * @format + * @flow strict-local + */ + +import type {MaterialRangeProps, Range} from './types'; +import RNMaterialRangePickerAndroid from './specs/NativeModuleMaterialRangePicker'; +import {DISMISS_ACTION, RANGE_SET_ACTION} from './constants'; +import { + createRangeDismissEvtParams, + createRangeSetEvtParams, +} from './eventCreators'; + +async function open({ + onChange, + value = {}, + maximumDate, + minimumDate, + onError, + ...props +}: MaterialRangeProps = {}) { + try { + const result = await RNMaterialRangePickerAndroid.open({ + startTimestamp: value.start?.getTime(), + endTimestamp: value.end?.getTime(), + maximumDate: maximumDate?.getTime(), + minimumDate: minimumDate?.getTime(), + ...props, + }); + + const {action, startTimestamp, endTimestamp, utcOffset} = result; + switch (action) { + case RANGE_SET_ACTION: { + const range: Range = { + start: new Date(startTimestamp), + end: new Date(endTimestamp), + }; + + const event = createRangeSetEvtParams(range, utcOffset); + onChange?.(event, range); + break; + } + case DISMISS_ACTION: + default: { + const [event] = createRangeDismissEvtParams(value, utcOffset); + onChange?.(event); + break; + } + } + } catch (error) { + onError?.(error); + } +} + +function dismiss(): Promise { + return RNMaterialRangePickerAndroid.dismiss(); +} + +export const MaterialRangePicker = {open, dismiss}; diff --git a/src/MaterialRangePicker.js b/src/MaterialRangePicker.js new file mode 100644 index 00000000..42976a6f --- /dev/null +++ b/src/MaterialRangePicker.js @@ -0,0 +1,11 @@ +/** + * @format + * @flow strict-local + */ +import {Platform} from 'react-native'; + +const warn = () => { + console.warn(`MaterialRangePicker is not supported on: ${Platform.OS}`); +}; + +export const MaterialRangePicker = {open: warn, dismiss: warn}; diff --git a/src/constants.js b/src/constants.js index acc91329..4cb16ee9 100644 --- a/src/constants.js +++ b/src/constants.js @@ -55,6 +55,7 @@ export const DAY_OF_WEEK = Object.freeze({ export const DATE_SET_ACTION = 'dateSetAction'; export const TIME_SET_ACTION = 'timeSetAction'; +export const RANGE_SET_ACTION = 'rangeSetAction'; export const DISMISS_ACTION = 'dismissedAction'; export const NEUTRAL_BUTTON_ACTION = 'neutralButtonAction'; diff --git a/src/eventCreators.js b/src/eventCreators.js index cd16fbd9..111d04de 100644 --- a/src/eventCreators.js +++ b/src/eventCreators.js @@ -1,7 +1,7 @@ /** * @flow strict-local */ -import type {DateTimePickerEvent} from './types'; +import type {DateTimePickerEvent, RangePickerEvent, Range} from './types'; import {ANDROID_EVT_TYPE, EVENT_TYPE_SET} from './constants'; export const createDateTimeSetEvtParams = ( @@ -20,6 +20,20 @@ export const createDateTimeSetEvtParams = ( ]; }; +export const createRangeSetEvtParams = ( + range: Range, + utcOffset: number, +): RangePickerEvent => { + return { + type: EVENT_TYPE_SET, + nativeEvent: { + startTimestamp: range.start ? range.start.getTime() : 0, + endTimestamp: range.end ? range.end.getTime() : 0, + utcOffset, + }, + }; +}; + export const createDismissEvtParams = ( date: Date, utcOffset: number, @@ -36,6 +50,23 @@ export const createDismissEvtParams = ( ]; }; +export const createRangeDismissEvtParams = ( + range: Range, + utcOffset: number, +): [RangePickerEvent, Range] => { + return [ + { + type: ANDROID_EVT_TYPE.dismissed, + nativeEvent: { + startTimestamp: range.start ? range.start.getTime() : 0, + endTimestamp: range.end ? range.end.getTime() : 0, + utcOffset, + }, + }, + range, + ]; +}; + export const createNeutralEvtParams = ( date: Date, utcOffset: number, diff --git a/src/index.d.ts b/src/index.d.ts index 701ddb68..dbed70a3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -206,6 +206,28 @@ export type AndroidNativeProps = Readonly< } >; +export type MaterialRangePickerProps = { + title?: string; + maximumDate?: Date; + minimumDate?: Date; + timeZoneOffsetInMinutes?: number; + timeZoneName?: string; + testID?: string; + initialInputMode?: string; + dialogButtons?: { + positive: {label: string}; + negative: {label: string}; + }; + fullscreen?: boolean; + value?: Range; + onChange?: (event: RangePickerEvent, range?: Range) => void; +}; + +type Range = { + start?: Date; + end?: Date; +}; + export type DatePickerOptions = DateOptions & { display?: Display; }; @@ -251,9 +273,15 @@ declare namespace DateTimePickerAndroidType { const dismiss: (mode: AndroidNativeProps['mode']) => Promise; } +declare namespace MaterialRangePickerType { + const open: (args: MaterialRangePickerProps) => void; + const dismiss: () => Promise; +} + declare const RNDateTimePicker: FC< IOSNativeProps | AndroidNativeProps | WindowsNativeProps >; export default RNDateTimePicker; export const DateTimePickerAndroid: typeof DateTimePickerAndroidType; +export const MaterialRangePicker: typeof MaterialRangePickerType; diff --git a/src/index.js b/src/index.js index ca4ff882..7c47396a 100644 --- a/src/index.js +++ b/src/index.js @@ -5,5 +5,6 @@ import RNDateTimePicker from './datetimepicker'; export * from './eventCreators'; export {DateTimePickerAndroid} from './DateTimePickerAndroid'; +export {MaterialRangePicker} from './MaterialRangePicker'; export default RNDateTimePicker; diff --git a/src/types.js b/src/types.js index 8c5f04be..269e6184 100644 --- a/src/types.js +++ b/src/types.js @@ -297,3 +297,36 @@ export type WindowsNativeProps = $ReadOnly<{| minuteInterval?: number, accessibilityLabel?: string, |}>; + +export type MaterialRangeProps = $ReadOnly<{| + title?: string, + maximumDate?: Date, + minimumDate?: Date, + timeZoneOffsetInMinutes?: number, + timeZoneName?: string, + testID?: string, + initialInputMode?: string, + dialogButtons?: { + positive: {label: string}, + negative: {label: string}, + }, + fullscreen?: boolean, + value?: Range, + onChange?: ?(event: RangePickerEvent, range?: Range) => void, +|}>; + +export type RangePickerEvent = { + type: 'set' | 'dismiss', + nativeEvent: $ReadOnly<{ + startTimestamp: number, + endTimestamp: number, + utcOffset: number, + ... + }>, + ... +}; + +export type Range = $ReadOnly<{| + start?: Date, + end?: Date, +|}>; From 081015ebec8f473cca17a40036fb6d1cfd4e7500 Mon Sep 17 00:00:00 2001 From: sidorchukandrew <36050911+sidorchukandrew@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:32:23 -0500 Subject: [PATCH 5/7] Update example app with button to display picker --- example/App.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/example/App.js b/example/App.js index ae542c80..c2d8af5d 100644 --- a/example/App.js +++ b/example/App.js @@ -13,7 +13,9 @@ import { Alert, FlatList, } from 'react-native'; -import DateTimePicker from '@react-native-community/datetimepicker'; +import DateTimePicker, { + MaterialRangePicker, +} from '@react-native-community/datetimepicker'; import SegmentedControl from './SegmentedControl'; import {Colors} from 'react-native/Libraries/NewAppScreen'; import React, {useRef, useState} from 'react'; @@ -216,6 +218,22 @@ export const App = () => { setMaximumDate(minimumDate ? undefined : endOfTomorrowUTC); }; + const handleShowRangePicker = () => { + MaterialRangePicker.open({ + fullscreen: isFullscreen, + initialInputMode: inputMode, + maximumDate, + minimumDate, + title, + timeZoneOffsetInMinutes: tzOffsetInMinutes, + timeZoneName: tzName, + dialogButtons: { + negative: {label: 'Nope'}, + positive: {label: 'Yes'}, + }, + }); + }; + if (Platform.OS !== 'windows') { return ( { title="Show and dismiss picker!" /> + + + [android] show range picker + + + +