Skip to content

Commit 1d16513

Browse files
authored
feat: Support entering invalid dates in DateField and constrain on blur (#9510)
* feat: Support entering invalid dates in DateField and constrain on blur * Fix crash when switching calendar systems * Refactor to support entering leading zeros
1 parent 7d1c780 commit 1d16513

13 files changed

Lines changed: 696 additions & 370 deletions

File tree

packages/@internationalized/date/src/calendars/EthiopicCalendar.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ export class EthiopicCalendar implements Calendar {
100100
return 365 + getLeapDay(date.year);
101101
}
102102

103+
getMaximumMonthsInYear(): number {
104+
return 13;
105+
}
106+
107+
getMaximumDaysInMonth(): number {
108+
return 30;
109+
}
110+
103111
getYearsInEra(date: AnyCalendarDate): number {
104112
// 9999-12-31 gregorian is 9992-20-02 ethiopic.
105113
// Round down to 9991 for the last full year.

packages/@internationalized/date/src/calendars/GregorianCalendar.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ export class GregorianCalendar implements Calendar {
113113
return isLeapYear(date.year) ? 366 : 365;
114114
}
115115

116+
getMaximumMonthsInYear(): number {
117+
return 12;
118+
}
119+
120+
getMaximumDaysInMonth(): number {
121+
return 31;
122+
}
123+
116124
// eslint-disable-next-line @typescript-eslint/no-unused-vars
117125
getYearsInEra(date: AnyCalendarDate): number {
118126
return 9999;

packages/@internationalized/date/src/calendars/HebrewCalendar.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@ export class HebrewCalendar implements Calendar {
180180
return getDaysInYear(date.year);
181181
}
182182

183+
getMaximumMonthsInYear(): number {
184+
return 13;
185+
}
186+
187+
getMaximumDaysInMonth(): number {
188+
return 30;
189+
}
190+
183191
getYearsInEra(): number {
184192
// 6239 gregorian
185193
return 9999;

packages/@internationalized/date/src/calendars/IslamicCalendar.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ export class IslamicCivilCalendar implements Calendar {
7777
return isLeapYear(date.year) ? 355 : 354;
7878
}
7979

80+
getMaximumMonthsInYear(): number {
81+
return 12;
82+
}
83+
84+
getMaximumDaysInMonth(): number {
85+
return 30;
86+
}
87+
8088
getYearsInEra(): number {
8189
// 9999 gregorian
8290
return 9665;

packages/@internationalized/date/src/calendars/PersianCalendar.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ export class PersianCalendar implements Calendar {
8080
return isLeapYear ? 30 : 29;
8181
}
8282

83+
getMaximumMonthsInYear(): number {
84+
return 12;
85+
}
86+
87+
getMaximumDaysInMonth(): number {
88+
return 31;
89+
}
90+
8391
getEras(): string[] {
8492
return ['AP'];
8593
}

packages/@internationalized/date/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ export interface Calendar {
7474
* eras may begin in the middle of a month.
7575
*/
7676
getMinimumDayInMonth?(date: AnyCalendarDate): number,
77+
/** Returns the maximum months across all years. */
78+
getMaximumMonthsInYear(): number,
79+
/** Returns the maximum days across all months. */
80+
getMaximumDaysInMonth(): number,
7781
/**
7882
* Returns a date that is the first day of the month for the given date.
7983
* This is used to determine the month that the given date falls in, if

packages/@react-aria/datepicker/src/useDateSegment.ts

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
5757
// The ARIA spec says aria-valuenow is optional if there's no value, but aXe seems to require it.
5858
// This doesn't seem to have any negative effects with real AT since we also use aria-valuetext.
5959
// https://github.com/dequelabs/axe-core/issues/3505
60-
value: segment.value,
60+
value: segment.value ?? undefined,
6161
textValue,
6262
minValue: segment.minValue,
6363
maxValue: segment.maxValue,
@@ -82,15 +82,11 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
8282
},
8383
onIncrementToMax: () => {
8484
enteredKeys.current = '';
85-
if (segment.maxValue !== undefined) {
86-
state.setSegment(segment.type, segment.maxValue);
87-
}
85+
state.incrementToMax(segment.type);
8886
},
8987
onDecrementToMin: () => {
9088
enteredKeys.current = '';
91-
if (segment.minValue !== undefined) {
92-
state.setSegment(segment.type, segment.minValue);
93-
}
89+
state.decrementToMin(segment.type);
9490
}
9591
});
9692

@@ -110,7 +106,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
110106
state.setSegment(segment.type, parsed);
111107
}
112108
enteredKeys.current = newValue;
113-
} else if (segment.type === 'dayPeriod') {
109+
} else if (segment.type === 'dayPeriod' || segment.type === 'era') {
114110
state.clearSegment(segment.type);
115111
}
116112
};
@@ -193,7 +189,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
193189
if (startsWith(am, key)) {
194190
state.setSegment('dayPeriod', 0);
195191
} else if (startsWith(pm, key)) {
196-
state.setSegment('dayPeriod', 12);
192+
state.setSegment('dayPeriod', 1);
197193
} else {
198194
break;
199195
}
@@ -219,43 +215,19 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
219215

220216
let numberValue = parser.parse(newValue);
221217
let segmentValue = numberValue;
222-
let allowsZero = segment.minValue === 0;
223-
if (segment.type === 'hour' && state.dateFormatter.resolvedOptions().hour12) {
224-
switch (state.dateFormatter.resolvedOptions().hourCycle) {
225-
case 'h11':
226-
if (numberValue > 11) {
227-
segmentValue = parser.parse(key);
228-
}
229-
break;
230-
case 'h12':
231-
allowsZero = false;
232-
if (numberValue > 12) {
233-
segmentValue = parser.parse(key);
234-
}
235-
break;
236-
}
237-
238-
if (segment.value !== undefined && segment.value >= 12 && numberValue > 1) {
239-
numberValue += 12;
240-
}
241-
} else if (segment.maxValue !== undefined && numberValue > segment.maxValue) {
218+
if (segment.maxValue !== undefined && numberValue > segment.maxValue) {
242219
segmentValue = parser.parse(key);
243220
}
244221

245222
if (isNaN(numberValue)) {
246223
return;
247224
}
248225

249-
let shouldSetValue = segmentValue !== 0 || allowsZero;
250-
if (shouldSetValue) {
251-
state.setSegment(segment.type, segmentValue);
252-
}
226+
state.setSegment(segment.type, segmentValue);
253227

254228
if (segment.maxValue !== undefined && (Number(numberValue + '0') > segment.maxValue || newValue.length >= String(segment.maxValue).length)) {
255229
enteredKeys.current = '';
256-
if (shouldSetValue) {
257-
focusManager.focusNext();
258-
}
230+
focusManager.focusNext();
259231
} else {
260232
enteredKeys.current = newValue;
261233
}

0 commit comments

Comments
 (0)