From cc5ac1e35ff61b560882d61c0d8b608e2f49320b Mon Sep 17 00:00:00 2001 From: Dmytro Klymenko Date: Sat, 6 Sep 2025 17:11:20 -0400 Subject: [PATCH] fix: Normalize literal before dayPeriod --- .../date/src/DateFormatter.ts | 13 +++++++++++- .../date/tests/DateFormatter.test.js | 11 ++++++++++ .../datepicker/test/DatePicker.test.js | 16 +++++++-------- .../datepicker/test/DateRangePicker.test.js | 20 +++++++++---------- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/packages/@internationalized/date/src/DateFormatter.ts b/packages/@internationalized/date/src/DateFormatter.ts index 21ab69f310e..26564aa6c38 100644 --- a/packages/@internationalized/date/src/DateFormatter.ts +++ b/packages/@internationalized/date/src/DateFormatter.ts @@ -34,7 +34,18 @@ export class DateFormatter implements Intl.DateTimeFormat { /** Formats a date to an array of parts such as separators, numbers, punctuation, and more. */ formatToParts(value: Date): Intl.DateTimeFormatPart[] { - return this.formatter.formatToParts(value); + const parts = this.formatter.formatToParts(value); + + const literalBeforeDayPeriodIndex = + parts.findIndex((p) => p.type === 'dayPeriod') - 1; + if (literalBeforeDayPeriodIndex >= 0) { + const normalizedParts = parts.map((p, i) => + i === literalBeforeDayPeriodIndex ? {...p, value: ' '} : p + ); + return normalizedParts; + } + + return parts; } /** Formats a date range as a string. */ diff --git a/packages/@internationalized/date/tests/DateFormatter.test.js b/packages/@internationalized/date/tests/DateFormatter.test.js index c70452ceadc..e0bc0c74fe1 100644 --- a/packages/@internationalized/date/tests/DateFormatter.test.js +++ b/packages/@internationalized/date/tests/DateFormatter.test.js @@ -42,6 +42,17 @@ describe('DateFormatter', function () { ]); }); + it('should format to parts with space as literal before am/pm', function () { + let formatter = new DateFormatter('en-US', {timeStyle: 'short'}); + expect(formatter.formatToParts(new Date(2020, 1, 3, 13, 0))).toEqual([ + {type: 'hour', value: '1'}, + {type: 'literal', value: ':'}, + {type: 'minute', value: '00'}, + {type: 'literal', value: ' '}, + {type: 'dayPeriod', value: 'PM'} + ]); + }); + it('should format a range', function () { let formatter = new DateFormatter('en-US'); // Test fallback diff --git a/packages/@react-spectrum/datepicker/test/DatePicker.test.js b/packages/@react-spectrum/datepicker/test/DatePicker.test.js index 3b931ba8152..774f385d5c2 100644 --- a/packages/@react-spectrum/datepicker/test/DatePicker.test.js +++ b/packages/@react-spectrum/datepicker/test/DatePicker.test.js @@ -437,7 +437,7 @@ describe('DatePicker', function () { ); let combobox = getAllByRole('group')[0]; - expect(getTextValue(combobox)).toBe('2/3/2019, 8:45 AM'); + expect(getTextValue(combobox)).toBe('2/3/2019, 8:45 AM'); let button = getByRole('button'); await user.click(button); @@ -450,7 +450,7 @@ describe('DatePicker', function () { expect(selected.children[0]).toHaveAttribute('aria-label', 'Sunday, February 3, 2019 selected'); let timeField = getAllByLabelText('Time')[0]; - expect(getTextValue(timeField)).toBe('8:45 AM'); + expect(getTextValue(timeField)).toBe('8:45 AM'); // selecting a date should not close the popover await user.click(selected.nextSibling.children[0]); @@ -458,7 +458,7 @@ describe('DatePicker', function () { expect(dialog).toBeVisible(); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(new CalendarDateTime(2019, 2, 4, 8, 45)); - expect(getTextValue(combobox)).toBe('2/4/2019, 8:45 AM'); + expect(getTextValue(combobox)).toBe('2/4/2019, 8:45 AM'); let hour = within(timeField).getByLabelText('hour,'); expect(hour).toHaveAttribute('role', 'spinbutton'); @@ -472,7 +472,7 @@ describe('DatePicker', function () { expect(dialog).toBeVisible(); expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenCalledWith(new CalendarDateTime(2019, 2, 4, 9, 45)); - expect(getTextValue(combobox)).toBe('2/4/2019, 9:45 AM'); + expect(getTextValue(combobox)).toBe('2/4/2019, 9:45 AM'); }); it('should not throw error when deleting values from time field when CalendarDateTime value is used', async function () { @@ -484,7 +484,7 @@ describe('DatePicker', function () { ); let combobox = getAllByRole('group')[0]; - expect(getTextValue(combobox)).toBe('2/3/2019, 10:45 AM'); + expect(getTextValue(combobox)).toBe('2/3/2019, 10:45 AM'); let button = getByRole('button'); await user.click(button); @@ -497,7 +497,7 @@ describe('DatePicker', function () { expect(selected.children[0]).toHaveAttribute('aria-label', 'Sunday, February 3, 2019 selected'); let timeField = getAllByLabelText('Time')[0]; - expect(getTextValue(timeField)).toBe('10:45 AM'); + expect(getTextValue(timeField)).toBe('10:45 AM'); // selecting a date should not close the popover await user.click(selected.nextSibling.children[0]); @@ -505,7 +505,7 @@ describe('DatePicker', function () { expect(dialog).toBeVisible(); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(new CalendarDateTime(2019, 2, 4, 10, 45)); - expect(getTextValue(combobox)).toBe('2/4/2019, 10:45 AM'); + expect(getTextValue(combobox)).toBe('2/4/2019, 10:45 AM'); let hour = within(timeField).getByLabelText('hour,'); expect(hour).toHaveAttribute('role', 'spinbutton'); @@ -521,7 +521,7 @@ describe('DatePicker', function () { expect(dialog).toBeVisible(); expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenCalledWith(new CalendarDateTime(2019, 2, 4, 1, 45)); - expect(getTextValue(combobox)).toBe('2/4/2019, 1:45 AM'); + expect(getTextValue(combobox)).toBe('2/4/2019, 1:45 AM'); }); it('should fire onChange until both date and time are selected', async function () { diff --git a/packages/@react-spectrum/datepicker/test/DateRangePicker.test.js b/packages/@react-spectrum/datepicker/test/DateRangePicker.test.js index cf027ab73ad..113950290ab 100644 --- a/packages/@react-spectrum/datepicker/test/DateRangePicker.test.js +++ b/packages/@react-spectrum/datepicker/test/DateRangePicker.test.js @@ -492,8 +492,8 @@ describe('DateRangePicker', function () { let startDate = getByTestId('start-date'); let endDate = getByTestId('end-date'); - expect(getTextValue(startDate)).toBe('2/3/2019, 8:45 AM'); - expect(getTextValue(endDate)).toBe('5/6/2019, 10:45 AM'); + expect(getTextValue(startDate)).toBe('2/3/2019, 8:45 AM'); + expect(getTextValue(endDate)).toBe('5/6/2019, 10:45 AM'); let button = getByRole('button'); await user.click(button); @@ -506,10 +506,10 @@ describe('DateRangePicker', function () { expect(selected.children[0]).toHaveAttribute('aria-label', 'Selected Range: Sunday, February 3 to Monday, May 6, 2019, Sunday, February 3, 2019 selected'); let startTimeField = getAllByLabelText('Start time')[0]; - expect(getTextValue(startTimeField)).toBe('8:45 AM'); + expect(getTextValue(startTimeField)).toBe('8:45 AM'); let endTimeField = getAllByLabelText('End time')[0]; - expect(getTextValue(endTimeField)).toBe('10:45 AM'); + expect(getTextValue(endTimeField)).toBe('10:45 AM'); // selecting a date should not close the popover await user.click(getByLabelText('Sunday, February 10, 2019 selected')); @@ -518,8 +518,8 @@ describe('DateRangePicker', function () { expect(dialog).toBeVisible(); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith({start: new CalendarDateTime(2019, 2, 10, 8, 45), end: new CalendarDateTime(2019, 2, 17, 10, 45)}); - expect(getTextValue(startDate)).toBe('2/10/2019, 8:45 AM'); - expect(getTextValue(endDate)).toBe('2/17/2019, 10:45 AM'); + expect(getTextValue(startDate)).toBe('2/10/2019, 8:45 AM'); + expect(getTextValue(endDate)).toBe('2/17/2019, 10:45 AM'); let hour = within(startTimeField).getByLabelText('hour,'); expect(hour).toHaveAttribute('role', 'spinbutton'); @@ -534,8 +534,8 @@ describe('DateRangePicker', function () { expect(dialog).toBeVisible(); expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenCalledWith({start: new CalendarDateTime(2019, 2, 10, 9, 45), end: new CalendarDateTime(2019, 2, 17, 10, 45)}); - expect(getTextValue(startDate)).toBe('2/10/2019, 9:45 AM'); - expect(getTextValue(endDate)).toBe('2/17/2019, 10:45 AM'); + expect(getTextValue(startDate)).toBe('2/10/2019, 9:45 AM'); + expect(getTextValue(endDate)).toBe('2/17/2019, 10:45 AM'); hour = within(endTimeField).getByLabelText('hour,'); expect(hour).toHaveAttribute('role', 'spinbutton'); @@ -550,8 +550,8 @@ describe('DateRangePicker', function () { expect(dialog).toBeVisible(); expect(onChange).toHaveBeenCalledTimes(3); expect(onChange).toHaveBeenCalledWith({start: new CalendarDateTime(2019, 2, 10, 9, 45), end: new CalendarDateTime(2019, 2, 17, 11, 45)}); - expect(getTextValue(startDate)).toBe('2/10/2019, 9:45 AM'); - expect(getTextValue(endDate)).toBe('2/17/2019, 11:45 AM'); + expect(getTextValue(startDate)).toBe('2/10/2019, 9:45 AM'); + expect(getTextValue(endDate)).toBe('2/17/2019, 11:45 AM'); }); it('should not fire onChange until both date range and time range are selected', async function () {