Skip to content

Commit 3b63563

Browse files
authored
feat(android): enable customizing first day of week (#902)
* 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
1 parent da55211 commit 3b63563

File tree

13 files changed

+194
-26
lines changed

13 files changed

+194
-26
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,15 @@ Autolinking is not yet implemented on Windows, so [manual installation ](/docs/m
144144
If you are using RN >= 0.60, only run `npx pod-install`. Then rebuild your project.
145145

146146
## React Native Support
147-
Check the `react-native` version support table below to find the corrosponding `datetimepicker` version to meet support requirements.
148147

149-
| react-native version | version |
150-
| -------------------- | -------- |
151-
| 0.73.0+ | 7.6.3+ |
152-
| <=0.72.0 | <=7.6.2 |
153-
| 0.70.0+ | 7.0.1+ |
154-
| <0.70.0 | <=7.0.0 |
148+
Check the `react-native` version support table below to find the corresponding `datetimepicker` version to meet support requirements.
155149

150+
| react-native version | version |
151+
| -------------------- | ------- |
152+
| 0.73.0+ | 7.6.3+ |
153+
| <=0.72.0 | <=7.6.2 |
154+
| 0.70.0+ | 7.0.1+ |
155+
| <0.70.0 | <=7.0.0 |
156156

157157
## Usage
158158

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

427-
#### `firstDayOfWeek` (`optional`, `Windows only`)
427+
#### `firstDayOfWeek` (`optional`, `Android and Windows only`)
428428

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

android/src/main/java/com/reactcommunity/rndatetimepicker/DatePickerModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ private Bundle createFragmentArguments(ReadableMap options) {
184184
if (options.hasKey(RNConstants.ARG_TESTID) && !options.isNull(RNConstants.ARG_TESTID)) {
185185
args.putString(RNConstants.ARG_TESTID, options.getString(RNConstants.ARG_TESTID));
186186
}
187+
if (options.hasKey(RNConstants.FIRST_DAY_OF_WEEK) && !options.isNull(RNConstants.FIRST_DAY_OF_WEEK)) {
188+
// FIRST_DAY_OF_WEEK is 0-indexed, since it uses the same constants DAY_OF_WEEK used in the Windows implementation
189+
// Android DatePicker uses 1-indexed values, SUNDAY being 1 and SATURDAY being 7, so the +1 is necessary in this case
190+
args.putInt(RNConstants.FIRST_DAY_OF_WEEK, options.getInt(RNConstants.FIRST_DAY_OF_WEEK)+1);
191+
}
187192
return args;
188193
}
189194
}

android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public final class RNConstants {
1818
public static final String ACTION_TIME_SET = "timeSetAction";
1919
public static final String ACTION_DISMISSED = "dismissedAction";
2020
public static final String ACTION_NEUTRAL_BUTTON = "neutralButtonAction";
21+
public static final String FIRST_DAY_OF_WEEK = "firstDayOfWeek";
2122

2223
/**
2324
* Minimum date supported by {@link TimePickerDialog}, 01 Jan 1900

android/src/main/java/com/reactcommunity/rndatetimepicker/RNDatePickerDialogFragment.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,11 @@ private DatePickerDialog createDialog(Bundle args) {
116116
// the date under certain conditions.
117117
datePicker.setMinDate(RNConstants.DEFAULT_MIN_DATE);
118118
}
119-
if (args.containsKey(RNConstants.ARG_MAXDATE)) {
120-
datePicker.setMaxDate(maxDate);
119+
120+
// Only compatible with SDK 21 and above
121+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && args.containsKey(RNConstants.FIRST_DAY_OF_WEEK)) {
122+
final int firstDayOfWeek = args.getInt(RNConstants.FIRST_DAY_OF_WEEK);
123+
datePicker.setFirstDayOfWeek(firstDayOfWeek);
121124
}
122125

123126
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (args.containsKey(RNConstants.ARG_MAXDATE) || args.containsKey(RNConstants.ARG_MINDATE))) {

example/App.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export const App = () => {
111111
const [maxDate] = useState(new Date('2021'));
112112
const [minDate] = useState(new Date('2018'));
113113
const [is24Hours, set24Hours] = useState(false);
114-
const [firstDayOfWeek, setFirstDayOfWeek] = useState(DAY_OF_WEEK.Monday);
114+
const [firstDayOfWeek, setFirstDayOfWeek] = useState(DAY_OF_WEEK.Sunday);
115115
const [dateFormat, setDateFormat] = useState('longdate');
116116
const [dayOfWeekFormat, setDayOfWeekFormat] = useState(
117117
'{dayofweek.abbreviated(2)}',
@@ -168,7 +168,7 @@ export const App = () => {
168168
: `${item} mins`
169169
: item;
170170
return (
171-
<View style={{marginHorizontal: 1}}>
171+
<View style={{marginHorizontal: 1}} testID={`${item}`}>
172172
<Button
173173
title={title || 'undefined'}
174174
onPress={() => {
@@ -180,6 +180,20 @@ export const App = () => {
180180
);
181181
};
182182

183+
const renderDayOfWeekItem = ({item}) => {
184+
const key = item[0];
185+
const value = item[1];
186+
return (
187+
<View style={{marginHorizontal: 1}} testID={`${key}`}>
188+
<Button
189+
title={`${key}`}
190+
value={value}
191+
onPress={() => setFirstDayOfWeek(value)}
192+
/>
193+
</View>
194+
);
195+
};
196+
183197
const toggleMinMaxDateInUTC = () => {
184198
setTzOffsetInMinutes(0);
185199
setTzName(undefined);
@@ -253,6 +267,13 @@ export const App = () => {
253267
/>
254268
</>
255269
)}
270+
<Info
271+
testID={'firstDayOfWeek'}
272+
title={'First Day of Week:'}
273+
body={`${Object.keys(DAY_OF_WEEK).find(
274+
(key) => DAY_OF_WEEK[key] === firstDayOfWeek,
275+
)}`}
276+
/>
256277
</View>
257278
</View>
258279
<ScrollView
@@ -334,6 +355,27 @@ export const App = () => {
334355
testID="neutralButtonLabelTextInput"
335356
/>
336357
</View>
358+
359+
<View
360+
style={{
361+
flexDirection: 'column',
362+
flexWrap: 'wrap',
363+
paddingBottom: 10,
364+
}}>
365+
<ThemedText style={styles.textLabel}>
366+
firstDayOfWeek (android only)
367+
</ThemedText>
368+
<View style={styles.firstDayOfWeekContainer}>
369+
<FlatList
370+
testID="firstDayOfWeekSelector"
371+
style={{marginBottom: 10}}
372+
horizontal={true}
373+
renderItem={renderDayOfWeekItem}
374+
data={Object.entries(DAY_OF_WEEK)}
375+
/>
376+
</View>
377+
</View>
378+
337379
<View style={styles.header}>
338380
<ThemedText style={styles.textLabel}>
339381
[android] show and dismiss picker after 3 secs
@@ -410,6 +452,7 @@ export const App = () => {
410452
neutralButton={{label: neutralButtonLabel}}
411453
negativeButton={{label: 'Cancel', textColor: 'red'}}
412454
disabled={disabled}
455+
firstDayOfWeek={firstDayOfWeek}
413456
/>
414457
)}
415458
</View>
@@ -631,6 +674,12 @@ const styles = StyleSheet.create({
631674
paddingTop: 10,
632675
width: 350,
633676
},
677+
firstDayOfWeekContainer: {
678+
flexDirection: 'row',
679+
justifyContent: 'center',
680+
flexWrap: 'wrap',
681+
gap: 5,
682+
},
634683
});
635684

636685
export default App;

example/e2e/detoxTest.spec.js

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ const {
55
getDatePickerAndroid,
66
getDateTimePickerControlIOS,
77
getInlineTimePickerIOS,
8+
getDatePickerButtonIOS,
89
} = require('./utils/matchers');
910
const {
1011
userChangesTimeValue,
1112
userOpensPicker,
1213
userTapsCancelButtonAndroid,
1314
userTapsOkButtonAndroid,
14-
userDismissesCompactDatePicker,
15+
userSelectsDayInCalendar,
16+
userSwipesTimezoneListUntilDesiredIsVisible,
1517
} = require('./utils/actions');
1618
const {isIOS, isAndroid, wait, Platform} = require('./utils/utils');
1719
const {device} = require('detox');
@@ -63,15 +65,15 @@ describe('e2e tests', () => {
6365
await userOpensPicker({mode: 'date', display: 'default'});
6466

6567
if (isIOS()) {
66-
await element(
67-
by.traits(['staticText']).withAncestor(by.label('Date Picker')),
68-
).tap();
68+
await elementById('DateTimePickerScrollView').scrollTo('bottom');
69+
await getDatePickerButtonIOS().tap();
70+
6971
// 'label' maps to 'description' in view hierarchy debugger
7072
const nextMonthArrow = element(by.label('Next Month'));
7173

7274
await nextMonthArrow.tap();
7375
await nextMonthArrow.tap();
74-
await userDismissesCompactDatePicker();
76+
await getDatePickerButtonIOS().tap();
7577
} else {
7678
const calendarHorizontalScrollView = element(
7779
by
@@ -119,9 +121,11 @@ describe('e2e tests', () => {
119121
ios: 'inline',
120122
android: 'default',
121123
});
124+
await elementById('DateTimePickerScrollView').scrollTo('top');
122125
await userOpensPicker({mode: 'time', display});
123126

124127
if (isIOS()) {
128+
await elementById('DateTimePickerScrollView').scrollTo('bottom');
125129
await expect(getInlineTimePickerIOS()).toBeVisible();
126130
} else {
127131
await expect(element(by.type('android.widget.TimePicker'))).toBeVisible();
@@ -165,16 +169,19 @@ describe('e2e tests', () => {
165169

166170
await expect(elementById('overriddenTzName')).toHaveText('Europe/Prague');
167171

168-
await elementById('timezone').swipe('left', 'fast', 0.5);
172+
await elementById('DateTimePickerScrollView').scrollTo('bottom');
169173

170174
let timeZone = 'America/Vancouver';
175+
await waitFor(elementById('timezone')).toBeVisible().withTimeout(1000);
176+
await userSwipesTimezoneListUntilDesiredIsVisible(timeZone);
177+
171178
if (isAndroid()) {
172179
timeZone = timeZone.toUpperCase();
173180
}
174181

175182
await waitFor(elementByText(timeZone)).toBeVisible().withTimeout(1000);
176183

177-
await elementByText(timeZone).tap();
184+
await elementByText(timeZone).multiTap(2);
178185

179186
await assertTimeLabels({
180187
utcTime: '2021-11-13T01:00:00Z',
@@ -188,17 +195,21 @@ describe('e2e tests', () => {
188195
});
189196

190197
it('daylight saving should work properly', async () => {
191-
await elementById('timezone').swipe('left', 'fast', 0.5);
198+
await elementById('DateTimePickerScrollView').scrollTo('bottom');
192199

193200
let timeZone = 'America/Vancouver';
201+
await waitFor(elementById('timezone')).toBeVisible().withTimeout(1000);
202+
await userSwipesTimezoneListUntilDesiredIsVisible(timeZone);
203+
194204
if (isAndroid()) {
195205
timeZone = timeZone.toUpperCase();
196206
}
197207

198208
await waitFor(elementByText(timeZone)).toBeVisible().withTimeout(1000);
199209

200-
await elementByText(timeZone).tap();
210+
await elementByText(timeZone).multiTap(2);
201211

212+
await elementById('DateTimePickerScrollView').scrollTo('top');
202213
await userOpensPicker({mode: 'date', display: getPickerDisplay()});
203214

204215
if (isIOS()) {
@@ -228,6 +239,7 @@ describe('e2e tests', () => {
228239
await uiDevice.pressEnter();
229240
await userTapsOkButtonAndroid();
230241

242+
await elementById('DateTimePickerScrollView').scrollTo('top');
231243
await userOpensPicker({mode: 'time', display: getPickerDisplay()});
232244
await userChangesTimeValue({hours: '2', minutes: '0'});
233245
await userTapsOkButtonAndroid();
@@ -275,6 +287,8 @@ describe('e2e tests', () => {
275287
tzOffsetPreset = tzOffsetPreset.toUpperCase();
276288
}
277289

290+
await elementById('DateTimePickerScrollView').scrollTo('top');
291+
278292
await userOpensPicker({
279293
mode: 'time',
280294
display: getPickerDisplay(),
@@ -310,7 +324,9 @@ describe('e2e tests', () => {
310324
await expect(elementById('utcTime')).toHaveText('2021-11-13T00:00:00Z');
311325

312326
// Ensure you can select tomorrow (iOS)
327+
await elementById('DateTimePickerScrollView').scrollTo('top');
313328
await userOpensPicker({mode: 'date', display: getPickerDisplay()});
329+
await elementById('DateTimePickerScrollView').scrollTo('bottom');
314330
await testElement.setDatePickerDate('2021-11-14T01:00:00Z', 'ISO8601');
315331
} else {
316332
const uiDevice = device.getUiDevice();
@@ -445,4 +461,47 @@ describe('e2e tests', () => {
445461
});
446462
});
447463
});
464+
465+
describe(':android: firstDayOfWeek functionality', () => {
466+
it.each([
467+
{
468+
firstDayOfWeekIn: 'Sunday',
469+
selectDayPositions: {xPosIn: -2, yPosIn: 4},
470+
},
471+
{
472+
firstDayOfWeekIn: 'Tuesday',
473+
selectDayPositions: {xPosIn: 3, yPosIn: 3},
474+
},
475+
])(
476+
':android: picker should have $firstDayOfWeekIn as firstDayOfWeek and select Sunday date',
477+
async ({firstDayOfWeekIn, selectDayPositions}) => {
478+
const targetDate = '2021-11-07T01:00:00Z';
479+
const targetDateWithTZ = '2021-11-07T02:00:00+01:00';
480+
481+
await userOpensPicker({
482+
mode: 'date',
483+
display: getPickerDisplay(),
484+
firstDayOfWeek: firstDayOfWeekIn,
485+
});
486+
await expect(getDatePickerAndroid()).toBeVisible();
487+
488+
const uiDevice = device.getUiDevice();
489+
await userSelectsDayInCalendar(uiDevice, {
490+
xPos: selectDayPositions.xPosIn,
491+
yPos: selectDayPositions.yPosIn,
492+
});
493+
494+
await userTapsOkButtonAndroid();
495+
496+
await expect(elementById('firstDayOfWeek')).toHaveText(
497+
firstDayOfWeekIn,
498+
);
499+
500+
await assertTimeLabels({
501+
utcTime: targetDate,
502+
deviceTime: targetDateWithTZ,
503+
});
504+
},
505+
);
506+
});
448507
});

0 commit comments

Comments
 (0)