From 3aff4cc18b4b6f487e0edba7f67dda40a7a2bdc9 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 21 May 2025 19:28:41 -0700 Subject: [PATCH] fix: Don't crash on unknown segment types in DateField --- .../@react-spectrum/datepicker/src/utils.tsx | 5 ++-- .../datepicker/test/DateField.test.js | 12 ++++++++ .../datepicker/src/useDateFieldState.ts | 29 +++++++++++-------- .../test/DateField.test.js | 19 +++++++++++- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/@react-spectrum/datepicker/src/utils.tsx b/packages/@react-spectrum/datepicker/src/utils.tsx index e8522aa6aef..e64da65dc39 100644 --- a/packages/@react-spectrum/datepicker/src/utils.tsx +++ b/packages/@react-spectrum/datepicker/src/utils.tsx @@ -31,11 +31,12 @@ export function useFormatHelpText(props: Pick, '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 {` ${s.value} `}; } - return {displayNames.of(s.type)}; + let type = s.type as string === 'relatedYear' ? 'year' : s.type; + return {displayNames.of(type)}; }) ); } diff --git a/packages/@react-spectrum/datepicker/test/DateField.test.js b/packages/@react-spectrum/datepicker/test/DateField.test.js index 41c58a5f9d7..829607e366b 100644 --- a/packages/@react-spectrum/datepicker/test/DateField.test.js +++ b/packages/@react-spectrum/datepicker/test/DateField.test.js @@ -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( + + + + ); + + 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 () { diff --git a/packages/@react-stately/datepicker/src/useDateFieldState.ts b/packages/@react-stately/datepicker/src/useDateFieldState.ts index 682dabe5018..d0a8a952f63 100644 --- a/packages/@react-stately/datepicker/src/useDateFieldState.ts +++ b/packages/@react-stately/datepicker/src/useDateFieldState.ts @@ -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... + 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 extends DatePickerProps { @@ -207,7 +211,7 @@ export function useDateFieldState(props: DateFi let allSegments: Partial = 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>( @@ -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 @@ -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', @@ -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', @@ -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({ diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 5bb55d52e93..c9a90251ca8 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -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'; @@ -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( + + + + + {segment => } + + + + ); + + 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']); + }); });