Skip to content

Commit 08db857

Browse files
sterlingwesvonovak
andauthored
fix(ios): avoid min>max date picker crash (#1009)
* fix(ios): avoid min>max date picker crash * fix zero value handling * update example, spec * Update src/utils.js * Update test/utils.test.js --------- Co-authored-by: Vojtech Novak <[email protected]>
1 parent 9c1db66 commit 08db857

File tree

5 files changed

+113
-9
lines changed

5 files changed

+113
-9
lines changed

example/App.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,24 @@ export const App = () => {
471471
title="toggleMinMaxDate"
472472
/>
473473
</View>
474+
{/* This button allows for testing the error box that should show when the minimumDate prop is greater than the maximumDate prop */}
475+
<View style={styles.button}>
476+
<Button
477+
testID="setInvertedMinMax"
478+
onPress={() => {
479+
if (minimumDate && maximumDate && minimumDate > maximumDate) {
480+
setMinimumDate(undefined);
481+
setMaximumDate(undefined);
482+
setShow(false);
483+
} else {
484+
setMinimumDate(new Date('2025-09-05'));
485+
setMaximumDate(new Date('2025-09-01'));
486+
setShow(true);
487+
}
488+
}}
489+
title={minimumDate && maximumDate && minimumDate > maximumDate ? "undo min > max" : "set min > max (errors)"}
490+
/>
491+
</View>
474492
<View style={{flexDirection: 'row', alignItems: 'center'}}>
475493
{/* This label ensures there is no regression in this former bug: https://github.com/react-native-datetimepicker/datetimepicker/issues/409 */}
476494
<Text style={{flexShrink: 1}}>

ios/fabric/RNDateTimePickerComponentView.mm

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,27 @@ - (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared cons
174174
needsToUpdateMeasurements = true;
175175
}
176176

177-
if (oldPickerProps.minimumDate != newPickerProps.minimumDate) {
178-
NSDate *minimumDate = convertJSTimeToDate(newPickerProps.minimumDate);
179-
picker.minimumDate = adjustMinimumDate(minimumDate, newPickerProps.minuteInterval);
180-
}
181-
182-
if (oldPickerProps.maximumDate != newPickerProps.maximumDate) {
183-
picker.maximumDate = convertJSTimeToDate(newPickerProps.maximumDate);
177+
Boolean minDateChanged = oldPickerProps.minimumDate != newPickerProps.minimumDate;
178+
Boolean maxDateChanged = oldPickerProps.maximumDate != newPickerProps.maximumDate;
179+
180+
if (minDateChanged || maxDateChanged) {
181+
NSDate *newMinDate = newPickerProps.minimumDate ? convertJSTimeToDate(newPickerProps.minimumDate) : nil;
182+
NSDate *newMaxDate = newPickerProps.maximumDate ? convertJSTimeToDate(newPickerProps.maximumDate) : nil;
183+
184+
if (newMinDate) {
185+
newMinDate = adjustMinimumDate(newMinDate, newPickerProps.minuteInterval);
186+
}
187+
188+
// avoid crash when min > max by ensuring a clean initial state
189+
picker.minimumDate = nil;
190+
picker.maximumDate = nil;
191+
192+
// set the dates in all cases (whether unset/nil, some set, or both set)
193+
// UNLESS min > max, then we leave them as nil and rely on our LogBox in JS
194+
if (!newMinDate || !newMaxDate || [newMinDate compare:newMaxDate] != NSOrderedDescending) {
195+
picker.minimumDate = newMinDate;
196+
picker.maximumDate = newMaxDate;
197+
}
184198
}
185199

186200
if (oldPickerProps.locale != newPickerProps.locale) {

src/datetimepicker.ios.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default function Picker({
6161
disabled = false,
6262
...other
6363
}: IOSNativeProps): React.Node {
64-
sharedPropsValidation({value, timeZoneOffsetInMinutes, timeZoneName});
64+
sharedPropsValidation({value, timeZoneOffsetInMinutes, timeZoneName, minimumDate, maximumDate});
6565

6666
const display = getDisplaySafe(providedDisplay);
6767

src/utils.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,14 @@ export function sharedPropsValidation({
3535
value,
3636
timeZoneName,
3737
timeZoneOffsetInMinutes,
38+
minimumDate,
39+
maximumDate,
3840
}: {
3941
value: Date,
4042
timeZoneName?: ?string,
4143
timeZoneOffsetInMinutes?: ?number,
44+
minimumDate?: ?Date,
45+
maximumDate?: ?Date,
4246
}) {
4347
invariant(value, 'A date or time must be specified as `value` prop');
4448
invariant(
@@ -49,6 +53,14 @@ export function sharedPropsValidation({
4953
timeZoneName == null || timeZoneOffsetInMinutes == null,
5054
'`timeZoneName` and `timeZoneOffsetInMinutes` cannot be specified at the same time',
5155
);
56+
57+
if (minimumDate && maximumDate) {
58+
invariant(
59+
minimumDate <= maximumDate,
60+
`DateTimePicker: minimumDate (${minimumDate.toISOString()}) is after maximumDate (${maximumDate.toISOString()}). Ensure minimumDate < maximumDate.`,
61+
);
62+
}
63+
5264
if (timeZoneOffsetInMinutes !== undefined) {
5365
console.warn(
5466
'`timeZoneOffsetInMinutes` is deprecated and will be removed in a future release. Use `timeZoneName` instead.',

test/utils.test.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {toMilliseconds} from '../src/utils.js';
1+
import {toMilliseconds, sharedPropsValidation} from '../src/utils.js';
22

33
describe('utils', () => {
44
describe('toMilliseconds', () => {
@@ -17,4 +17,64 @@ describe('utils', () => {
1717
expect(options).toHaveProperty('maximumDate', 2556057600000);
1818
});
1919
});
20+
21+
describe('sharedPropsValidation', () => {
22+
describe('minimumDate and maximumDate validation', () => {
23+
it('should not throw when dates are in correct order', () => {
24+
const value = new Date('2023-06-15');
25+
const minimumDate = new Date('2023-01-01');
26+
const maximumDate = new Date('2023-12-31');
27+
28+
expect(() => {
29+
sharedPropsValidation({value, minimumDate, maximumDate});
30+
}).not.toThrow();
31+
});
32+
33+
it('should not throw when dates are equal', () => {
34+
const value = new Date('2023-06-15');
35+
const minimumDate = new Date('2023-06-15');
36+
const maximumDate = new Date('2023-06-15');
37+
38+
expect(() => {
39+
sharedPropsValidation({value, minimumDate, maximumDate});
40+
}).not.toThrow();
41+
});
42+
43+
it('should not throw when only minimumDate is provided', () => {
44+
const value = new Date('2023-06-15');
45+
const minimumDate = new Date('2023-01-01');
46+
47+
expect(() => {
48+
sharedPropsValidation({value, minimumDate});
49+
}).not.toThrow();
50+
});
51+
52+
it('should not throw when only maximumDate is provided', () => {
53+
const value = new Date('2023-06-15');
54+
const maximumDate = new Date('2023-12-31');
55+
56+
expect(() => {
57+
sharedPropsValidation({value, maximumDate});
58+
}).not.toThrow();
59+
});
60+
61+
it('should not throw when neither minimumDate nor maximumDate is provided', () => {
62+
const value = new Date('2023-06-15');
63+
64+
expect(() => {
65+
sharedPropsValidation({value});
66+
}).not.toThrow();
67+
});
68+
69+
it('should throw when minimumDate is after maximumDate', () => {
70+
const value = new Date('2023-06-15');
71+
const minimumDate = new Date('2023-12-31');
72+
const maximumDate = new Date('2023-01-01');
73+
74+
expect(() => {
75+
sharedPropsValidation({value, minimumDate, maximumDate});
76+
}).toThrow('DateTimePicker: minimumDate (2023-12-31T00:00:00.000Z) is after maximumDate (2023-01-01T00:00:00.000Z). Ensure minimumDate < maximumDate.');
77+
});
78+
});
79+
});
2080
});

0 commit comments

Comments
 (0)