Skip to content

Commit 8f684df

Browse files
authored
fix: Don't crash on unknown segment types in DateField (#8284)
1 parent 393c8b0 commit 8f684df

File tree

4 files changed

+50
-15
lines changed

4 files changed

+50
-15
lines changed

packages/@react-spectrum/datepicker/src/utils.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ export function useFormatHelpText(props: Pick<SpectrumDatePickerBase<any>, 'desc
3131
if (props.showFormatHelpText) {
3232
return (
3333
formatter.formatToParts(new Date()).map((s, i) => {
34-
if (s.type === 'literal') {
34+
if (s.type === 'literal' || s.type === 'unknown' || (s.type as string) === 'yearName') {
3535
return <span key={i}>{` ${s.value} `}</span>;
3636
}
3737

38-
return <span key={i} style={{unicodeBidi: 'embed', direction: 'ltr'}}>{displayNames.of(s.type)}</span>;
38+
let type = s.type as string === 'relatedYear' ? 'year' : s.type;
39+
return <span key={i} style={{unicodeBidi: 'embed', direction: 'ltr'}}>{displayNames.of(type)}</span>;
3940
})
4041
);
4142
}

packages/@react-spectrum/datepicker/test/DateField.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,18 @@ describe('DateField', function () {
238238
await user.keyboard('01011980');
239239
expect(tree.getByText('Date unavailable.')).toBeInTheDocument();
240240
});
241+
242+
it('does not crash on unknown segment types', async () => {
243+
let {getByRole} = render(
244+
<Provider theme={theme} locale="zh-CN-u-ca-chinese">
245+
<DateField label="Date" showFormatHelpText />
246+
</Provider>
247+
);
248+
249+
let segments = Array.from(getByRole('group').querySelectorAll('[data-testid]'));
250+
let segmentTypes = segments.map(s => s.getAttribute('data-testid'));
251+
expect(segmentTypes).toEqual(['year', 'month', 'day']);
252+
});
241253
});
242254

243255
describe('events', function () {

packages/@react-stately/datepicker/src/useDateFieldState.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,13 @@ const PAGE_STEP = {
118118
second: 15
119119
};
120120

121-
// Node seems to convert everything to lowercase...
122121
const TYPE_MAPPING = {
123-
dayperiod: 'dayPeriod'
122+
// Node seems to convert everything to lowercase...
123+
dayperiod: 'dayPeriod',
124+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts#named_years
125+
relatedYear: 'year',
126+
yearName: 'literal', // not editable
127+
unknown: 'literal'
124128
};
125129

126130
export interface DateFieldStateOptions<T extends DateValue = DateValue> extends DatePickerProps<T> {
@@ -207,7 +211,7 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
207211
let allSegments: Partial<typeof EDITABLE_SEGMENTS> = useMemo(() =>
208212
dateFormatter.formatToParts(new Date())
209213
.filter(seg => EDITABLE_SEGMENTS[seg.type])
210-
.reduce((p, seg) => (p[seg.type] = true, p), {})
214+
.reduce((p, seg) => (p[TYPE_MAPPING[seg.type] || seg.type] = true, p), {})
211215
, [dateFormatter]);
212216

213217
let [validSegments, setValidSegments] = useState<Partial<typeof EDITABLE_SEGMENTS>>(
@@ -413,18 +417,19 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
413417
let segments = dateFormatter.formatToParts(dateValue);
414418
let processedSegments: DateSegment[] = [];
415419
for (let segment of segments) {
416-
let isEditable = EDITABLE_SEGMENTS[segment.type];
417-
if (segment.type === 'era' && calendar.getEras().length === 1) {
420+
let type = TYPE_MAPPING[segment.type] || segment.type;
421+
let isEditable = EDITABLE_SEGMENTS[type];
422+
if (type === 'era' && calendar.getEras().length === 1) {
418423
isEditable = false;
419424
}
420425

421-
let isPlaceholder = EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type];
422-
let placeholder = EDITABLE_SEGMENTS[segment.type] ? getPlaceholder(segment.type, segment.value, locale) : null;
426+
let isPlaceholder = EDITABLE_SEGMENTS[type] && !validSegments[type];
427+
let placeholder = EDITABLE_SEGMENTS[type] ? getPlaceholder(type, segment.value, locale) : null;
423428

424429
let dateSegment = {
425-
type: TYPE_MAPPING[segment.type] || segment.type,
430+
type,
426431
text: isPlaceholder ? placeholder : segment.value,
427-
...getSegmentLimits(displayValue, segment.type, resolvedOptions),
432+
...getSegmentLimits(displayValue, type, resolvedOptions),
428433
isPlaceholder,
429434
placeholder,
430435
isEditable
@@ -433,7 +438,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
433438
// There is an issue in RTL languages where time fields render (minute:hour) instead of (hour:minute).
434439
// To force an LTR direction on the time field since, we wrap the time segments in LRI (left-to-right) isolate unicode. See https://www.w3.org/International/questions/qa-bidi-unicode-controls.
435440
// These unicode characters will be added to the array of processed segments as literals and will mark the start and end of the embedded direction change.
436-
if (segment.type === 'hour') {
441+
if (type === 'hour') {
437442
// This marks the start of the embedded direction change.
438443
processedSegments.push({
439444
type: 'literal',
@@ -445,7 +450,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
445450
});
446451
processedSegments.push(dateSegment);
447452
// This marks the end of the embedded direction change in the case that the granularity it set to "hour".
448-
if (segment.type === granularity) {
453+
if (type === granularity) {
449454
processedSegments.push({
450455
type: 'literal',
451456
text: '\u2069',
@@ -455,7 +460,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
455460
isEditable: false
456461
});
457462
}
458-
} else if (timeValue.includes(segment.type) && segment.type === granularity) {
463+
} else if (timeValue.includes(type) && type === granularity) {
459464
processedSegments.push(dateSegment);
460465
// This marks the end of the embedded direction change.
461466
processedSegments.push({

packages/react-aria-components/test/DateField.test.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {act, installPointerEvent, pointerMap, render, within} from '@react-spectrum/test-utils-internal';
1414
import {CalendarDate} from '@internationalized/date';
15-
import {DateField, DateFieldContext, DateInput, DateSegment, FieldError, Label, Text} from '../';
15+
import {DateField, DateFieldContext, DateInput, DateSegment, FieldError, I18nProvider, Label, Text} from '../';
1616
import React from 'react';
1717
import userEvent from '@testing-library/user-event';
1818

@@ -338,4 +338,21 @@ describe('DateField', () => {
338338
await user.keyboard('{backspace}');
339339
expect(document.activeElement).toBe(segments[0]);
340340
});
341+
342+
it('does not crash on unknown segment types', async () => {
343+
let {getByRole} = render(
344+
<I18nProvider locale="zh-CN-u-ca-chinese">
345+
<DateField defaultValue={new CalendarDate(2024, 12, 31)}>
346+
<Label>Birth date</Label>
347+
<DateInput>
348+
{segment => <DateSegment segment={segment} />}
349+
</DateInput>
350+
</DateField>
351+
</I18nProvider>
352+
);
353+
354+
let segments = Array.from(getByRole('group').querySelectorAll('.react-aria-DateSegment'));
355+
let segmentTypes = segments.map(s => s.getAttribute('data-type'));
356+
expect(segmentTypes).toEqual(['year', 'literal', 'month', 'day']);
357+
});
341358
});

0 commit comments

Comments
 (0)