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)
}
}
- }
}
}