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. diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/Common.java index ca6b2e06..d47a9e24 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,31 @@ public static DialogInterface.OnShowListener setButtonTextColor(@NonNull final C }; } + @NonNull + public static DialogInterface.OnShowListener openYearDialog(final AlertDialog dialog, final boolean canOpenYearDialog) { + return dialogInterface -> { + if (canOpenYearDialog && dialog instanceof DatePickerDialog datePickerDialog) { + 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(); + } + } + }; + } + + @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 +273,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..f4c7db55 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,13 @@ 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 && args.getBoolean(RNConstants.ARG_SHOW_YEAR_PICKER_FIRST); + dialog.setOnShowListener( + combine( + openYearDialog(dialog, canOpenYearDialog), + 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..6239bf6f 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,45 @@ 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 -> + 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 + } + if (hit != null) { + hit.performClick() + return@post + } + liveData.removeObservers(lifecycleOwner) + } + } + } + } + + 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/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} /> )} 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. *