Skip to content

Commit

Permalink
feat(android): enable customizing first day of week (#902)
Browse files Browse the repository at this point in the history
* Added implementation for firstDayOfWeek for Android

* Fixed failing tests due to changes in App.js and fixed inconsistent test due to horizontal scroll

* Added e2e tests for firstDayOfWeek Android

* Fixed .java files formatting issues due to changes

* Fixed .java files formatting issues due to changes (2)

* Fixed failing test on iOS

* Added descriptions in type files and checked SDK version compatability

* Updated firstDayOfWeek in README.md

* Modified descriptions in type files

* Fixed failing e2e tests on Android

* Fixed failing e2e tests on iOS

* Fixed failing e2e tests on iOS (2)

* README.md changes as requested, detox tests modified to reduce repetitiveness and changed example App firstDayOfWeek selector to a FlatList

* Added helper function userSwipesTimezoneListUntilDesiredIsVisible
  • Loading branch information
JoshBarnesD committed Jun 3, 2024
1 parent da55211 commit 3b63563
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 26 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -424,7 +424,7 @@ Reference: https://docs.microsoft.com/en-us/uwp/api/windows.globalization.dateti
<RNDateTimePicker dateFormat="dayofweek day month" />
```

#### `firstDayOfWeek` (`optional`, `Windows only`)
#### `firstDayOfWeek` (`optional`, `Android and Windows only`)

Indicates which day is shown as the first day of the week.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))) {
Expand Down
53 changes: 51 additions & 2 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}',
Expand Down Expand Up @@ -168,7 +168,7 @@ export const App = () => {
: `${item} mins`
: item;
return (
<View style={{marginHorizontal: 1}}>
<View style={{marginHorizontal: 1}} testID={`${item}`}>
<Button
title={title || 'undefined'}
onPress={() => {
Expand All @@ -180,6 +180,20 @@ export const App = () => {
);
};

const renderDayOfWeekItem = ({item}) => {
const key = item[0];
const value = item[1];
return (
<View style={{marginHorizontal: 1}} testID={`${key}`}>
<Button
title={`${key}`}
value={value}
onPress={() => setFirstDayOfWeek(value)}
/>
</View>
);
};

const toggleMinMaxDateInUTC = () => {
setTzOffsetInMinutes(0);
setTzName(undefined);
Expand Down Expand Up @@ -253,6 +267,13 @@ export const App = () => {
/>
</>
)}
<Info
testID={'firstDayOfWeek'}
title={'First Day of Week:'}
body={`${Object.keys(DAY_OF_WEEK).find(
(key) => DAY_OF_WEEK[key] === firstDayOfWeek,
)}`}
/>
</View>
</View>
<ScrollView
Expand Down Expand Up @@ -334,6 +355,27 @@ export const App = () => {
testID="neutralButtonLabelTextInput"
/>
</View>

<View
style={{
flexDirection: 'column',
flexWrap: 'wrap',
paddingBottom: 10,
}}>
<ThemedText style={styles.textLabel}>
firstDayOfWeek (android only)
</ThemedText>
<View style={styles.firstDayOfWeekContainer}>
<FlatList
testID="firstDayOfWeekSelector"
style={{marginBottom: 10}}
horizontal={true}
renderItem={renderDayOfWeekItem}
data={Object.entries(DAY_OF_WEEK)}
/>
</View>
</View>

<View style={styles.header}>
<ThemedText style={styles.textLabel}>
[android] show and dismiss picker after 3 secs
Expand Down Expand Up @@ -410,6 +452,7 @@ export const App = () => {
neutralButton={{label: neutralButtonLabel}}
negativeButton={{label: 'Cancel', textColor: 'red'}}
disabled={disabled}
firstDayOfWeek={firstDayOfWeek}
/>
)}
</View>
Expand Down Expand Up @@ -631,6 +674,12 @@ const styles = StyleSheet.create({
paddingTop: 10,
width: 350,
},
firstDayOfWeekContainer: {
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 5,
},
});

export default App;
77 changes: 68 additions & 9 deletions example/e2e/detoxTest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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',
Expand All @@ -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()) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -275,6 +287,8 @@ describe('e2e tests', () => {
tzOffsetPreset = tzOffsetPreset.toUpperCase();
}

await elementById('DateTimePickerScrollView').scrollTo('top');

await userOpensPicker({
mode: 'time',
display: getPickerDisplay(),
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
});
},
);
});
});
Loading

0 comments on commit 3b63563

Please sign in to comment.