diff --git a/README.md b/README.md
index c089e0ae..0e39d4e1 100644
--- a/README.md
+++ b/README.md
@@ -144,15 +144,15 @@ Autolinking is not yet implemented on Windows, so [manual installation ](/docs/m
If you are using RN >= 0.60, only run `npx pod-install`. Then rebuild your project.
## React Native Support
-Check the `react-native` version support table below to find the corrosponding `datetimepicker` version to meet support requirements.
-| react-native version | version |
-| -------------------- | -------- |
-| 0.73.0+ | 7.6.3+ |
-| <=0.72.0 | <=7.6.2 |
-| 0.70.0+ | 7.0.1+ |
-| <0.70.0 | <=7.0.0 |
+Check the `react-native` version support table below to find the corresponding `datetimepicker` version to meet support requirements.
+| react-native version | version |
+| -------------------- | ------- |
+| 0.73.0+ | 7.6.3+ |
+| <=0.72.0 | <=7.6.2 |
+| 0.70.0+ | 7.0.1+ |
+| <0.70.0 | <=7.0.0 |
## Usage
@@ -424,7 +424,7 @@ Reference: https://docs.microsoft.com/en-us/uwp/api/windows.globalization.dateti
```
-#### `firstDayOfWeek` (`optional`, `Windows only`)
+#### `firstDayOfWeek` (`optional`, `Android and Windows only`)
Indicates which day is shown as the first day of the week.
diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java
index c7272205..6eec370b 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java
@@ -184,6 +184,11 @@ private Bundle createFragmentArguments(ReadableMap options) {
if (options.hasKey(RNConstants.ARG_TESTID) && !options.isNull(RNConstants.ARG_TESTID)) {
args.putString(RNConstants.ARG_TESTID, options.getString(RNConstants.ARG_TESTID));
}
+ if (options.hasKey(RNConstants.FIRST_DAY_OF_WEEK) && !options.isNull(RNConstants.FIRST_DAY_OF_WEEK)) {
+ // FIRST_DAY_OF_WEEK is 0-indexed, since it uses the same constants DAY_OF_WEEK used in the Windows implementation
+ // 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);
+ }
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 99530d51..acb56cb9 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java
@@ -18,6 +18,7 @@ public final class RNConstants {
public static final String ACTION_TIME_SET = "timeSetAction";
public static final String ACTION_DISMISSED = "dismissedAction";
public static final String ACTION_NEUTRAL_BUTTON = "neutralButtonAction";
+ public static final String FIRST_DAY_OF_WEEK = "firstDayOfWeek";
/**
* 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 72de7cbb..eeb19702 100644
--- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java
+++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java
@@ -116,8 +116,11 @@ private DatePickerDialog createDialog(Bundle args) {
// the date under certain conditions.
datePicker.setMinDate(RNConstants.DEFAULT_MIN_DATE);
}
- if (args.containsKey(RNConstants.ARG_MAXDATE)) {
- datePicker.setMaxDate(maxDate);
+
+ // Only compatible with SDK 21 and above
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && args.containsKey(RNConstants.FIRST_DAY_OF_WEEK)) {
+ final int firstDayOfWeek = args.getInt(RNConstants.FIRST_DAY_OF_WEEK);
+ datePicker.setFirstDayOfWeek(firstDayOfWeek);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (args.containsKey(RNConstants.ARG_MAXDATE) || args.containsKey(RNConstants.ARG_MINDATE))) {
diff --git a/example/App.js b/example/App.js
index 2190714c..6788cdee 100644
--- a/example/App.js
+++ b/example/App.js
@@ -111,7 +111,7 @@ export const App = () => {
const [maxDate] = useState(new Date('2021'));
const [minDate] = useState(new Date('2018'));
const [is24Hours, set24Hours] = useState(false);
- const [firstDayOfWeek, setFirstDayOfWeek] = useState(DAY_OF_WEEK.Monday);
+ const [firstDayOfWeek, setFirstDayOfWeek] = useState(DAY_OF_WEEK.Sunday);
const [dateFormat, setDateFormat] = useState('longdate');
const [dayOfWeekFormat, setDayOfWeekFormat] = useState(
'{dayofweek.abbreviated(2)}',
@@ -168,7 +168,7 @@ export const App = () => {
: `${item} mins`
: item;
return (
-
+
{
testID="neutralButtonLabelTextInput"
/>
+
+
+
+ firstDayOfWeek (android only)
+
+
+
+
+
+
[android] show and dismiss picker after 3 secs
@@ -410,6 +452,7 @@ export const App = () => {
neutralButton={{label: neutralButtonLabel}}
negativeButton={{label: 'Cancel', textColor: 'red'}}
disabled={disabled}
+ firstDayOfWeek={firstDayOfWeek}
/>
)}
@@ -631,6 +674,12 @@ const styles = StyleSheet.create({
paddingTop: 10,
width: 350,
},
+ firstDayOfWeekContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ flexWrap: 'wrap',
+ gap: 5,
+ },
});
export default App;
diff --git a/example/e2e/detoxTest.spec.js b/example/e2e/detoxTest.spec.js
index 3c6b63b1..e42d39ba 100644
--- a/example/e2e/detoxTest.spec.js
+++ b/example/e2e/detoxTest.spec.js
@@ -5,13 +5,15 @@ const {
getDatePickerAndroid,
getDateTimePickerControlIOS,
getInlineTimePickerIOS,
+ getDatePickerButtonIOS,
} = require('./utils/matchers');
const {
userChangesTimeValue,
userOpensPicker,
userTapsCancelButtonAndroid,
userTapsOkButtonAndroid,
- userDismissesCompactDatePicker,
+ userSelectsDayInCalendar,
+ userSwipesTimezoneListUntilDesiredIsVisible,
} = require('./utils/actions');
const {isIOS, isAndroid, wait, Platform} = require('./utils/utils');
const {device} = require('detox');
@@ -63,15 +65,15 @@ describe('e2e tests', () => {
await userOpensPicker({mode: 'date', display: 'default'});
if (isIOS()) {
- await element(
- by.traits(['staticText']).withAncestor(by.label('Date Picker')),
- ).tap();
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
+ await getDatePickerButtonIOS().tap();
+
// 'label' maps to 'description' in view hierarchy debugger
const nextMonthArrow = element(by.label('Next Month'));
await nextMonthArrow.tap();
await nextMonthArrow.tap();
- await userDismissesCompactDatePicker();
+ await getDatePickerButtonIOS().tap();
} else {
const calendarHorizontalScrollView = element(
by
@@ -119,9 +121,11 @@ describe('e2e tests', () => {
ios: 'inline',
android: 'default',
});
+ await elementById('DateTimePickerScrollView').scrollTo('top');
await userOpensPicker({mode: 'time', display});
if (isIOS()) {
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
await expect(getInlineTimePickerIOS()).toBeVisible();
} else {
await expect(element(by.type('android.widget.TimePicker'))).toBeVisible();
@@ -165,16 +169,19 @@ describe('e2e tests', () => {
await expect(elementById('overriddenTzName')).toHaveText('Europe/Prague');
- await elementById('timezone').swipe('left', 'fast', 0.5);
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
let timeZone = 'America/Vancouver';
+ await waitFor(elementById('timezone')).toBeVisible().withTimeout(1000);
+ await userSwipesTimezoneListUntilDesiredIsVisible(timeZone);
+
if (isAndroid()) {
timeZone = timeZone.toUpperCase();
}
await waitFor(elementByText(timeZone)).toBeVisible().withTimeout(1000);
- await elementByText(timeZone).tap();
+ await elementByText(timeZone).multiTap(2);
await assertTimeLabels({
utcTime: '2021-11-13T01:00:00Z',
@@ -188,17 +195,21 @@ describe('e2e tests', () => {
});
it('daylight saving should work properly', async () => {
- await elementById('timezone').swipe('left', 'fast', 0.5);
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
let timeZone = 'America/Vancouver';
+ await waitFor(elementById('timezone')).toBeVisible().withTimeout(1000);
+ await userSwipesTimezoneListUntilDesiredIsVisible(timeZone);
+
if (isAndroid()) {
timeZone = timeZone.toUpperCase();
}
await waitFor(elementByText(timeZone)).toBeVisible().withTimeout(1000);
- await elementByText(timeZone).tap();
+ await elementByText(timeZone).multiTap(2);
+ await elementById('DateTimePickerScrollView').scrollTo('top');
await userOpensPicker({mode: 'date', display: getPickerDisplay()});
if (isIOS()) {
@@ -228,6 +239,7 @@ describe('e2e tests', () => {
await uiDevice.pressEnter();
await userTapsOkButtonAndroid();
+ await elementById('DateTimePickerScrollView').scrollTo('top');
await userOpensPicker({mode: 'time', display: getPickerDisplay()});
await userChangesTimeValue({hours: '2', minutes: '0'});
await userTapsOkButtonAndroid();
@@ -275,6 +287,8 @@ describe('e2e tests', () => {
tzOffsetPreset = tzOffsetPreset.toUpperCase();
}
+ await elementById('DateTimePickerScrollView').scrollTo('top');
+
await userOpensPicker({
mode: 'time',
display: getPickerDisplay(),
@@ -310,7 +324,9 @@ describe('e2e tests', () => {
await expect(elementById('utcTime')).toHaveText('2021-11-13T00:00:00Z');
// Ensure you can select tomorrow (iOS)
+ await elementById('DateTimePickerScrollView').scrollTo('top');
await userOpensPicker({mode: 'date', display: getPickerDisplay()});
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
await testElement.setDatePickerDate('2021-11-14T01:00:00Z', 'ISO8601');
} else {
const uiDevice = device.getUiDevice();
@@ -445,4 +461,47 @@ describe('e2e tests', () => {
});
});
});
+
+ describe(':android: firstDayOfWeek functionality', () => {
+ it.each([
+ {
+ firstDayOfWeekIn: 'Sunday',
+ selectDayPositions: {xPosIn: -2, yPosIn: 4},
+ },
+ {
+ firstDayOfWeekIn: 'Tuesday',
+ selectDayPositions: {xPosIn: 3, yPosIn: 3},
+ },
+ ])(
+ ':android: picker should have $firstDayOfWeekIn as firstDayOfWeek and select Sunday date',
+ async ({firstDayOfWeekIn, selectDayPositions}) => {
+ const targetDate = '2021-11-07T01:00:00Z';
+ const targetDateWithTZ = '2021-11-07T02:00:00+01:00';
+
+ await userOpensPicker({
+ mode: 'date',
+ display: getPickerDisplay(),
+ firstDayOfWeek: firstDayOfWeekIn,
+ });
+ await expect(getDatePickerAndroid()).toBeVisible();
+
+ const uiDevice = device.getUiDevice();
+ await userSelectsDayInCalendar(uiDevice, {
+ xPos: selectDayPositions.xPosIn,
+ yPos: selectDayPositions.yPosIn,
+ });
+
+ await userTapsOkButtonAndroid();
+
+ await expect(elementById('firstDayOfWeek')).toHaveText(
+ firstDayOfWeekIn,
+ );
+
+ await assertTimeLabels({
+ utcTime: targetDate,
+ deviceTime: targetDateWithTZ,
+ });
+ },
+ );
+ });
});
diff --git a/example/e2e/utils/actions.js b/example/e2e/utils/actions.js
index cca2b178..b87cd77d 100644
--- a/example/e2e/utils/actions.js
+++ b/example/e2e/utils/actions.js
@@ -1,3 +1,5 @@
+const {elementById} = require('./matchers');
+
async function userChangesTimeValue(
{hours, minutes} = {hours: undefined, minutes: undefined},
) {
@@ -23,14 +25,27 @@ async function userChangesTimeValue(
}
}
-async function userOpensPicker({mode, display, interval, tzOffsetPreset}) {
+async function userOpensPicker({
+ mode,
+ display,
+ interval,
+ tzOffsetPreset,
+ firstDayOfWeek,
+}) {
+ await elementById('DateTimePickerScrollView').scrollTo('top');
+
await element(by.text(mode)).tap();
await element(by.text(display)).tap();
if (interval) {
await element(by.text(String(interval))).tap();
}
if (tzOffsetPreset) {
+ await elementById('DateTimePickerScrollView').scrollTo('bottom');
await element(by.text(tzOffsetPreset)).tap();
+ await elementById('DateTimePickerScrollView').scrollTo('top');
+ }
+ if (firstDayOfWeek) {
+ await element(by.id(firstDayOfWeek)).tap();
}
await element(by.id('showPickerButton')).tap();
}
@@ -56,8 +71,26 @@ async function userTapsOkButtonAndroid() {
await okButton.tap();
}
-async function userDismissesCompactDatePicker() {
- await element(by.type('_UIDatePickerContainerView')).tap();
+async function userSwipesTimezoneListUntilDesiredIsVisible(timeZone) {
+ await waitFor(elementById(timeZone))
+ .toBeVisible()
+ .whileElement(by.id('timezone'))
+ .scroll(200, 'right');
+}
+
+// Helper function to select a day in the calendar
+// A negative number xPos and yPos means we go left and up respectively
+// A positive number xPos and yPos means we go right and down respectively
+async function userSelectsDayInCalendar(uiDevice, {xPos, yPos}) {
+ for (let i = 0; i < Math.abs(yPos); i++) {
+ yPos < 0 ? await uiDevice.pressDPadUp(i) : await uiDevice.pressDPadDown(i);
+ }
+ for (let j = 0; j < Math.abs(xPos); j++) {
+ xPos < 0
+ ? await uiDevice.pressDPadLeft(j)
+ : await uiDevice.pressDPadRight(j);
+ }
+ await uiDevice.pressEnter();
}
module.exports = {
@@ -65,5 +98,6 @@ module.exports = {
userTapsCancelButtonAndroid,
userTapsOkButtonAndroid,
userChangesTimeValue,
- userDismissesCompactDatePicker,
+ userSelectsDayInCalendar,
+ userSwipesTimezoneListUntilDesiredIsVisible,
};
diff --git a/example/e2e/utils/matchers.js b/example/e2e/utils/matchers.js
index 02cf20c0..74af7541 100644
--- a/example/e2e/utils/matchers.js
+++ b/example/e2e/utils/matchers.js
@@ -10,6 +10,8 @@ const getDateTimePickerControlIOS = () => element(by.type('UIDatePicker'));
const getDatePickerAndroid = () => element(by.id('dateTimePicker'));
+const getDatePickerButtonIOS = () => element(by.id('dateTimePicker'));
+
module.exports = {
elementById,
elementByText,
@@ -17,4 +19,5 @@ module.exports = {
getDateTimePickerControlIOS,
getDatePickerAndroid,
getInlineTimePickerIOS,
+ getDatePickerButtonIOS,
};
diff --git a/src/DateTimePickerAndroid.android.js b/src/DateTimePickerAndroid.android.js
index 9b495114..282800e0 100644
--- a/src/DateTimePickerAndroid.android.js
+++ b/src/DateTimePickerAndroid.android.js
@@ -42,6 +42,7 @@ function open(props: AndroidNativeProps) {
positiveButtonLabel,
negativeButtonLabel,
testID,
+ firstDayOfWeek,
} = props;
validateAndroidProps(props);
invariant(originalValue, 'A date or time must be specified as `value` prop.');
@@ -84,6 +85,7 @@ function open(props: AndroidNativeProps) {
timeZoneName,
dialogButtons,
testID,
+ firstDayOfWeek,
});
switch (action) {
diff --git a/src/androidUtils.js b/src/androidUtils.js
index 9442cf6f..1cc8936a 100644
--- a/src/androidUtils.js
+++ b/src/androidUtils.js
@@ -26,6 +26,7 @@ type OpenParams = {
timeZoneOffsetInMinutes: AndroidNativeProps['timeZoneOffsetInMinutes'],
timeZoneName: AndroidNativeProps['timeZoneName'],
testID: AndroidNativeProps['testID'],
+ firstDayOfWeek: AndroidNativeProps['firstDayOfWeek'],
dialogButtons: {
positive: ProcessedButton,
negative: ProcessedButton,
@@ -70,6 +71,7 @@ function getOpenPicker(
timeZoneName,
dialogButtons,
testID,
+ firstDayOfWeek,
}: OpenParams) =>
// $FlowFixMe - `AbstractComponent` [1] is not an instance type.
pickers[ANDROID_MODE.date].open({
@@ -81,6 +83,7 @@ function getOpenPicker(
timeZoneName,
dialogButtons,
testID,
+ firstDayOfWeek,
});
}
}
diff --git a/src/datetimepicker.android.js b/src/datetimepicker.android.js
index c530d70e..9f17d7ef 100644
--- a/src/datetimepicker.android.js
+++ b/src/datetimepicker.android.js
@@ -32,6 +32,7 @@ export default function RNDateTimePickerAndroid(
negativeButtonLabel,
neutralButtonLabel,
testID,
+ firstDayOfWeek,
} = props;
const valueTimestamp = value.getTime();
@@ -62,6 +63,7 @@ export default function RNDateTimePickerAndroid(
negativeButtonLabel,
neutralButtonLabel,
testID,
+ firstDayOfWeek,
};
DateTimePickerAndroid.open(params);
},
diff --git a/src/index.d.ts b/src/index.d.ts
index a3c346ad..93467898 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -167,7 +167,10 @@ export type AndroidNativeProps = Readonly<
* @deprecated use negativeButton instead
* */
negativeButtonLabel?: string;
-
+ /**
+ * Sets the first day of the week shown in the calendar
+ */
+ firstDayOfWeek?: DAY_OF_WEEK;
/**
* callback when an error occurs inside the date picker native code (such as null activity)
*/
diff --git a/src/types.js b/src/types.js
index 521c1f82..377b4bfd 100644
--- a/src/types.js
+++ b/src/types.js
@@ -205,6 +205,10 @@ export type AndroidNativeProps = $ReadOnly<{|
* @deprecated use negativeButton instead
* */
negativeButtonLabel?: string,
+ /**
+ * Sets the first day of the week shown in the calendar
+ */
+ firstDayOfWeek?: typeof DAY_OF_WEEK,
onError?: (Error) => void,
|}>;