From f031e83061dabb1b5878c7566291f2f805f81333 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Thu, 4 Sep 2025 08:38:21 +0200 Subject: [PATCH 1/4] feat: Date formats for date range picker --- ...te-format-day-picker.permutations.page.tsx | 4 +- ...-format-month-picker.permutations.page.tsx | 4 +- pages/date-range-picker/common.tsx | 33 +- .../month-calendar-permutations.page.tsx | 26 +- .../date-range-picker/range-calendar.page.tsx | 9 +- pages/date-range-picker/with-value.page.tsx | 3 +- .../year-calendar-permutations.page.tsx | 15 + .../__snapshots__/documenter.test.ts.snap | 55 +++- ...nge-picker-absolute-input-formats.test.tsx | 286 ++++++++++++++++++ .../date-range-picker-absolute.test.tsx | 4 +- .../__tests__/i18n-strings.ts | 32 +- src/date-range-picker/calendar/index.tsx | 23 +- src/date-range-picker/calendar/interfaces.ts | 72 +++++ .../calendar/range-inputs.tsx | 62 +--- src/date-range-picker/calendar/utils.ts | 48 +++ src/date-range-picker/dropdown.tsx | 32 +- src/date-range-picker/index.tsx | 4 + src/date-range-picker/interfaces.ts | 92 ++++-- .../date-time/format-date-time-with-offset.ts | 4 + 19 files changed, 684 insertions(+), 124 deletions(-) create mode 100644 src/date-range-picker/__tests__/date-range-picker-absolute-input-formats.test.tsx create mode 100644 src/date-range-picker/calendar/interfaces.ts diff --git a/pages/date-range-picker/absolute-format-day-picker.permutations.page.tsx b/pages/date-range-picker/absolute-format-day-picker.permutations.page.tsx index 31c98a6380..b2fd31a964 100644 --- a/pages/date-range-picker/absolute-format-day-picker.permutations.page.tsx +++ b/pages/date-range-picker/absolute-format-day-picker.permutations.page.tsx @@ -12,14 +12,14 @@ import { isValid, placeholders } from './common'; const permutations = createPermutations([ { - absoluteFormat: ['iso', 'long-localized'], + absoluteFormat: ['iso', 'slashed', 'long-localized'], dateOnly: [true, false], value: [{ type: 'absolute', startDate: '2024-12-30', endDate: '2024-12-31' }], isValidRange: [() => ({ valid: true })], relativeOptions: [[]], }, { - absoluteFormat: ['iso', 'long-localized'], + absoluteFormat: ['iso', 'slashed', 'long-localized'], dateOnly: [true, false], hideTimeOffset: [true, false], value: [{ type: 'absolute', startDate: '2024-12-30T00:00:00+01:00', endDate: '2024-12-31T23:59:59+01:00' }], diff --git a/pages/date-range-picker/absolute-format-month-picker.permutations.page.tsx b/pages/date-range-picker/absolute-format-month-picker.permutations.page.tsx index 6ebec93473..b9e4e154f9 100644 --- a/pages/date-range-picker/absolute-format-month-picker.permutations.page.tsx +++ b/pages/date-range-picker/absolute-format-month-picker.permutations.page.tsx @@ -12,7 +12,7 @@ import { isValid, placeholders } from './common'; const permutations = createPermutations([ { - absoluteFormat: ['iso', 'long-localized'], + absoluteFormat: ['iso', 'slashed', 'long-localized'], value: [ { type: 'absolute', @@ -24,7 +24,7 @@ const permutations = createPermutations([ relativeOptions: [[]], }, { - absoluteFormat: ['iso', 'long-localized'], + absoluteFormat: ['iso', 'slashed', 'long-localized'], hideTimeOffset: [true, false], value: [ { diff --git a/pages/date-range-picker/common.tsx b/pages/date-range-picker/common.tsx index 7779aefa6f..7d628ab9e3 100644 --- a/pages/date-range-picker/common.tsx +++ b/pages/date-range-picker/common.tsx @@ -19,6 +19,7 @@ interface DateRangePickerPageSettings { warning?: boolean; rangeSelectorMode?: DateRangePickerProps.RangeSelectorMode; absoluteFormat?: DateRangePickerProps.AbsoluteFormat; + dateInputFormat?: DateRangePickerProps['dateInputFormat']; timeInputFormat?: DateRangePickerProps['timeInputFormat']; timeOffset?: number; hideTimeOffset?: boolean; @@ -36,6 +37,7 @@ const defaultSettings: Required = { warning: false, rangeSelectorMode: 'default', absoluteFormat: 'iso', + dateInputFormat: 'iso', timeInputFormat: 'hh:mm:ss', timeOffset: 0, hideTimeOffset: false, @@ -80,6 +82,7 @@ export function useDateRangePickerSettings( const rangeSelectorMode = urlParams.rangeSelectorMode ?? def('rangeSelectorMode'); const absoluteFormat = urlParams.absoluteFormat ?? def('absoluteFormat'); const timeInputFormat = urlParams.timeInputFormat ?? def('timeInputFormat'); + const dateInputFormat = urlParams.dateInputFormat ?? def('dateInputFormat'); const timeOffset = parseNumber(def('timeOffset'), urlParams.timeOffset); const hideTimeOffset = parseBoolean(def('hideTimeOffset'), urlParams.hideTimeOffset); const expandToViewport = parseBoolean(def('expandToViewport'), urlParams.expandToViewport); @@ -94,6 +97,7 @@ export function useDateRangePickerSettings( warning, rangeSelectorMode, absoluteFormat, + dateInputFormat, timeInputFormat, timeOffset, hideTimeOffset, @@ -242,6 +246,8 @@ export function Settings({ warning, rangeSelectorMode, absoluteFormat, + dateInputFormat, + timeInputFormat, timeOffset, hideTimeOffset, expandToViewport, @@ -263,7 +269,8 @@ export function Settings({ { value: 'end-of-page' }, { value: 'overlapping-pages' }, ]; - const absoluteFormatOptions = [{ value: 'iso' }, { value: 'long-localized' }]; + const dateFormatOptions = [{ value: 'iso' }, { value: 'slashed' }, { value: 'long-localized' }]; + const timeFormatOptions = [{ value: 'hh:mm:ss' }, { value: 'hh:mm' }, { value: 'hh' }]; return ( @@ -286,14 +293,34 @@ export function Settings({ o.value === dateInputFormat) ?? null} + onChange={({ detail }) => + setSettings({ dateInputFormat: detail.selectedOption.value as DateRangePickerProps.DateInputFormat }) + } + /> + + + + ([ locale: ['en-GB'], startOfWeek: [1], onChange: [() => {}], - timeInputFormat: ['hh:mm:ss'] as const, customAbsoluteRangeControl: [undefined], + timeInputFormat: ['hh:mm:ss'] as const, + absoluteFormat: ['long-localized'] as const, })), // Disabled dates { @@ -42,6 +43,8 @@ const permutations = createPermutations([ setValue: [() => {}], isDateEnabled: [() => false, (date: Date) => date.getDate() % 2 !== 0], customAbsoluteRangeControl: [undefined], + timeInputFormat: ['hh:mm:ss'] as const, + absoluteFormat: ['long-localized'] as const, }, // Date-only { @@ -49,12 +52,33 @@ const permutations = createPermutations([ setValue: [() => {}], dateOnly: [true], customAbsoluteRangeControl: [undefined], + timeInputFormat: ['hh:mm:ss'] as const, + absoluteFormat: ['long-localized'] as const, }, // Custom control { value: [{ start: { date: '', time: '' }, end: { date: '', time: '' } }], setValue: [() => {}], customAbsoluteRangeControl: [() => 'Custom control'], + timeInputFormat: ['hh:mm:ss'] as const, + absoluteFormat: ['long-localized'] as const, + }, + // Date input formats + { + value: [{ start: { date: '', time: '' }, end: { date: '', time: '' } }], + setValue: [() => {}], + customAbsoluteRangeControl: [undefined], + timeInputFormat: ['hh:mm:ss'] as const, + dateInputFormat: ['iso', 'slashed'] as const, + absoluteFormat: ['long-localized'] as const, + }, + // Time input formats + { + value: [{ start: { date: '', time: '' }, end: { date: '', time: '' } }], + setValue: [() => {}], + customAbsoluteRangeControl: [undefined], + timeInputFormat: ['hh:mm', 'hh'] as const, + absoluteFormat: ['long-localized'] as const, }, ]); diff --git a/pages/date-range-picker/range-calendar.page.tsx b/pages/date-range-picker/range-calendar.page.tsx index 25cc88be96..f4b93f1241 100644 --- a/pages/date-range-picker/range-calendar.page.tsx +++ b/pages/date-range-picker/range-calendar.page.tsx @@ -23,7 +23,14 @@ export default function RangeCalendarScenario() { Focusable element before - + Focusable element after diff --git a/pages/date-range-picker/with-value.page.tsx b/pages/date-range-picker/with-value.page.tsx index 6127a0cd8b..2a64da8cb0 100644 --- a/pages/date-range-picker/with-value.page.tsx +++ b/pages/date-range-picker/with-value.page.tsx @@ -33,7 +33,8 @@ export default function DatePickerScenario() {

-
+ {/* We give more space at the bottom so that the dropdown opens down and stays within the screenshot area. */} +
Raw value: {JSON.stringify(props.value, undefined, 2)} diff --git a/pages/date-range-picker/year-calendar-permutations.page.tsx b/pages/date-range-picker/year-calendar-permutations.page.tsx index 93c16354bf..5568dcf765 100644 --- a/pages/date-range-picker/year-calendar-permutations.page.tsx +++ b/pages/date-range-picker/year-calendar-permutations.page.tsx @@ -31,6 +31,8 @@ const permutations = createPermutations([ locale: ['en-GB'], onChange: [() => {}], customAbsoluteRangeControl: [undefined], + timeInputFormat: ['hh:mm:ss'] as const, + absoluteFormat: ['long-localized'] as const, })), // Disabled dates { @@ -38,12 +40,25 @@ const permutations = createPermutations([ setValue: [() => {}], isDateEnabled: [() => false], customAbsoluteRangeControl: [undefined], + timeInputFormat: ['hh:mm:ss'] as const, + absoluteFormat: ['long-localized'] as const, }, // Custom control { value: [{ start: { date: '', time: '' }, end: { date: '', time: '' } }], setValue: [() => {}], customAbsoluteRangeControl: [() => 'Custom control'], + timeInputFormat: ['hh:mm:ss'] as const, + absoluteFormat: ['long-localized'] as const, + }, + // Input date formats + { + value: [{ start: { date: '', time: '' }, end: { date: '', time: '' } }], + setValue: [() => {}], + customAbsoluteRangeControl: [undefined], + timeInputFormat: ['hh:mm:ss'] as const, + dateInputFormat: ['iso', 'slashed'] as const, + absoluteFormat: ['long-localized'] as const, }, ]); diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index c8ad729ae9..bf0ee37ff1 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -10180,13 +10180,13 @@ The event \`detail\` contains the current value of the field.", It can take the following values: * \`iso\`: ISO 8601 format, e.g.: 2024-01-30T13:32:32+01:00 (or 2024-01-30 when \`dateOnly\` is true) * \`long-localized\`: a more human-readable, localized format, e.g.: January 30, 2024, 13:32:32 (UTC+1) (or January 30, 2024 when \`dateOnly\` is true) - -Defaults to \`iso\`.", +* \`slashed\`: similar to ISO 8601 but with '/' in place of '-'. e.g.: 2024/01/30 (or 2024/01)", "inlineType": { - "name": "DateRangePickerProps.AbsoluteFormat", + "name": "DateFormat", "type": "union", "values": [ "iso", + "slashed", "long-localized", ], }, @@ -10281,6 +10281,25 @@ If provided, the date becomes focusable.", "optional": true, "type": "DateRangePickerProps.DateDisabledReasonFunction", }, + { + "defaultValue": "'slashed'", + "description": "Specifies the date format to use on the date inputs in the absolute dropdown. + +The format of the input as it is being interacted with. It can take the following values: +* \`iso\`: ISO 8601 format without time, e.g.: 2024-01-30 (or 2024-01) +* \`slashed\`: similar to ISO 8601 but with '/' in place of '-'. e.g.: 2024/01/30 (or 2024/01)", + "inlineType": { + "name": "EditableDateFormat", + "type": "union", + "values": [ + "iso", + "slashed", + ], + }, + "name": "dateInputFormat", + "optional": true, + "type": "string", + }, { "defaultValue": "false", "description": "Hides time inputs and changes the input format to date-only, e.g. 2021-04-06. @@ -10503,6 +10522,21 @@ Defaults to \`false\`.", "optional": true, "type": "((unit: DateRangePickerProps.TimeUnit, value: number) => string)", }, + { + "name": "isoDateConstraintText", + "optional": true, + "type": "string", + }, + { + "name": "isoDateTimeConstraintText", + "optional": true, + "type": "string", + }, + { + "name": "isoMonthConstraintText", + "optional": true, + "type": "string", + }, { "name": "modeSelectionLabel", "optional": true, @@ -10568,6 +10602,21 @@ Defaults to \`false\`.", "optional": true, "type": "((startDate: string, endDate: string) => string)", }, + { + "name": "slashedDateConstraintText", + "optional": true, + "type": "string", + }, + { + "name": "slashedDateTimeConstraintText", + "optional": true, + "type": "string", + }, + { + "name": "slashedMonthConstraintText", + "optional": true, + "type": "string", + }, { "name": "startDateLabel", "optional": true, diff --git a/src/date-range-picker/__tests__/date-range-picker-absolute-input-formats.test.tsx b/src/date-range-picker/__tests__/date-range-picker-absolute-input-formats.test.tsx new file mode 100644 index 0000000000..04e1fa3d76 --- /dev/null +++ b/src/date-range-picker/__tests__/date-range-picker-absolute-input-formats.test.tsx @@ -0,0 +1,286 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from 'react'; +import { render as rtlRender } from '@testing-library/react'; + +import '../../__a11y__/to-validate-a11y'; +import DateRangePicker, { DateRangePickerProps } from '../../../lib/components/date-range-picker'; +import TestI18nProvider from '../../../lib/components/i18n/testing'; +import createWrapper from '../../../lib/components/test-utils/dom'; +import { + i18nMessages as i18nMessagesFallback, + i18nMessagesWithExtraFormatConstraints as i18nMessages, + i18nStrings as i18nStringsFallback, + i18nStringsWithExtraFormatConstraints as i18nStrings, +} from './i18n-strings'; + +const defaultProps: DateRangePickerProps = { + locale: 'en-US', + value: { type: 'absolute', startDate: '2020-01-01T12:13:14', endDate: '2020-01-01T12:13:14' }, + onChange: () => {}, + relativeOptions: [], + isValidRange: () => ({ valid: true }), +}; + +function randomItem(array: readonly T[]): T { + return array[Math.floor(Math.random() * array.length)]; +} + +type RenderProps = Partial & { i18nMessages?: Record> }; + +function renderDateRangePicker(props: RenderProps) { + function Component(props: RenderProps) { + return ( + + + + ); + } + const { container, rerender } = rtlRender(); + const wrapper = createWrapper(container).findDateRangePicker()!; + const rerenderWrapper = (props: RenderProps) => rerender(); + + wrapper.openDropdown(); + + const dropdown = wrapper.findDropdown()!; + const startDateInput = dropdown!.findStartDateInput()!; + const endDateInput = dropdown!.findEndDateInput()!; + const randomDateInput = randomItem([startDateInput, endDateInput]); + const startTimeInput = dropdown!.findStartTimeInput()!; + const endTimeInput = dropdown!.findEndTimeInput()!; + const randomTimeInput = randomItem([startTimeInput, endTimeInput]); + + return { dropdown, randomDateInput, randomTimeInput, rerender: rerenderWrapper }; +} + +function randomAbsoluteFormat() { + return randomItem([undefined, 'iso', 'slashed', 'long-localized'] as const); +} + +const isoDateProps: { + absoluteFormat: undefined | DateRangePickerProps.AbsoluteFormat; + dateInputFormat: undefined | DateRangePickerProps.DateInputFormat; +}[] = [{ absoluteFormat: randomAbsoluteFormat(), dateInputFormat: 'iso' }]; + +const slashedDateProps: { + absoluteFormat: undefined | DateRangePickerProps.AbsoluteFormat; + dateInputFormat: undefined | DateRangePickerProps.DateInputFormat; +}[] = [ + { absoluteFormat: randomAbsoluteFormat(), dateInputFormat: undefined }, + { absoluteFormat: randomAbsoluteFormat(), dateInputFormat: 'slashed' }, +]; + +describe('Date range picker: absolute mode input formats', () => { + describe('date input formats', () => { + test.each(isoDateProps)('accepts iso date input with props: %s', props => { + const { randomDateInput } = renderDateRangePicker({ ...props }); + expect(randomDateInput.getInputValue()).toBe('2020-01-01'); + + randomDateInput.setInputValue(''); + randomDateInput.setInputValue('2021/02/03'); + expect(randomDateInput.getInputValue()).toBe(''); + + randomDateInput.setInputValue('2021-02-03'); + expect(randomDateInput.getInputValue()).toBe('2021-02-03'); + }); + + test.each(slashedDateProps)('accepts slashed date input with props: %s', props => { + const { randomDateInput } = renderDateRangePicker({ ...props }); + expect(randomDateInput.getInputValue()).toBe('2020/01/01'); + + randomDateInput.setInputValue(''); + randomDateInput.setInputValue('2021-02-03'); + expect(randomDateInput.getInputValue()).toBe(''); + + randomDateInput.setInputValue('2021/02/03'); + expect(randomDateInput.getInputValue()).toBe('2021/02/03'); + }); + + test('accepts iso input with granularity "month"', () => { + const { randomDateInput } = renderDateRangePicker({ ...randomItem(isoDateProps), granularity: 'month' }); + expect(randomDateInput.getInputValue()).toBe('2020-01'); + + randomDateInput.setInputValue('2021-02-03'); + expect(randomDateInput.getInputValue()).toBe('2021-02'); + }); + + test('accepts slashed input with granularity "month"', () => { + const { randomDateInput } = renderDateRangePicker({ ...randomItem(slashedDateProps), granularity: 'month' }); + expect(randomDateInput.getInputValue()).toBe('2020/01'); + + randomDateInput.setInputValue('2021/02/03'); + expect(randomDateInput.getInputValue()).toBe('2021/02'); + }); + }); + + describe('time input formats', () => { + test.each([undefined, 'hh:mm:ss'] as const)('accepts hh:mm:ss input with format %s', timeInputFormat => { + const { randomTimeInput } = renderDateRangePicker({ timeInputFormat }); + expect(randomTimeInput.getInputValue()).toBe('12:13:14'); + + randomTimeInput.setInputValue(''); + randomTimeInput.setInputValue('13:14:15'); + expect(randomTimeInput.getInputValue()).toBe('13:14:15'); + }); + + test('accepts hh:mm input with format hh:mm', () => { + const { randomTimeInput } = renderDateRangePicker({ timeInputFormat: 'hh:mm' }); + expect(randomTimeInput.getInputValue()).toBe('12:13'); + + randomTimeInput.setInputValue(''); + randomTimeInput.setInputValue('13:14:15'); + expect(randomTimeInput.getInputValue()).toBe(''); + + randomTimeInput.setInputValue('13:14'); + expect(randomTimeInput.getInputValue()).toBe('13:14'); + }); + + test('accepts hh input with format hh', () => { + const { randomTimeInput } = renderDateRangePicker({ timeInputFormat: 'hh' }); + expect(randomTimeInput.getInputValue()).toBe('12'); + + randomTimeInput.setInputValue(''); + randomTimeInput.setInputValue('13:14:15'); + expect(randomTimeInput.getInputValue()).toBe(''); + + randomTimeInput.setInputValue('13:14'); + expect(randomTimeInput.getInputValue()).toBe(''); + + randomTimeInput.setInputValue('13'); + expect(randomTimeInput.getInputValue()).toBe('13'); + }); + }); + + describe('inputs i18n', () => { + function assertInput(input: HTMLInputElement, placeholder: string, constraint: string) { + expect(input.placeholder).toBe(placeholder); + expect(input).toHaveAccessibleDescription(constraint); + } + function assertDateInput(placeholder: string, constraint: string) { + const dropdown = createWrapper().findDateRangePicker()!.findDropdown()!; + const input = Math.random() < 0.5 ? dropdown.findStartDateInput()! : dropdown.findEndDateInput()!; + assertInput(input.findNativeInput().getElement(), placeholder, constraint); + } + function assertTimeInput(placeholder: string, constraint: string) { + const dropdown = createWrapper().findDateRangePicker()!.findDropdown()!; + const input = Math.random() < 0.5 ? dropdown.findStartTimeInput()! : dropdown.findEndTimeInput()!; + assertInput(input.findNativeInput().getElement(), placeholder, constraint); + } + + const messages = (caseOptions: { i18n: boolean; fallback: boolean }) => { + switch (`${caseOptions.i18n}-${caseOptions.fallback}`) { + case 'true-false': + return { i18nMessages, i18nStrings: {} }; + case 'true-true': + return { i18nMessages: i18nMessagesFallback, i18nStrings: {} }; + case 'false-false': + return { i18nMessages, i18nStrings }; + case 'false-true': + default: + return { i18nMessages, i18nStrings: i18nStringsFallback }; + } + }; + + test('date only iso inputs use correct i18n strings', () => { + const dateOnlyIsoProps = { ...randomItem(isoDateProps), dateOnly: true, value: null }; + const result = renderDateRangePicker({ ...dateOnlyIsoProps, ...messages({ i18n: true, fallback: false }) }); + assertDateInput('YYYY-MM-DD', '(i18n) For date, use YYYY-MM-DD.'); + + result.rerender({ ...dateOnlyIsoProps, ...messages({ i18n: true, fallback: true }) }); + assertDateInput('YYYY-MM-DD', '(i18n) (fallback) For date, use YYYY-MM-DD.'); + + result.rerender({ ...dateOnlyIsoProps, ...messages({ i18n: false, fallback: false }) }); + assertDateInput('YYYY-MM-DD', 'For date, use YYYY-MM-DD.'); + + result.rerender({ ...dateOnlyIsoProps, ...messages({ i18n: false, fallback: true }) }); + assertDateInput('YYYY-MM-DD', '(fallback) For date, use YYYY-MM-DD.'); + }); + + test.each(['hh:mm:ss', 'hh:mm', 'hh'] as const)( + 'date/time iso inputs use correct i18n strings, timeInputFormat=%s', + timeInputFormat => { + const dateTimeIsoProps = { ...randomItem(isoDateProps), dateOnly: false, value: null, timeInputFormat }; + const result = renderDateRangePicker({ ...dateTimeIsoProps, ...messages({ i18n: true, fallback: false }) }); + assertDateInput('YYYY-MM-DD', '(i18n) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + assertTimeInput(timeInputFormat, '(i18n) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + + result.rerender({ ...dateTimeIsoProps, ...messages({ i18n: true, fallback: true }) }); + assertDateInput('YYYY-MM-DD', '(i18n) (fallback) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + assertTimeInput(timeInputFormat, '(i18n) (fallback) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + + result.rerender({ ...dateTimeIsoProps, ...messages({ i18n: false, fallback: false }) }); + assertDateInput('YYYY-MM-DD', 'For date, use YYYY-MM-DD. For time, use 24 hour format.'); + assertTimeInput(timeInputFormat, 'For date, use YYYY-MM-DD. For time, use 24 hour format.'); + + result.rerender({ ...dateTimeIsoProps, ...messages({ i18n: false, fallback: true }) }); + assertDateInput('YYYY-MM-DD', '(fallback) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + assertTimeInput(timeInputFormat, '(fallback) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + } + ); + + test('date only slashed inputs use correct i18n strings', () => { + const dateOnlySlashedProps = { ...randomItem(slashedDateProps), dateOnly: true, value: null }; + const result = renderDateRangePicker({ ...dateOnlySlashedProps, ...messages({ i18n: true, fallback: false }) }); + assertDateInput('YYYY/MM/DD', '(i18n) For date, use YYYY/MM/DD.'); + + result.rerender({ ...dateOnlySlashedProps, ...messages({ i18n: true, fallback: true }) }); + assertDateInput('YYYY/MM/DD', '(i18n) (fallback) For date, use YYYY-MM-DD.'); + + result.rerender({ ...dateOnlySlashedProps, ...messages({ i18n: false, fallback: false }) }); + assertDateInput('YYYY/MM/DD', 'For date, use YYYY/MM/DD.'); + + result.rerender({ ...dateOnlySlashedProps, ...messages({ i18n: false, fallback: true }) }); + assertDateInput('YYYY/MM/DD', '(fallback) For date, use YYYY-MM-DD.'); + }); + + test('date/time slashed inputs use correct i18n strings', () => { + const dateTimeSlashedProps = { ...randomItem(slashedDateProps), dateOnly: false, value: null }; + const result = renderDateRangePicker({ ...dateTimeSlashedProps, ...messages({ i18n: true, fallback: false }) }); + assertDateInput('YYYY/MM/DD', '(i18n) For date, use YYYY/MM/DD. For time, use 24 hour format.'); + assertTimeInput('hh:mm:ss', '(i18n) For date, use YYYY/MM/DD. For time, use 24 hour format.'); + + result.rerender({ ...dateTimeSlashedProps, ...messages({ i18n: true, fallback: true }) }); + assertDateInput('YYYY/MM/DD', '(i18n) (fallback) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + assertTimeInput('hh:mm:ss', '(i18n) (fallback) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + + result.rerender({ ...dateTimeSlashedProps, ...messages({ i18n: false, fallback: false }) }); + assertDateInput('YYYY/MM/DD', 'For date, use YYYY/MM/DD. For time, use 24 hour format.'); + assertTimeInput('hh:mm:ss', 'For date, use YYYY/MM/DD. For time, use 24 hour format.'); + + result.rerender({ ...dateTimeSlashedProps, ...messages({ i18n: false, fallback: true }) }); + assertDateInput('YYYY/MM/DD', '(fallback) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + assertTimeInput('hh:mm:ss', '(fallback) For date, use YYYY-MM-DD. For time, use 24 hour format.'); + }); + + test('month iso inputs use correct i18n strings', () => { + const dateOnlyIsoProps = { ...randomItem(isoDateProps), granularity: 'month' as const, value: null }; + const result = renderDateRangePicker({ ...dateOnlyIsoProps, ...messages({ i18n: true, fallback: false }) }); + assertDateInput('YYYY-MM', '(i18n) For month, use YYYY-MM.'); + + result.rerender({ ...dateOnlyIsoProps, ...messages({ i18n: true, fallback: true }) }); + assertDateInput('YYYY-MM', '(i18n) (fallback) For month, use YYYY-MM.'); + + result.rerender({ ...dateOnlyIsoProps, ...messages({ i18n: false, fallback: false }) }); + assertDateInput('YYYY-MM', 'For month, use YYYY-MM.'); + + result.rerender({ ...dateOnlyIsoProps, ...messages({ i18n: false, fallback: true }) }); + assertDateInput('YYYY-MM', '(fallback) For month, use YYYY-MM.'); + }); + + test('month slashed inputs use correct i18n strings', () => { + const dateOnlyIsoProps = { ...randomItem(slashedDateProps), granularity: 'month' as const, value: null }; + const result = renderDateRangePicker({ ...dateOnlyIsoProps, ...messages({ i18n: true, fallback: false }) }); + assertDateInput('YYYY/MM', '(i18n) For month, use YYYY/MM.'); + + result.rerender({ ...dateOnlyIsoProps, ...messages({ i18n: true, fallback: true }) }); + assertDateInput('YYYY/MM', '(i18n) (fallback) For month, use YYYY-MM.'); + + result.rerender({ ...dateOnlyIsoProps, ...messages({ i18n: false, fallback: false }) }); + assertDateInput('YYYY/MM', 'For month, use YYYY/MM.'); + + result.rerender({ ...dateOnlyIsoProps, ...messages({ i18n: false, fallback: true }) }); + assertDateInput('YYYY/MM', '(fallback) For month, use YYYY-MM.'); + }); + }); +}); diff --git a/src/date-range-picker/__tests__/date-range-picker-absolute.test.tsx b/src/date-range-picker/__tests__/date-range-picker-absolute.test.tsx index f5273a27a8..852e0f53b2 100644 --- a/src/date-range-picker/__tests__/date-range-picker-absolute.test.tsx +++ b/src/date-range-picker/__tests__/date-range-picker-absolute.test.tsx @@ -107,7 +107,7 @@ describe('Date range picker', () => { }); }); - describe('data formats', () => { + describe('date formats', () => { test(`granularity of month overrides dateOnly and parses out milliseconds`, () => { const { wrapper } = renderDateRangePicker({ ...defaultProps, @@ -516,7 +516,7 @@ describe('Date range picker', () => { ); }); - testIf(granularity === 'day')('hh:mm:ss gets hidded when dateOnly', () => { + testIf(granularity === 'day')('hh:mm:ss gets hidden when dateOnly', () => { const onChangeSpy = jest.fn(); const { wrapper } = renderDateRangePicker({ ...defaultProps, diff --git a/src/date-range-picker/__tests__/i18n-strings.ts b/src/date-range-picker/__tests__/i18n-strings.ts index fa257c60a0..a6a2ec7c31 100644 --- a/src/date-range-picker/__tests__/i18n-strings.ts +++ b/src/date-range-picker/__tests__/i18n-strings.ts @@ -17,9 +17,9 @@ export const i18nStrings: DateRangePickerProps.I18nStrings = { customRelativeRangeUnitLabel: 'Unit of time', formatRelativeRange: range => `${range.unit}${range.amount}`, formatUnit: (unit, value) => (value === 1 ? unit : `${unit}s`), - dateConstraintText: 'Range must be between 6 and 30 days.', - dateTimeConstraintText: 'Range must be between 6 and 30 days. Use 24 hour format.', - monthConstraintText: 'For month use YYYY/MM.', + dateConstraintText: '(fallback) For date, use YYYY-MM-DD.', + dateTimeConstraintText: '(fallback) For date, use YYYY-MM-DD. For time, use 24 hour format.', + monthConstraintText: '(fallback) For month, use YYYY-MM.', modeSelectionLabel: 'Date range mode', relativeModeTitle: 'Relative range', absoluteModeTitle: 'Absolute range', @@ -35,3 +35,29 @@ export const i18nStrings: DateRangePickerProps.I18nStrings = { applyButtonLabel: 'Apply', renderSelectedAbsoluteRangeAriaLive: () => `Range selected from A to B`, }; + +export const i18nStringsWithExtraFormatConstraints: DateRangePickerProps.I18nStrings = { + ...i18nStrings, + slashedDateConstraintText: 'For date, use YYYY/MM/DD.', + slashedDateTimeConstraintText: 'For date, use YYYY/MM/DD. For time, use 24 hour format.', + isoDateConstraintText: 'For date, use YYYY-MM-DD.', + isoDateTimeConstraintText: 'For date, use YYYY-MM-DD. For time, use 24 hour format.', + slashedMonthConstraintText: 'For month, use YYYY/MM.', + isoMonthConstraintText: 'For month, use YYYY-MM.', +}; + +function createI18nMessages(i18nStrings: DateRangePickerProps.I18nStrings) { + return Object.entries(i18nStrings).reduce( + (acc, [key, value]) => { + if (typeof value === 'string') { + acc['date-range-picker'][`i18nStrings.${key}`] = `(i18n) ${value}`; + } + return acc; + }, + { 'date-range-picker': {} } as Record> + ); +} + +export const i18nMessages = createI18nMessages(i18nStrings); + +export const i18nMessagesWithExtraFormatConstraints = createI18nMessages(i18nStringsWithExtraFormatConstraints); diff --git a/src/date-range-picker/calendar/index.tsx b/src/date-range-picker/calendar/index.tsx index 04ffa49783..84de2b5adb 100644 --- a/src/date-range-picker/calendar/index.tsx +++ b/src/date-range-picker/calendar/index.tsx @@ -18,39 +18,26 @@ import { import { useUniqueId } from '@cloudscape-design/component-toolkit/internal'; -import { CalendarProps } from '../../calendar/interfaces'; import { getDateLabel, renderTimeLabel } from '../../calendar/utils/intl'; import { getBaseDay } from '../../calendar/utils/navigation-day'; import { getBaseMonth } from '../../calendar/utils/navigation-month'; import { useInternalI18n } from '../../i18n/context.js'; -import { BaseComponentProps } from '../../internal/base-component'; import { useMobile } from '../../internal/hooks/use-mobile/index.js'; import { formatDate, formatDateTime, parseDate, splitDateTime } from '../../internal/utils/date-time'; import { normalizeLocale, normalizeStartOfWeek } from '../../internal/utils/locale'; import InternalLiveRegion from '../../live-region/internal'; import SpaceBetween from '../../space-between/internal'; -import { TimeInputProps } from '../../time-input/interfaces'; -import { DateRangePickerProps, RangeCalendarI18nStrings } from '../interfaces'; +import { DateRangePickerProps } from '../interfaces'; import { Grids } from './grids'; import CalendarHeader from './header'; +import { DateRangePickerCalendarProps } from './interfaces'; import RangeInputs from './range-inputs.js'; import { findDateToFocus, findMonthToDisplay, findMonthToFocus, findYearToDisplay } from './utils'; import styles from '../styles.css.js'; import testutilStyles from '../test-classes/styles.css.js'; -export interface DateRangePickerCalendarProps extends BaseComponentProps, Pick { - value: DateRangePickerProps.PendingAbsoluteValue; - setValue: React.Dispatch>; - locale?: string; - startOfWeek?: number; - isDateEnabled?: (date: Date) => boolean; - dateDisabledReason?: (date: Date) => string; - i18nStrings?: RangeCalendarI18nStrings; - dateOnly?: boolean; - timeInputFormat?: TimeInputProps.Format; - customAbsoluteRangeControl: DateRangePickerProps.AbsoluteRangeControl | undefined; -} +export { DateRangePickerCalendarProps }; export default function DateRangePickerCalendar({ value, @@ -61,7 +48,8 @@ export default function DateRangePickerCalendar({ dateDisabledReason = () => '', i18nStrings, dateOnly = false, - timeInputFormat = 'hh:mm:ss', + timeInputFormat, + dateInputFormat, customAbsoluteRangeControl, granularity = 'day', }: DateRangePickerCalendarProps) { @@ -302,6 +290,7 @@ export default function DateRangePickerCalendar({ i18nStrings={i18nStrings} dateOnly={dateOnly} timeInputFormat={timeInputFormat} + dateInputFormat={dateInputFormat} granularity={granularity} /> {customAbsoluteRangeControl &&
{customAbsoluteRangeControl(value, interceptedSetValue)}
} diff --git a/src/date-range-picker/calendar/interfaces.ts b/src/date-range-picker/calendar/interfaces.ts new file mode 100644 index 0000000000..2cdc14878b --- /dev/null +++ b/src/date-range-picker/calendar/interfaces.ts @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { BaseComponentProps } from '../../internal/base-component'; +import { SomeRequired } from '../../internal/types'; +import { DateRangePickerProps } from '../interfaces'; + +export type RangeCalendarI18nStrings = Pick< + DateRangePickerProps.I18nStrings, + | 'todayAriaLabel' + | 'nextMonthAriaLabel' + | 'previousMonthAriaLabel' + | 'currentMonthAriaLabel' + | 'nextYearAriaLabel' + | 'previousYearAriaLabel' + | 'startMonthLabel' + | 'startDateLabel' + | 'startTimeLabel' + | 'endMonthLabel' + | 'endDateLabel' + | 'endTimeLabel' + | 'dateConstraintText' + | 'isoDateConstraintText' + | 'slashedDateConstraintText' + | 'dateTimeConstraintText' + | 'isoDateTimeConstraintText' + | 'slashedDateTimeConstraintText' + | 'monthConstraintText' + | 'isoMonthConstraintText' + | 'slashedMonthConstraintText' + | 'renderSelectedAbsoluteRangeAriaLive' +>; + +export interface DateRangePickerCalendarProps + extends BaseComponentProps, + SomeRequired< + Pick< + DateRangePickerProps, + | 'granularity' + | 'locale' + | 'startOfWeek' + | 'timeInputFormat' + | 'dateInputFormat' + | 'i18nStrings' + | 'dateOnly' + | 'absoluteFormat' + | 'customAbsoluteRangeControl' + | 'isDateEnabled' + | 'dateDisabledReason' + >, + 'absoluteFormat' | 'timeInputFormat' + > { + value: DateRangePickerProps.PendingAbsoluteValue; + setValue: React.Dispatch>; + i18nStrings?: RangeCalendarI18nStrings; +} + +export interface RangeInputsProps + extends BaseComponentProps, + SomeRequired< + Pick, + 'dateOnly' | 'timeInputFormat' | 'granularity' + > { + startDate: string; + onChangeStartDate: (value: string) => void; + startTime: string; + onChangeStartTime: (value: string) => void; + endDate: string; + onChangeEndDate: (value: string) => void; + endTime: string; + onChangeEndTime: (value: string) => void; + i18nStrings?: RangeCalendarI18nStrings; +} diff --git a/src/date-range-picker/calendar/range-inputs.tsx b/src/date-range-picker/calendar/range-inputs.tsx index 703139f67f..3f21ade19c 100644 --- a/src/date-range-picker/calendar/range-inputs.tsx +++ b/src/date-range-picker/calendar/range-inputs.tsx @@ -4,45 +4,16 @@ import React from 'react'; import clsx from 'clsx'; -import { CalendarProps } from '../../calendar/interfaces'; import InternalDateInput from '../../date-input/internal'; import InternalFormField from '../../form-field/internal'; import { useInternalI18n } from '../../i18n/context.js'; -import { BaseComponentProps } from '../../internal/base-component'; -import { TimeInputProps } from '../../time-input/interfaces'; import InternalTimeInput from '../../time-input/internal'; -import { RangeCalendarI18nStrings } from '../interfaces'; +import { RangeInputsProps } from './interfaces'; +import { generateI18NFallbackKey, generateI18NKey, provideI18N } from './utils'; import styles from '../styles.css.js'; import testutilStyles from '../test-classes/styles.css.js'; -type I18nStrings = Pick< - RangeCalendarI18nStrings, - | 'dateConstraintText' - | 'dateTimeConstraintText' - | 'monthConstraintText' - | 'startMonthLabel' - | 'startDateLabel' - | 'startTimeLabel' - | 'endMonthLabel' - | 'endDateLabel' - | 'endTimeLabel' ->; - -interface RangeInputsProps extends BaseComponentProps, Pick { - startDate: string; - onChangeStartDate: (value: string) => void; - startTime: string; - onChangeStartTime: (value: string) => void; - endDate: string; - onChangeEndDate: (value: string) => void; - endTime: string; - onChangeEndTime: (value: string) => void; - i18nStrings?: I18nStrings; - dateOnly: boolean; - timeInputFormat: TimeInputProps.Format; -} - export default function RangeInputs({ startDate, onChangeStartDate, @@ -55,28 +26,21 @@ export default function RangeInputs({ i18nStrings, dateOnly, timeInputFormat, - granularity = 'day', + dateInputFormat, + granularity, }: RangeInputsProps) { const i18n = useInternalI18n('date-range-picker'); const isMonthPicker = granularity === 'month'; - const dateInputPlaceholder = isMonthPicker ? 'YYYY/MM' : 'YYYY/MM/DD'; const showTimeInput = !dateOnly && !isMonthPicker; - + const parsedDateInputFormat = dateInputFormat; + const isIso = parsedDateInputFormat === 'iso'; + const separator = isIso ? '-' : '/'; + const dateInputPlaceholder = `YYYY${separator}MM${isMonthPicker ? '' : `${separator}DD`}`; + const i18nProvided = provideI18N(i18nStrings!, isMonthPicker, dateOnly, isIso); + const i18nKey = generateI18NKey(isMonthPicker, dateOnly, isIso); + const i18nFallbackKey = generateI18NFallbackKey(isMonthPicker, dateOnly); return ( - +
onChangeStartDate(event.detail.value)} + format={parsedDateInputFormat} placeholder={dateInputPlaceholder} granularity={granularity} /> @@ -119,6 +84,7 @@ export default function RangeInputs({ value={endDate} className={clsx(testutilStyles['end-date-input'], isMonthPicker && testutilStyles['end-month-picker'])} onChange={event => onChangeEndDate(event.detail.value)} + format={parsedDateInputFormat} placeholder={dateInputPlaceholder} granularity={granularity} /> diff --git a/src/date-range-picker/calendar/utils.ts b/src/date-range-picker/calendar/utils.ts index bd9fa52218..0dc45f6b90 100644 --- a/src/date-range-picker/calendar/utils.ts +++ b/src/date-range-picker/calendar/utils.ts @@ -4,6 +4,7 @@ import { addMonths, addYears, isSameMonth, isSameYear, startOfMonth, startOfYear import { parseDate } from '../../internal/utils/date-time'; import { DateRangePickerProps } from '../interfaces'; +import { RangeCalendarI18nStrings } from './interfaces'; export function findDateToFocus( selected: Date | null, @@ -69,3 +70,50 @@ export function findYearToDisplay(value: DateRangePickerProps.PendingAbsoluteVal } return startOfYear(Date.now()); } + +export const generateI18NFallbackKey = (isMonthPicker: boolean, isDateOnly: boolean) => { + if (isMonthPicker) { + return 'i18nStrings.monthConstraintText'; + } + if (isDateOnly) { + return 'i18nStrings.dateConstraintText'; + } + return 'i18nStrings.dateTimeConstraintText'; +}; + +export const generateI18NKey = (isMonthPicker: boolean, isDateOnly: boolean, isIso: boolean) => { + if (isMonthPicker) { + return isIso ? 'i18nStrings.isoMonthConstraintText' : 'i18nStrings.slashedMonthConstraintText'; + } + if (isDateOnly) { + return isIso ? 'i18nStrings.isoDateConstraintText' : 'i18nStrings.slashedDateConstraintText'; + } + return isIso ? 'i18nStrings.isoDateTimeConstraintText' : 'i18nStrings.slashedDateTimeConstraintText'; +}; + +export const provideI18N = ( + i18nStrings: RangeCalendarI18nStrings, + isMonthPicker: boolean, + isDateOnly: boolean, + isIso: boolean +): undefined | string => { + let result; + if (isMonthPicker) { + result = isIso ? i18nStrings?.isoMonthConstraintText : i18nStrings?.slashedMonthConstraintText; + if (!result) { + result = i18nStrings?.monthConstraintText; + } + } else if (isDateOnly) { + result = isIso ? i18nStrings?.isoDateConstraintText : i18nStrings?.slashedDateConstraintText; + if (!result) { + result = i18nStrings?.dateConstraintText; + } + } + if (!result) { + result = isIso ? i18nStrings?.isoDateTimeConstraintText : i18nStrings?.slashedDateTimeConstraintText; + if (!result) { + result = i18nStrings?.dateTimeConstraintText; + } + } + return result; +}; diff --git a/src/date-range-picker/dropdown.tsx b/src/date-range-picker/dropdown.tsx index 48e7301813..f9d45ed3a6 100644 --- a/src/date-range-picker/dropdown.tsx +++ b/src/date-range-picker/dropdown.tsx @@ -11,6 +11,7 @@ import { InternalButton } from '../button/internal'; import { CalendarProps } from '../calendar/interfaces'; import { useInternalI18n } from '../i18n/context'; import FocusLock from '../internal/components/focus-lock'; +import { SomeRequired } from '../internal/types'; import InternalLiveRegion, { InternalLiveRegionRef } from '../live-region/internal'; import InternalSpaceBetween from '../space-between/internal'; import Calendar from './calendar'; @@ -37,17 +38,22 @@ interface DateRangePickerDropdownProps | 'dateOnly' | 'rangeSelectorMode' >, - Pick< - DateRangePickerProps, - | 'startOfWeek' - | 'getTimeOffset' - | 'timeInputFormat' - | 'timeOffset' - | 'ariaLabelledby' - | 'ariaDescribedby' - | 'i18nStrings' - | 'customRelativeRangeUnits' - | 'dateDisabledReason' + SomeRequired< + Pick< + DateRangePickerProps, + | 'startOfWeek' + | 'getTimeOffset' + | 'absoluteFormat' + | 'timeInputFormat' + | 'dateInputFormat' + | 'timeOffset' + | 'ariaLabelledby' + | 'ariaDescribedby' + | 'i18nStrings' + | 'customRelativeRangeUnits' + | 'dateDisabledReason' + >, + 'absoluteFormat' | 'timeInputFormat' >, Pick { onClear: () => void; @@ -74,7 +80,9 @@ export function DateRangePickerDropdown({ isSingleGrid, i18nStrings, dateOnly, + absoluteFormat, timeInputFormat, + dateInputFormat, rangeSelectorMode, ariaLabelledby, ariaDescribedby, @@ -201,7 +209,9 @@ export function DateRangePickerDropdown({ dateDisabledReason={dateDisabledReason} i18nStrings={i18nStrings} dateOnly={dateOnly} + absoluteFormat={absoluteFormat} timeInputFormat={timeInputFormat} + dateInputFormat={dateInputFormat} customAbsoluteRangeControl={customAbsoluteRangeControl} granularity={granularity} /> diff --git a/src/date-range-picker/index.tsx b/src/date-range-picker/index.tsx index 6c3399e862..3164f2250d 100644 --- a/src/date-range-picker/index.tsx +++ b/src/date-range-picker/index.tsx @@ -107,6 +107,7 @@ const DateRangePicker = React.forwardRef( timeOffset, getTimeOffset, timeInputFormat = 'hh:mm:ss', + dateInputFormat = 'slashed', expandToViewport = false, rangeSelectorMode = 'default', customAbsoluteRangeControl, @@ -127,6 +128,7 @@ const DateRangePicker = React.forwardRef( readOnly, showClearButton, timeInputFormat, + dateInputFormat, hideTimeOffset, granularity, }, @@ -338,7 +340,9 @@ const DateRangePicker = React.forwardRef( relativeOptions={relativeOptions} isValidRange={isValidRange} dateOnly={dateOnly} + absoluteFormat={absoluteFormat} timeInputFormat={timeInputFormat} + dateInputFormat={dateInputFormat} rangeSelectorMode={rangeSelectorMode} ariaLabelledby={ariaLabelledby} ariaDescribedby={ariaDescribedby} diff --git a/src/date-range-picker/interfaces.ts b/src/date-range-picker/interfaces.ts index c98bc1cfeb..48d64cb432 100644 --- a/src/date-range-picker/interfaces.ts +++ b/src/date-range-picker/interfaces.ts @@ -7,6 +7,7 @@ import { BaseComponentProps } from '../internal/base-component'; import { ExpandToViewport } from '../internal/components/dropdown/interfaces'; import { FormFieldValidationControlProps } from '../internal/context/form-field-context'; import { NonCancelableEventHandler } from '../internal/events'; +import { DateFormat, EditableDateFormat } from '../internal/utils/date-time/interfaces'; import { TimeInputProps } from '../time-input/interfaces'; export interface DateRangePickerBaseProps { @@ -81,6 +82,15 @@ export interface DateRangePickerBaseProps { */ rangeSelectorMode?: DateRangePickerProps.RangeSelectorMode; + /** + * Specifies the date format to use on the date inputs in the absolute dropdown. + * + * The format of the input as it is being interacted with. It can take the following values: + * * `iso`: ISO 8601 format without time, e.g.: 2024-01-30 (or 2024-01) + * * `slashed`: similar to ISO 8601 but with '/' in place of '-'. e.g.: 2024/01/30 (or 2024/01) + */ + dateInputFormat?: DateRangePickerProps.DateInputFormat; + /** * Specifies the format of the time input for absolute ranges. * @@ -88,7 +98,7 @@ export interface DateRangePickerBaseProps { * * Has no effect when `dateOnly` is true or `granularity` is set to 'month'. */ - timeInputFormat?: TimeInputProps.Format; + timeInputFormat?: DateRangePickerProps.TimeInputFormat; /** * Fired whenever a user changes the component's value. @@ -183,8 +193,7 @@ export interface DateRangePickerProps * It can take the following values: * * `iso`: ISO 8601 format, e.g.: 2024-01-30T13:32:32+01:00 (or 2024-01-30 when `dateOnly` is true) * * `long-localized`: a more human-readable, localized format, e.g.: January 30, 2024, 13:32:32 (UTC+1) (or January 30, 2024 when `dateOnly` is true) - * - * Defaults to `iso`. + * * `slashed`: similar to ISO 8601 but with '/' in place of '-'. e.g.: 2024/01/30 (or 2024/01) */ absoluteFormat?: DateRangePickerProps.AbsoluteFormat; @@ -438,8 +447,9 @@ export namespace DateRangePickerProps { endDateLabel?: string; /** - * Visible label for the End Time input for the - * absolute range. + * Visible label for the End Time input for the absolute range. + * This serves as a fallback if no format specific date constraint test is provided + * * @i18n */ endTimeLabel?: string; @@ -452,19 +462,57 @@ export namespace DateRangePickerProps { dateConstraintText?: string; /** - * Constraint text for the input fields for the - * absolute range. + * Constraint text for the input fields for the absolute range in 'slashed' format with no time option. + * @i18n + */ + slashedDateConstraintText?: string; + + /** + * Constraint text for the input fields for the absolute range in 'iso' format with no time option. + * @i18n + */ + isoDateConstraintText?: string; + + /** + * Constraint text for the input fields for the absolute range. + * This serves as a fallback if no format specific datetime constraint test is provided + * * @i18n */ dateTimeConstraintText?: string; /** - * Constraint text for the month input fields for the - * absolute range. + * Constraint text for the date input fields for the absolute range in 'slashed' format. + * @i18n + */ + slashedDateTimeConstraintText?: string; + + /** + * Constraint text for the date input fields for the absolute range in 'iso' format with. + * @i18n + */ + isoDateTimeConstraintText?: string; + + /** + * Constraint text for the month input fields for the absolute range. + * This serves as a fallback if no format specific month constraint test is provided. + * * @i18n */ monthConstraintText?: string; + /** + * Constraint text for the month input fields for the absolute range in 'slashed' format. + * @i18n + */ + slashedMonthConstraintText?: string; + + /** + * Constraint text for the month input fields for the absolute range in 'iso' format. + * @i18n + */ + isoMonthConstraintText?: string; + /** * Provides a text alternative for the error icon in the error alert. * @i18n @@ -513,29 +561,13 @@ export namespace DateRangePickerProps { previousYearAriaLabel?: string; } - export type AbsoluteFormat = 'iso' | 'long-localized'; + export type AbsoluteFormat = DateFormat; + + export type DateInputFormat = EditableDateFormat | undefined; + + export type TimeInputFormat = TimeInputProps.Format; } export type DayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6; export type QuarterIndex = 0 | 1 | 2; - -export type RangeCalendarI18nStrings = Pick< - DateRangePickerProps.I18nStrings, - | 'todayAriaLabel' - | 'nextMonthAriaLabel' - | 'previousMonthAriaLabel' - | 'currentMonthAriaLabel' - | 'nextYearAriaLabel' - | 'previousYearAriaLabel' - | 'startMonthLabel' - | 'startDateLabel' - | 'startTimeLabel' - | 'endMonthLabel' - | 'endDateLabel' - | 'endTimeLabel' - | 'dateConstraintText' - | 'dateTimeConstraintText' - | 'monthConstraintText' - | 'renderSelectedAbsoluteRangeAriaLive' ->; diff --git a/src/internal/utils/date-time/format-date-time-with-offset.ts b/src/internal/utils/date-time/format-date-time-with-offset.ts index 33f920c462..8e9ca04126 100644 --- a/src/internal/utils/date-time/format-date-time-with-offset.ts +++ b/src/internal/utils/date-time/format-date-time-with-offset.ts @@ -25,6 +25,10 @@ export function formatDateTimeWithOffset({ case 'long-localized': { return formatDateLocalized({ date, hideTimeOffset, isDateOnly, isMonthOnly, locale, timeOffset }); } + case 'slashed': { + const formatted = formatDateIso({ date, hideTimeOffset, isDateOnly, isMonthOnly, timeOffset }).split('T'); + return `${formatted[0].split('-').join('/')}${formatted[1] ? `T${formatted[1]}` : ''}`; + } default: { return formatDateIso({ date, hideTimeOffset, isDateOnly, isMonthOnly, timeOffset }); } From 51e28ff460430172ac633078f586b7ca8dd2824d Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Fri, 12 Sep 2025 15:08:00 +0200 Subject: [PATCH 2/4] fix dev pages --- pages/date-range-picker/common.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pages/date-range-picker/common.tsx b/pages/date-range-picker/common.tsx index 7d628ab9e3..e46d2d4391 100644 --- a/pages/date-range-picker/common.tsx +++ b/pages/date-range-picker/common.tsx @@ -201,6 +201,7 @@ export function useDateRangePickerSettings( warning, rangeSelectorMode, absoluteFormat, + dateInputFormat, timeInputFormat, timeOffset, hideTimeOffset, @@ -270,6 +271,7 @@ export function Settings({ { value: 'overlapping-pages' }, ]; const dateFormatOptions = [{ value: 'iso' }, { value: 'slashed' }, { value: 'long-localized' }]; + const inputDateFormat = [{ value: 'iso' }, { value: 'slashed' }]; const timeFormatOptions = [{ value: 'hh:mm:ss' }, { value: 'hh:mm' }, { value: 'hh' }]; return ( @@ -303,8 +305,8 @@ export function Settings({