Skip to content

fix: Don't crash on unknown segment types in DateField #8284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 23, 2025
Merged
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
5 changes: 3 additions & 2 deletions packages/@react-spectrum/datepicker/src/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export function useFormatHelpText(props: Pick<SpectrumDatePickerBase<any>, 'desc
if (props.showFormatHelpText) {
return (
formatter.formatToParts(new Date()).map((s, i) => {
if (s.type === 'literal') {
if (s.type === 'literal' || s.type === 'unknown' || (s.type as string) === 'yearName') {
return <span key={i}>{` ${s.value} `}</span>;
}

return <span key={i} style={{unicodeBidi: 'embed', direction: 'ltr'}}>{displayNames.of(s.type)}</span>;
let type = s.type as string === 'relatedYear' ? 'year' : s.type;
return <span key={i} style={{unicodeBidi: 'embed', direction: 'ltr'}}>{displayNames.of(type)}</span>;
})
);
}
Expand Down
12 changes: 12 additions & 0 deletions packages/@react-spectrum/datepicker/test/DateField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,18 @@ describe('DateField', function () {
await user.keyboard('01011980');
expect(tree.getByText('Date unavailable.')).toBeInTheDocument();
});

it('does not crash on unknown segment types', async () => {
let {getByRole} = render(
<Provider theme={theme} locale="zh-CN-u-ca-chinese">
<DateField label="Date" showFormatHelpText />
</Provider>
);

let segments = Array.from(getByRole('group').querySelectorAll('[data-testid]'));
let segmentTypes = segments.map(s => s.getAttribute('data-testid'));
expect(segmentTypes).toEqual(['year', 'month', 'day']);
});
});

describe('events', function () {
Expand Down
29 changes: 17 additions & 12 deletions packages/@react-stately/datepicker/src/useDateFieldState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,13 @@ const PAGE_STEP = {
second: 15
};

// Node seems to convert everything to lowercase...
const TYPE_MAPPING = {
dayperiod: 'dayPeriod'
// Node seems to convert everything to lowercase...
Copy link
Member

Choose a reason for hiding this comment

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

Are you trying on node 22? I'm not seeing the issue in my local node environment
Screenshot 2025-05-22 at 5 40 14 pm

Copy link
Member Author

Choose a reason for hiding this comment

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

this was there before. probably from some older version

dayperiod: 'dayPeriod',
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts#named_years
relatedYear: 'year',
yearName: 'literal', // not editable
unknown: 'literal'
};

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

let [validSegments, setValidSegments] = useState<Partial<typeof EDITABLE_SEGMENTS>>(
Expand Down Expand Up @@ -413,18 +417,19 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
let segments = dateFormatter.formatToParts(dateValue);
let processedSegments: DateSegment[] = [];
for (let segment of segments) {
let isEditable = EDITABLE_SEGMENTS[segment.type];
if (segment.type === 'era' && calendar.getEras().length === 1) {
let type = TYPE_MAPPING[segment.type] || segment.type;
let isEditable = EDITABLE_SEGMENTS[type];
if (type === 'era' && calendar.getEras().length === 1) {
isEditable = false;
}

let isPlaceholder = EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type];
let placeholder = EDITABLE_SEGMENTS[segment.type] ? getPlaceholder(segment.type, segment.value, locale) : null;
let isPlaceholder = EDITABLE_SEGMENTS[type] && !validSegments[type];
let placeholder = EDITABLE_SEGMENTS[type] ? getPlaceholder(type, segment.value, locale) : null;

let dateSegment = {
type: TYPE_MAPPING[segment.type] || segment.type,
type,
text: isPlaceholder ? placeholder : segment.value,
...getSegmentLimits(displayValue, segment.type, resolvedOptions),
...getSegmentLimits(displayValue, type, resolvedOptions),
isPlaceholder,
placeholder,
isEditable
Expand All @@ -433,7 +438,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
// There is an issue in RTL languages where time fields render (minute:hour) instead of (hour:minute).
// 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.
// 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.
if (segment.type === 'hour') {
if (type === 'hour') {
// This marks the start of the embedded direction change.
processedSegments.push({
type: 'literal',
Expand All @@ -445,7 +450,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
});
processedSegments.push(dateSegment);
// This marks the end of the embedded direction change in the case that the granularity it set to "hour".
if (segment.type === granularity) {
if (type === granularity) {
processedSegments.push({
type: 'literal',
text: '\u2069',
Expand All @@ -455,7 +460,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
isEditable: false
});
}
} else if (timeValue.includes(segment.type) && segment.type === granularity) {
} else if (timeValue.includes(type) && type === granularity) {
processedSegments.push(dateSegment);
// This marks the end of the embedded direction change.
processedSegments.push({
Expand Down
19 changes: 18 additions & 1 deletion packages/react-aria-components/test/DateField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

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

Expand Down Expand Up @@ -338,4 +338,21 @@ describe('DateField', () => {
await user.keyboard('{backspace}');
expect(document.activeElement).toBe(segments[0]);
});

it('does not crash on unknown segment types', async () => {
let {getByRole} = render(
<I18nProvider locale="zh-CN-u-ca-chinese">
<DateField defaultValue={new CalendarDate(2024, 12, 31)}>
<Label>Birth date</Label>
<DateInput>
{segment => <DateSegment segment={segment} />}
</DateInput>
</DateField>
</I18nProvider>
);

let segments = Array.from(getByRole('group').querySelectorAll('.react-aria-DateSegment'));
let segmentTypes = segments.map(s => s.getAttribute('data-type'));
expect(segmentTypes).toEqual(['year', 'literal', 'month', 'day']);
});
});