From d589750b63ad0996cd601a3701e69db864f10f8d Mon Sep 17 00:00:00 2001 From: TatsunoriMorita <114038079+TatsunoriMorita@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:50:54 +0900 Subject: [PATCH 1/6] add option to show year selection view first. --- .../rndatetimepicker/Common.java | 30 +++++++++++++ .../rndatetimepicker/RNConstants.java | 1 + .../RNDatePickerDialogFragment.java | 11 ++++- .../rndatetimepicker/RNMaterialDatePicker.kt | 42 +++++++++++++++++++ src/DateTimePickerAndroid.android.js | 2 + src/androidUtils.js | 3 ++ src/datetimepicker.android.js | 2 + src/index.d.ts | 4 ++ src/specs/NativeModuleDatePicker.js | 1 + src/specs/NativeModuleMaterialDatePicker.js | 1 + src/types.js | 7 ++++ 11 files changed, 103 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java index ca6b2e06..68233cff 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java @@ -1,13 +1,16 @@ package com.reactcommunity.rndatetimepicker; import android.app.AlertDialog; +import android.app.DatePickerDialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.Color; import android.os.Bundle; import android.util.TypedValue; +import android.view.View; import android.widget.Button; +import android.widget.DatePicker; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; @@ -92,6 +95,30 @@ public static DialogInterface.OnShowListener setButtonTextColor(@NonNull final C }; } + @NonNull + public static DialogInterface.OnShowListener openYearDialog(final AlertDialog dialog, final boolean canOpenYearDialog, final boolean showYearPickerFirst) { + return dialogInterface -> { + if (canOpenYearDialog && showYearPickerFirst && dialog instanceof DatePickerDialog datePickerDialog) { + DatePicker datePicker = datePickerDialog.getDatePicker(); + + int yearId = Resources.getSystem().getIdentifier("date_picker_header_year", "id", "android"); + View yearView = datePicker.findViewById(yearId); + if (yearView != null) { + yearView.performClick(); + } + } + }; + } + + @NonNull + public static DialogInterface.OnShowListener combine(@NonNull DialogInterface.OnShowListener... listeners) { + return dialogInterface -> { + for (DialogInterface.OnShowListener l : listeners) { + if (l != null) l.onShow(dialogInterface); + } + }; + } + private static void setTextColor(Button button, String buttonKey, final Bundle args, final boolean needsColorOverride, int textColorPrimary) { if (button == null) return; @@ -245,6 +272,9 @@ public static Bundle createDatePickerArguments(ReadableMap options) { // 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); } + if (options.hasKey(RNConstants.ARG_SHOW_YEAR_PICKER_FIRST) && !options.isNull(RNConstants.ARG_SHOW_YEAR_PICKER_FIRST)) { + args.putBoolean(RNConstants.ARG_SHOW_YEAR_PICKER_FIRST, options.getBoolean(RNConstants.ARG_SHOW_YEAR_PICKER_FIRST)); + } return args; } diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java index 07220b79..1384daa3 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java @@ -22,6 +22,7 @@ public final class RNConstants { public static final String ACTION_DISMISSED = "dismissedAction"; public static final String ACTION_NEUTRAL_BUTTON = "neutralButtonAction"; public static final String FIRST_DAY_OF_WEEK = "firstDayOfWeek"; + public static final String ARG_SHOW_YEAR_PICKER_FIRST = "showYearPickerFirst"; /** * Minimum date supported by {@link TimePickerDialog}, 01 Jan 1900 diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java index 357a67b0..5799b1dd 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java @@ -7,7 +7,9 @@ package com.reactcommunity.rndatetimepicker; +import static com.reactcommunity.rndatetimepicker.Common.combine; import static com.reactcommunity.rndatetimepicker.Common.getDisplayDate; +import static com.reactcommunity.rndatetimepicker.Common.openYearDialog; import static com.reactcommunity.rndatetimepicker.Common.setButtonTextColor; import static com.reactcommunity.rndatetimepicker.Common.setButtonTitles; @@ -101,7 +103,14 @@ private DatePickerDialog createDialog(Bundle args) { if (activityContext != null) { RNDatePickerDisplay display = getDisplayDate(args); boolean needsColorOverride = display == RNDatePickerDisplay.SPINNER; - dialog.setOnShowListener(setButtonTextColor(activityContext, dialog, args, needsColorOverride)); + boolean canOpenYearDialog = display == RNDatePickerDisplay.DEFAULT; + boolean showYearPickerFirst = args.getBoolean(RNConstants.ARG_SHOW_YEAR_PICKER_FIRST); + dialog.setOnShowListener( + combine( + openYearDialog(dialog, canOpenYearDialog, showYearPickerFirst), + setButtonTextColor(activityContext, dialog, args, needsColorOverride) + ) + ); } } diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt index 9dece7a1..d0acdf8a 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt @@ -3,6 +3,10 @@ package com.reactcommunity.rndatetimepicker import android.content.DialogInterface import android.os.Bundle import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentManager import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext @@ -42,6 +46,8 @@ class RNMaterialDatePicker( setFullscreen() datePicker = builder.build() + + setYearPickerFirst() } private fun setInitialDate() { @@ -108,6 +114,42 @@ class RNMaterialDatePicker( } } + private fun setYearPickerFirst() { + val showYearPickerFirst = args.getBoolean(RNConstants.ARG_SHOW_YEAR_PICKER_FIRST) + if (!showYearPickerFirst) return + val initialDate = RNDate(args) + val activity = reactContext.currentActivity as? AppCompatActivity + activity?.let { lifecycleOwner -> + datePicker!!.viewLifecycleOwnerLiveData.observe(lifecycleOwner) { owner -> + if (owner != null) { + datePicker?.requireDialog()?.window?.decorView?.post { + val root = datePicker!!.dialog?.window?.decorView ?: return@post + + val yearText = initialDate.year().toString() + val hit = findViewBy(root) { v -> + v is TextView && v.isShown && v.isClickable && v.text?.toString()?.contains(yearText) == true + } + if (hit != null) { + hit.performClick() + return@post + } + } + } + } + } + } + + private fun findViewBy(root: View, pred: (View) -> Boolean): View? { + if (pred(root)) return root + + if (root is ViewGroup) { + for (i in 0 until root.childCount) { + findViewBy(root.getChildAt(i), pred)?.let { return it } + } + } + return null + } + private fun obtainMaterialThemeOverlayId(resId: Int): Int { val theme = reactContext.currentActivity?.theme ?: run { return resId diff --git a/src/DateTimePickerAndroid.android.js b/src/DateTimePickerAndroid.android.js index 51d657f1..b00c653d 100644 --- a/src/DateTimePickerAndroid.android.js +++ b/src/DateTimePickerAndroid.android.js @@ -51,6 +51,7 @@ function open(props: AndroidNativeProps) { initialInputMode, design, fullscreen, + showYearPickerFirst, } = props; validateAndroidProps(props); invariant(originalValue, 'A date or time must be specified as `value` prop.'); @@ -97,6 +98,7 @@ function open(props: AndroidNativeProps) { title, initialInputMode, fullscreen, + showYearPickerFirst, }); switch (action) { diff --git a/src/androidUtils.js b/src/androidUtils.js index 6601214f..72b2df09 100644 --- a/src/androidUtils.js +++ b/src/androidUtils.js @@ -38,6 +38,7 @@ type OpenParams = { title: AndroidNativeProps['title'], design: AndroidNativeProps['design'], fullscreen: AndroidNativeProps['fullscreen'], + showYearPickerFirst: AndroidNativeProps['showYearPickerFirst'], }; export type PresentPickerCallback = @@ -88,6 +89,7 @@ function getOpenPicker( title, initialInputMode, fullscreen, + showYearPickerFirst, }: OpenParams) => // $FlowFixMe - `AbstractComponent` [1] is not an instance type. pickers[ANDROID_MODE.date].open({ @@ -103,6 +105,7 @@ function getOpenPicker( title, initialInputMode, fullscreen, + showYearPickerFirst, }); } } diff --git a/src/datetimepicker.android.js b/src/datetimepicker.android.js index 21868da3..fff6277f 100644 --- a/src/datetimepicker.android.js +++ b/src/datetimepicker.android.js @@ -37,6 +37,7 @@ export default function RNDateTimePickerAndroid( initialInputMode, design, fullscreen, + showYearPickerFirst, } = props; const valueTimestamp = value.getTime(); @@ -72,6 +73,7 @@ export default function RNDateTimePickerAndroid( initialInputMode, design, fullscreen, + showYearPickerFirst, }; DateTimePickerAndroid.open(params); }, diff --git a/src/index.d.ts b/src/index.d.ts index 701ddb68..e0819701 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -203,6 +203,10 @@ export type AndroidNativeProps = Readonly< * Use Material 3 pickers or the default ones */ design?: Design; + /** + * Show the year picker first when opening the calendar dialog. + */ + showYearPickerFirst?: boolean; } >; diff --git a/src/specs/NativeModuleDatePicker.js b/src/specs/NativeModuleDatePicker.js index eae787f4..070264d9 100644 --- a/src/specs/NativeModuleDatePicker.js +++ b/src/specs/NativeModuleDatePicker.js @@ -11,6 +11,7 @@ export type DatePickerOpenParams = $ReadOnly<{ testID?: string, timeZoneName?: number, timeZoneOffsetInMinutes?: number, + showYearPickerFirst?: boolean, }>; type DateSetAction = 'dateSetAction' | 'dismissedAction'; diff --git a/src/specs/NativeModuleMaterialDatePicker.js b/src/specs/NativeModuleMaterialDatePicker.js index 7121ecfb..a7ff9367 100644 --- a/src/specs/NativeModuleMaterialDatePicker.js +++ b/src/specs/NativeModuleMaterialDatePicker.js @@ -14,6 +14,7 @@ export type DatePickerOpenParams = $ReadOnly<{ timeZoneName?: number, timeZoneOffsetInMinutes?: number, firstDayOfWeek?: number, + showYearPickerFirst?: boolean, }>; type DateSetAction = 'dateSetAction' | 'dismissedAction'; diff --git a/src/types.js b/src/types.js index 8c5f04be..d513d12a 100644 --- a/src/types.js +++ b/src/types.js @@ -218,6 +218,13 @@ export type AndroidNativeProps = $ReadOnly<{| */ design?: 'default' | 'material', + /** + * If true, the date picker will open with the year selector first. + * + * Only supported for default pickers. + */ + showYearPickerFirst?: boolean, + /** * The interval at which minutes can be selected. * From e12fa5fe825ae1b2ab778adad955f93efad2523a Mon Sep 17 00:00:00 2001 From: TatsunoriMorita <114038079+TatsunoriMorita@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:51:37 +0900 Subject: [PATCH 2/6] add showYearPickerFirst option to example. --- example/App.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/example/App.js b/example/App.js index ae542c80..debb8716 100644 --- a/example/App.js +++ b/example/App.js @@ -106,6 +106,7 @@ export const App = () => { const [neutralButtonLabel, setNeutralButtonLabel] = useState(undefined); const [disabled, setDisabled] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); + const [showYearPickerFirst, setShowYearPickerFirst] = useState(false); const [minimumDate, setMinimumDate] = useState(); const [maximumDate, setMaximumDate] = useState(); const [design, setDesign] = useState(DESIGNS[0]); @@ -386,6 +387,14 @@ export const App = () => { + + + showYearPickerFirst (android only) + + + + + neutralButtonLabel (android only) @@ -501,6 +510,7 @@ export const App = () => { initialInputMode={isMaterialDesign ? inputMode : undefined} design={design} fullscreen={isMaterialDesign ? isFullscreen : undefined} + showYearPickerFirst={showYearPickerFirst} /> )} From b21c38f8077822668f3f66753069bbe188eebe7d Mon Sep 17 00:00:00 2001 From: TatsunoriMorita <114038079+TatsunoriMorita@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:40:10 +0900 Subject: [PATCH 3/6] add readme. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 9a6542cc..5a326ecd 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ React Native date & time picker component for iOS, Android and Windows (please n - [`initialInputMode` (`optional`, `Android only`)](#initialinputmode-optional-android-only) - [`title` (`optional`, `Android only`)](#title-optional-android-only) - [`fullscreen` (`optional`, `Android only`)](#fullscreen-optional-android-only) + - [`showYearPickerFirst` (`optional`, `Android only`)](#showyearpickerfirst-optional-android-only) - [`onChange` (`optional`)](#onchange-optional) - [`value` (`required`)](#value-required) - [`maximumDate` (`optional`)](#maximumdate-optional) @@ -534,6 +535,14 @@ List of possible values: ``` +#### `showYearPickerFirst` (`optional`, `Android only`) + +If true, the date picker will open with the year selector first. + +```js + +``` + #### `positiveButton` (`optional`, `Android only`) Set the positive button label and text color. From 1eb1136b1e1c417a54efa3d6389ad0556860bf82 Mon Sep 17 00:00:00 2001 From: TatsunoriMorita <114038079+TatsunoriMorita@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:49:35 +0900 Subject: [PATCH 4/6] Add a gourd clause. https://github.com/react-native-datetimepicker/datetimepicker/pull/1004#discussion_r2321816300 --- .../main/java/com/reactcommunity/rndatetimepicker/Common.java | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java index 68233cff..e86df397 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java @@ -102,6 +102,7 @@ public static DialogInterface.OnShowListener openYearDialog(final AlertDialog di DatePicker datePicker = datePickerDialog.getDatePicker(); int yearId = Resources.getSystem().getIdentifier("date_picker_header_year", "id", "android"); + if (yearId == 0) return; View yearView = datePicker.findViewById(yearId); if (yearView != null) { yearView.performClick(); From fe3ebaa1cac7584e158c84c9dff599608eaadcca Mon Sep 17 00:00:00 2001 From: TatsunoriMorita <114038079+TatsunoriMorita@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:51:35 +0900 Subject: [PATCH 5/6] Merge variables. https://github.com/react-native-datetimepicker/datetimepicker/pull/1004#discussion_r2321834001 --- .../java/com/reactcommunity/rndatetimepicker/Common.java | 4 ++-- .../rndatetimepicker/RNDatePickerDialogFragment.java | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java index e86df397..d47a9e24 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java @@ -96,9 +96,9 @@ public static DialogInterface.OnShowListener setButtonTextColor(@NonNull final C } @NonNull - public static DialogInterface.OnShowListener openYearDialog(final AlertDialog dialog, final boolean canOpenYearDialog, final boolean showYearPickerFirst) { + public static DialogInterface.OnShowListener openYearDialog(final AlertDialog dialog, final boolean canOpenYearDialog) { return dialogInterface -> { - if (canOpenYearDialog && showYearPickerFirst && dialog instanceof DatePickerDialog datePickerDialog) { + if (canOpenYearDialog && dialog instanceof DatePickerDialog datePickerDialog) { DatePicker datePicker = datePickerDialog.getDatePicker(); int yearId = Resources.getSystem().getIdentifier("date_picker_header_year", "id", "android"); diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java index 5799b1dd..f4c7db55 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java @@ -103,11 +103,10 @@ private DatePickerDialog createDialog(Bundle args) { if (activityContext != null) { RNDatePickerDisplay display = getDisplayDate(args); boolean needsColorOverride = display == RNDatePickerDisplay.SPINNER; - boolean canOpenYearDialog = display == RNDatePickerDisplay.DEFAULT; - boolean showYearPickerFirst = args.getBoolean(RNConstants.ARG_SHOW_YEAR_PICKER_FIRST); + boolean canOpenYearDialog = display == RNDatePickerDisplay.DEFAULT && args.getBoolean(RNConstants.ARG_SHOW_YEAR_PICKER_FIRST); dialog.setOnShowListener( combine( - openYearDialog(dialog, canOpenYearDialog, showYearPickerFirst), + openYearDialog(dialog, canOpenYearDialog), setButtonTextColor(activityContext, dialog, args, needsColorOverride) ) ); From d23930a1527dfc8f63856dd5e2587e5bfd54cd7c Mon Sep 17 00:00:00 2001 From: TatsunoriMorita <114038079+TatsunoriMorita@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:53:14 +0900 Subject: [PATCH 6/6] Fix year section display logic. - Remove no force unwrap - Add a guard clause - Add observer cleanup https://github.com/react-native-datetimepicker/datetimepicker/pull/1004#discussion_r2321810515 --- .../rndatetimepicker/RNMaterialDatePicker.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt index d0acdf8a..6239bf6f 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMaterialDatePicker.kt @@ -120,22 +120,25 @@ class RNMaterialDatePicker( val initialDate = RNDate(args) val activity = reactContext.currentActivity as? AppCompatActivity activity?.let { lifecycleOwner -> - datePicker!!.viewLifecycleOwnerLiveData.observe(lifecycleOwner) { owner -> - if (owner != null) { - datePicker?.requireDialog()?.window?.decorView?.post { - val root = datePicker!!.dialog?.window?.decorView ?: return@post + val picker = datePicker ?: return@let + val liveData = picker.viewLifecycleOwnerLiveData + liveData.observe(lifecycleOwner) { owner -> + if (owner == null) return@observe + picker.requireDialog().window?.decorView?.post { + val root = picker.dialog?.window?.decorView ?: return@post val yearText = initialDate.year().toString() val hit = findViewBy(root) { v -> - v is TextView && v.isShown && v.isClickable && v.text?.toString()?.contains(yearText) == true + v is TextView && v.isShown && v.isClickable && v.text?.toString() + ?.contains(yearText) == true } if (hit != null) { hit.performClick() return@post } + liveData.removeObservers(lifecycleOwner) } } - } } }