Skip to content
Open

Fix/#67 #1004

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -534,6 +535,14 @@ List of possible values:
<RNDateTimePicker fullscreen={true} />
```

#### `showYearPickerFirst` (`optional`, `Android only`)

If true, the date picker will open with the year selector first.

```js
<RNDateTimePicker showYearPickerFirst={true} />
```

#### `positiveButton` (`optional`, `Android only`)

Set the positive button label and text color.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's please add an early return if yearId == 0. It probably isn't needed but let's be on the safer side of things. Android is a mess so you never know.

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;

Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
)
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -42,6 +46,8 @@ class RNMaterialDatePicker(
setFullscreen()

datePicker = builder.build()

setYearPickerFirst()
}

private fun setInitialDate() {
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -386,6 +387,14 @@ export const App = () => {
<Switch value={isFullscreen} onValueChange={setIsFullscreen} />
</View>
</View>
<View style={styles.header}>
<ThemedText style={styles.textLabel}>
showYearPickerFirst (android only)
</ThemedText>
<View style={{flex: 1, alignItems: 'flex-start'}}>
<Switch value={showYearPickerFirst} onValueChange={setShowYearPickerFirst} />
</View>
</View>
<View style={styles.header}>
<ThemedText style={styles.textLabel}>
neutralButtonLabel (android only)
Expand Down Expand Up @@ -501,6 +510,7 @@ export const App = () => {
initialInputMode={isMaterialDesign ? inputMode : undefined}
design={design}
fullscreen={isMaterialDesign ? isFullscreen : undefined}
showYearPickerFirst={showYearPickerFirst}
/>
)}
</View>
Expand Down
2 changes: 2 additions & 0 deletions src/DateTimePickerAndroid.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down Expand Up @@ -97,6 +98,7 @@ function open(props: AndroidNativeProps) {
title,
initialInputMode,
fullscreen,
showYearPickerFirst,
});

switch (action) {
Expand Down
3 changes: 3 additions & 0 deletions src/androidUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type OpenParams = {
title: AndroidNativeProps['title'],
design: AndroidNativeProps['design'],
fullscreen: AndroidNativeProps['fullscreen'],
showYearPickerFirst: AndroidNativeProps['showYearPickerFirst'],
};

export type PresentPickerCallback =
Expand Down Expand Up @@ -88,6 +89,7 @@ function getOpenPicker(
title,
initialInputMode,
fullscreen,
showYearPickerFirst,
}: OpenParams) =>
// $FlowFixMe - `AbstractComponent` [1] is not an instance type.
pickers[ANDROID_MODE.date].open({
Expand All @@ -103,6 +105,7 @@ function getOpenPicker(
title,
initialInputMode,
fullscreen,
showYearPickerFirst,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/datetimepicker.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function RNDateTimePickerAndroid(
initialInputMode,
design,
fullscreen,
showYearPickerFirst,
} = props;
const valueTimestamp = value.getTime();

Expand Down Expand Up @@ -72,6 +73,7 @@ export default function RNDateTimePickerAndroid(
initialInputMode,
design,
fullscreen,
showYearPickerFirst,
};
DateTimePickerAndroid.open(params);
},
Expand Down
4 changes: 4 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
>;

Expand Down
1 change: 1 addition & 0 deletions src/specs/NativeModuleDatePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type DatePickerOpenParams = $ReadOnly<{
testID?: string,
timeZoneName?: number,
timeZoneOffsetInMinutes?: number,
showYearPickerFirst?: boolean,
}>;

type DateSetAction = 'dateSetAction' | 'dismissedAction';
Expand Down
1 change: 1 addition & 0 deletions src/specs/NativeModuleMaterialDatePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type DatePickerOpenParams = $ReadOnly<{
timeZoneName?: number,
timeZoneOffsetInMinutes?: number,
firstDayOfWeek?: number,
showYearPickerFirst?: boolean,
}>;

type DateSetAction = 'dateSetAction' | 'dismissedAction';
Expand Down
7 changes: 7 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down