Skip to content
Open
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
18 changes: 18 additions & 0 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,24 @@ export const App = () => {
title="toggleMinMaxDate"
/>
</View>
{/* This button allows for testing the error box that should show when the minimumDate prop is greater than the maximumDate prop */}
<View style={styles.button}>
<Button
testID="setInvertedMinMax"
onPress={() => {
if (minimumDate && maximumDate && minimumDate > maximumDate) {
setMinimumDate(undefined);
setMaximumDate(undefined);
setShow(false);
} else {
setMinimumDate(new Date('2025-09-05'));
setMaximumDate(new Date('2025-09-01'));
setShow(true);
}
}}
title={minimumDate && maximumDate && minimumDate > maximumDate ? "undo min > max" : "set min > max (errors)"}
/>
</View>
<View style={{flexDirection: 'row', alignItems: 'center'}}>
{/* This label ensures there is no regression in this former bug: https://github.com/react-native-datetimepicker/datetimepicker/issues/409 */}
<Text style={{flexShrink: 1}}>
Expand Down
28 changes: 21 additions & 7 deletions ios/fabric/RNDateTimePickerComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,27 @@ - (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared cons
needsToUpdateMeasurements = true;
}

if (oldPickerProps.minimumDate != newPickerProps.minimumDate) {
NSDate *minimumDate = convertJSTimeToDate(newPickerProps.minimumDate);
picker.minimumDate = adjustMinimumDate(minimumDate, newPickerProps.minuteInterval);
}

if (oldPickerProps.maximumDate != newPickerProps.maximumDate) {
picker.maximumDate = convertJSTimeToDate(newPickerProps.maximumDate);
Copy link
Author

Choose a reason for hiding this comment

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

while my reproducer is a naive example, i believe the behaviour leading to the crash was less obvious b/c of this UIDatePicker instance re-update logic:

if a datepicker is rendered (inline on a screen or sheet for example, as is the case in the reproducer), and the component is re-rendered, the crash would occur when either minimumDate or maximumDate is updated in a way that breaks the expected constraint order (min < max) but the prior picker value for one of the props is not unset first

since these conditionals were evaluated independently this seems like the culprit, hence why I've grouped them together to validate them together

Boolean minDateChanged = oldPickerProps.minimumDate != newPickerProps.minimumDate;
Boolean maxDateChanged = oldPickerProps.maximumDate != newPickerProps.maximumDate;

if (minDateChanged || maxDateChanged) {
NSDate *newMinDate = newPickerProps.minimumDate ? convertJSTimeToDate(newPickerProps.minimumDate) : nil;
NSDate *newMaxDate = newPickerProps.maximumDate ? convertJSTimeToDate(newPickerProps.maximumDate) : nil;

if (newMinDate) {
newMinDate = adjustMinimumDate(newMinDate, newPickerProps.minuteInterval);
}

// avoid crash when min > max by ensuring a clean initial state
picker.minimumDate = nil;
picker.maximumDate = nil;

// set the dates in all cases (whether unset/nil, some set, or both set)
// UNLESS min > max, then we leave them as nil and rely on our LogBox in JS
if (!newMinDate || !newMaxDate || [newMinDate compare:newMaxDate] != NSOrderedDescending) {
picker.minimumDate = newMinDate;
picker.maximumDate = newMaxDate;
}
}

if (oldPickerProps.locale != newPickerProps.locale) {
Expand Down
2 changes: 1 addition & 1 deletion src/datetimepicker.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default function Picker({
disabled = false,
...other
}: IOSNativeProps): React.Node {
sharedPropsValidation({value, timeZoneOffsetInMinutes, timeZoneName});
sharedPropsValidation({value, timeZoneOffsetInMinutes, timeZoneName, minimumDate, maximumDate});

const display = getDisplaySafe(providedDisplay);

Expand Down
12 changes: 12 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,14 @@ export function sharedPropsValidation({
value,
timeZoneName,
timeZoneOffsetInMinutes,
minimumDate,
maximumDate,
}: {
value: Date,
timeZoneName?: ?string,
timeZoneOffsetInMinutes?: ?number,
minimumDate?: ?Date,
maximumDate?: ?Date,
}) {
invariant(value, 'A date or time must be specified as `value` prop');
invariant(
Expand All @@ -49,6 +53,14 @@ export function sharedPropsValidation({
timeZoneName == null || timeZoneOffsetInMinutes == null,
'`timeZoneName` and `timeZoneOffsetInMinutes` cannot be specified at the same time',
);

if (minimumDate && maximumDate) {
invariant(
minimumDate <= maximumDate,
`DateTimePicker: minimumDate (${minimumDate.toISOString()}) is after maximumDate (${maximumDate.toISOString()}). Please ensure minimumDate < maximumDate.`,
);
}

if (timeZoneOffsetInMinutes !== undefined) {
console.warn(
'`timeZoneOffsetInMinutes` is deprecated and will be removed in a future release. Use `timeZoneName` instead.',
Expand Down
62 changes: 61 additions & 1 deletion test/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {toMilliseconds} from '../src/utils.js';
import {toMilliseconds, sharedPropsValidation} from '../src/utils.js';

describe('utils', () => {
describe('toMilliseconds', () => {
Expand All @@ -17,4 +17,64 @@ describe('utils', () => {
expect(options).toHaveProperty('maximumDate', 2556057600000);
});
});

describe('sharedPropsValidation', () => {
describe('minimumDate and maximumDate validation', () => {
it('should not throw when dates are in correct order', () => {
const value = new Date('2023-06-15');
const minimumDate = new Date('2023-01-01');
const maximumDate = new Date('2023-12-31');

expect(() => {
sharedPropsValidation({value, minimumDate, maximumDate});
}).not.toThrow();
});

it('should not throw when dates are equal', () => {
const value = new Date('2023-06-15');
const minimumDate = new Date('2023-06-15');
const maximumDate = new Date('2023-06-15');

expect(() => {
sharedPropsValidation({value, minimumDate, maximumDate});
}).not.toThrow();
});

it('should not throw when only minimumDate is provided', () => {
const value = new Date('2023-06-15');
const minimumDate = new Date('2023-01-01');

expect(() => {
sharedPropsValidation({value, minimumDate});
}).not.toThrow();
});

it('should not throw when only maximumDate is provided', () => {
const value = new Date('2023-06-15');
const maximumDate = new Date('2023-12-31');

expect(() => {
sharedPropsValidation({value, maximumDate});
}).not.toThrow();
});

it('should not throw when neither minimumDate nor maximumDate is provided', () => {
const value = new Date('2023-06-15');

expect(() => {
sharedPropsValidation({value});
}).not.toThrow();
});

it('should throw when minimumDate is after maximumDate', () => {
const value = new Date('2023-06-15');
const minimumDate = new Date('2023-12-31');
const maximumDate = new Date('2023-01-01');

expect(() => {
sharedPropsValidation({value, minimumDate, maximumDate});
}).toThrow('DateTimePicker: minimumDate (2023-12-31T00:00:00.000Z) is after maximumDate (2023-01-01T00:00:00.000Z). Please ensure minimumDate < maximumDate.');
});
});
});
});