diff --git a/src/layout/Datepicker/DatePickerCalendar.tsx b/src/layout/Datepicker/DatePickerCalendar.tsx index 3002ff2f46..1a37b2f053 100644 --- a/src/layout/Datepicker/DatePickerCalendar.tsx +++ b/src/layout/Datepicker/DatePickerCalendar.tsx @@ -43,7 +43,7 @@ export const DatePickerCalendar = ({ }} locale={currentLocale} today={new Date()} - month={selectedDate} + defaultMonth={selectedDate} disabled={[{ before: minDate, after: maxDate }]} weekStartsOn={1} mode='single' diff --git a/src/layout/Datepicker/DatePickerInput.tsx b/src/layout/Datepicker/DatePickerInput.tsx index 1591e3febc..fa347cc8a3 100644 --- a/src/layout/Datepicker/DatePickerInput.tsx +++ b/src/layout/Datepicker/DatePickerInput.tsx @@ -1,81 +1,69 @@ -import React, { forwardRef, useEffect, useState } from 'react'; -import type { FocusEventHandler, RefObject } from 'react'; +import React, { useEffect, useState } from 'react'; +import { PatternFormat } from 'react-number-format'; -import { Button, Textfield } from '@digdir/designsystemet-react'; -import { CalendarIcon } from '@navikt/aksel-icons'; -import { format, isMatch, isValid } from 'date-fns'; +import { Textfield } from '@digdir/designsystemet-react'; +import { format, isValid } from 'date-fns'; -import { useLanguage } from 'src/features/language/useLanguage'; import styles from 'src/layout/Datepicker/Calendar.module.css'; -import { DatepickerSaveFormatNoTimestamp, DatepickerSaveFormatTimestamp } from 'src/utils/dateHelpers'; +import { getSaveFormattedDateString, strictParseFormat, strictParseISO } from 'src/utils/dateHelpers'; +import { getFormatPattern } from 'src/utils/formatDateLocale'; export interface DatePickerInputProps { id: string; + datepickerFormat: string; + timeStamp: boolean; value?: string; - formatString?: string; - onBlur?: FocusEventHandler; - onClick?: () => void; - isDialogOpen?: boolean; + onValueChange?: (value: string) => void; readOnly?: boolean; } -export const DatePickerInput = forwardRef( - ( - { id, value, formatString, onBlur, isDialogOpen, readOnly, onClick }: DatePickerInputProps, - ref: RefObject, - ) => { - const [input, setInput] = useState(value ?? ''); +export function DatePickerInput({ + id, + value, + datepickerFormat, + timeStamp, + onValueChange, + readOnly, +}: DatePickerInputProps) { + const formatPattern = getFormatPattern(datepickerFormat); + const dateValue = strictParseISO(value); + const formattedDateValue = dateValue ? format(dateValue, datepickerFormat) : value; + const [inputValue, setInputValue] = useState(formattedDateValue ?? ''); - const { langAsString } = useLanguage(); + useEffect(() => { + setInputValue(formattedDateValue ?? ''); + }, [formattedDateValue]); - useEffect(() => { - if (value) { - if (formatString && isMatch(value, formatString)) { - setInput(isValid(new Date(value)) ? format(value, formatString) : value); - } else if (isMatch(value, DatepickerSaveFormatNoTimestamp)) { - setInput(isValid(new Date(value)) ? format(value, formatString ?? 'dd.MM.yyyy') : value); - } else if (isMatch(value, DatepickerSaveFormatTimestamp)) { - setInput(isValid(new Date(value)) ? format(value, formatString ?? 'dd.MM.yyyy') : value); - } - } - }, [value, formatString]); + const saveValue = (e: React.ChangeEvent) => { + const stringValue = e.target.value; + const date = strictParseFormat(stringValue, datepickerFormat); + const valueToSave = getSaveFormattedDateString(date, timeStamp) ?? stringValue; + onValueChange && onValueChange(valueToSave); + }; - const handleInputChange = (e: React.ChangeEvent) => { - setInput(e.target.value); - }; + const handleChange = (e: React.ChangeEvent) => { + const stringValue = e.target.value; + setInputValue(stringValue); + // If the date is valid, save immediately + if (stringValue.length == 0 || isValid(strictParseFormat(stringValue, datepickerFormat))) { + saveValue(e); + } + }; - return ( -
- - -
- ); - }, -); - -DatePickerInput.displayName = 'DatePickerInput'; + return ( + + ); +} diff --git a/src/layout/Datepicker/DatepickerComponent.test.tsx b/src/layout/Datepicker/DatepickerComponent.test.tsx index 0e33b2c370..2b05abf017 100644 --- a/src/layout/Datepicker/DatepickerComponent.test.tsx +++ b/src/layout/Datepicker/DatepickerComponent.test.tsx @@ -179,12 +179,12 @@ describe('DatepickerComponent', () => { it('should call setLeafValue if not finished filling out the date', async () => { const { formDataMethods } = await render(); - await userEvent.type(screen.getByRole('textbox'), `12.34`); + await userEvent.type(screen.getByRole('textbox'), `1234`); await userEvent.tab(); expect(formDataMethods.setLeafValue).toHaveBeenCalledWith({ reference: { field: 'myDate', dataType: defaultDataTypeMock }, - newValue: '12.34', + newValue: '12.34.____', }); }); diff --git a/src/layout/Datepicker/DatepickerComponent.tsx b/src/layout/Datepicker/DatepickerComponent.tsx index 745a1f79ff..88c66e9f0d 100644 --- a/src/layout/Datepicker/DatepickerComponent.tsx +++ b/src/layout/Datepicker/DatepickerComponent.tsx @@ -1,9 +1,9 @@ -import React, { useRef, useState } from 'react'; -import type { ReactNode } from 'react'; +import React, { useState } from 'react'; -import { Modal, Popover } from '@digdir/designsystemet-react'; +import { Button } from '@digdir/designsystemet-react'; import { Grid } from '@material-ui/core'; -import { formatDate, isValid as isValidDate, parse, parseISO } from 'date-fns'; +import { CalendarIcon } from '@navikt/aksel-icons'; +import { formatDate, isValid as isValidDate } from 'date-fns'; import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; @@ -12,8 +12,10 @@ import { useIsMobile } from 'src/hooks/useDeviceWidths'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; import styles from 'src/layout/Datepicker/Calendar.module.css'; import { DatePickerCalendar } from 'src/layout/Datepicker/DatePickerCalendar'; +import { DatePickerDialog } from 'src/layout/Datepicker/DatepickerDialog'; import { DatePickerInput } from 'src/layout/Datepicker/DatePickerInput'; -import { getDateConstraint, getDateFormat, getLocale, getSaveFormattedDateString } from 'src/utils/dateHelpers'; +import { getDateConstraint, getDateFormat, getSaveFormattedDateString, strictParseISO } from 'src/utils/dateHelpers'; +import { getDatepickerFormat } from 'src/utils/formatDateLocale'; import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; @@ -24,75 +26,30 @@ export type IDatepickerProps = PropsFromGenericComponent<'Datepicker'>; export function DatepickerComponent({ node }: IDatepickerProps) { const { langAsString } = useLanguage(); const languageLocale = useCurrentLanguage(); - const currentLocale = getLocale(languageLocale ?? 'nb'); const { minDate, maxDate, format, timeStamp = true, readOnly, required, id, dataModelBindings } = useNodeItem(node); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const modalRef = useRef(null); const calculatedMinDate = getDateConstraint(minDate, 'min'); const calculatedMaxDate = getDateConstraint(maxDate, 'max'); - const dateFormat = getDateFormat(format || 'dd.MM.yyyy', languageLocale); + const dateFormat = getDatepickerFormat(getDateFormat(format, languageLocale)); const isMobile = useIsMobile(); const { setValue, formData } = useDataModelBindings(dataModelBindings); const value = formData.simpleBinding; - const selectedDate = isValidDate(parseISO(value)) ? parseISO(value) : new Date(); + const dateValue = strictParseISO(value); + const dayPickerDate = dateValue ? dateValue : new Date(); const handleDayPickerSelect = (date: Date) => { if (date && isValidDate(date)) { setValue('simpleBinding', getSaveFormattedDateString(date, timeStamp)); } - modalRef.current?.close(); setIsDialogOpen(false); }; - const handleInputChange = (e: React.ChangeEvent) => { - const parsed = parse(e.target.value, dateFormat, new Date()); - if (isValidDate(parsed)) { - setValue('simpleBinding', getSaveFormattedDateString(parsed, timeStamp)); - } else { - setValue('simpleBinding', e.target.value ?? ''); - } + const handleInputValueChange = (isoDateString: string) => { + setValue('simpleBinding', isoDateString); }; - const renderModal = (trigger: ReactNode, content: ReactNode) => - isMobile ? ( - <> - {trigger} - modalRef.current?.close()} - style={{ width: 'fit-content', minWidth: 'fit-content' }} - > - {content} - - - ) : ( - setIsDialogOpen(false)} - size='lg' - placement='top' - > - setIsDialogOpen(!isDialogOpen)} - asChild={true} - > - {trigger} - - - {content} - - - ); - return ( - {renderModal( +
(isMobile ? modalRef.current?.showModal() : setIsDialogOpen(!isDialogOpen))} + datepickerFormat={dateFormat} + timeStamp={timeStamp} + onValueChange={handleInputValueChange} readOnly={readOnly} - />, - , - )} + /> + setIsDialogOpen(!isDialogOpen)} + aria-expanded={isDialogOpen} + aria-label={langAsString('date_picker.aria_label_icon')} + disabled={readOnly} + color='first' + size='small' + > + + + } + > + + +
- {langAsString('date_picker.format_text', [formatDate(new Date(), dateFormat, { locale: currentLocale })])} + {langAsString('date_picker.format_text', [formatDate(new Date(), dateFormat)])}
diff --git a/src/layout/Datepicker/DatepickerDialog.tsx b/src/layout/Datepicker/DatepickerDialog.tsx new file mode 100644 index 0000000000..ea13c7119c --- /dev/null +++ b/src/layout/Datepicker/DatepickerDialog.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useRef } from 'react'; +import type { PropsWithChildren, ReactNode } from 'react'; + +import { Modal, Popover } from '@digdir/designsystemet-react'; + +import { useIsMobile } from 'src/hooks/useDeviceWidths'; +import styles from 'src/layout/Datepicker/Calendar.module.css'; + +export function DatePickerDialog({ + children, + trigger, + isDialogOpen, + setIsDialogOpen, +}: PropsWithChildren<{ trigger: ReactNode; isDialogOpen: boolean; setIsDialogOpen: (open: boolean) => void }>) { + const isMobile = useIsMobile(); + const modalRef = useRef(null); + + useEffect(() => { + isDialogOpen && modalRef.current?.showModal(); + !isDialogOpen && modalRef.current?.close(); + }, [isMobile, isDialogOpen]); + + if (isMobile) { + return ( + <> + {trigger} + setIsDialogOpen(false)} + style={{ width: 'fit-content', minWidth: 'fit-content' }} + > + {children} + + + ); + } + return ( + setIsDialogOpen(false)} + size='lg' + placement='top' + > + setIsDialogOpen(!isDialogOpen)} + asChild={true} + > + {trigger} + + + {children} + + + ); +} diff --git a/src/layout/Datepicker/DropdownCaption.tsx b/src/layout/Datepicker/DropdownCaption.tsx index 857a74bc3e..f71a82dca5 100644 --- a/src/layout/Datepicker/DropdownCaption.tsx +++ b/src/layout/Datepicker/DropdownCaption.tsx @@ -36,8 +36,7 @@ export const DropdownCaption = ({ calendarMonth, id }: MonthCaptionProps) => { const years = getYears(fromDate, toDate, calendarMonth.date.getFullYear()).reverse(); const months = getMonths(fromDate, toDate, calendarMonth.date); - const yearDropdownLabel = langAsString('date_picker.aria_label_year_dropdown'); - const MonthDropdownLabel = langAsString('date_picker.aria_label_month_dropdown'); + return ( <>
@@ -59,20 +58,21 @@ export const DropdownCaption = ({ calendarMonth, id }: MonthCaptionProps) => { size='small' value={[calendarMonth.date.getMonth().toString()]} onValueChange={(months) => handleMonthChange(months[0])} - aria-label={MonthDropdownLabel} + aria-label={langAsString('date_picker.aria_label_month_dropdown')} className={comboboxClasses.container} portal={!isMobile} > - {months.map((value) => ( + {months.map((date) => ( - + ))} @@ -82,7 +82,7 @@ export const DropdownCaption = ({ calendarMonth, id }: MonthCaptionProps) => { size='small' value={[calendarMonth.date.getFullYear().toString()]} onValueChange={(years) => handleYearChange(years[0])} - aria-label={yearDropdownLabel} + aria-label={langAsString('date_picker.aria_label_year_dropdown')} className={comboboxClasses.container} portal={!isMobile} > @@ -91,6 +91,7 @@ export const DropdownCaption = ({ calendarMonth, id }: MonthCaptionProps) => { {years.map((date) => ( { ); }; + +/** + * This is a workaround to make sure that the selected option is visible when opening the dropdown see: https://github.com/Altinn/app-frontend-react/issues/2637 + * The ref attribute will call this function with the option element, and this will try to scroll it into view once. + * When it succeeds, we set a 'data-scroll' attribute to make sure it does not keep trying so the user can scroll as they please. + */ +function scrollToIfSelected(selected: boolean) { + return selected + ? (el: HTMLButtonElement | null) => { + if ( + el && + el.parentElement && + // The option list seems to first render with unbounded height and later rendering with scroll-bars, we need to check again when the height changes + el.getAttribute('data-scroll-element-height') !== `${el.parentElement.clientHeight}` + ) { + el.parentElement.scrollTop = el.offsetTop; + if ( + el.offsetTop == 0 || // no scrolling necessary + el.parentElement.scrollHeight - el.parentElement.clientHeight == 0 || // container does not have scroll-bars + (el.offsetTop > 0 && el.parentElement.scrollTop > 0) // check that we have actually scrolled + ) { + el.setAttribute('data-scroll-element-height', `${el.parentElement.clientHeight}`); + } + } + } + : undefined; +} diff --git a/src/layout/Datepicker/index.tsx b/src/layout/Datepicker/index.tsx index 29a3257fb7..531f5e1d2a 100644 --- a/src/layout/Datepicker/index.tsx +++ b/src/layout/Datepicker/index.tsx @@ -1,14 +1,15 @@ import React, { forwardRef } from 'react'; import type { JSX } from 'react'; -import { isAfter, isBefore, isValid, parseISO } from 'date-fns'; +import { isAfter, isBefore } from 'date-fns'; import { FrontendValidationSource, ValidationMask } from 'src/features/validation'; import { DatepickerDef } from 'src/layout/Datepicker/config.def.generated'; import { DatepickerComponent } from 'src/layout/Datepicker/DatepickerComponent'; import { DatepickerSummary } from 'src/layout/Datepicker/DatepickerSummary'; import { SummaryItemSimple } from 'src/layout/Summary/SummaryItemSimple'; -import { formatISOString, getDateConstraint, getDateFormat } from 'src/utils/dateHelpers'; +import { formatISOString, getDateConstraint, getDateFormat, strictParseISO } from 'src/utils/dateHelpers'; +import { getDatepickerFormat } from 'src/utils/formatDateLocale'; import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation/types'; import type { DisplayDataProps } from 'src/features/displayData'; import type { BaseValidation, ComponentValidation, ValidationDataSources } from 'src/features/validation'; @@ -87,26 +88,27 @@ export class Datepicker extends DatepickerDef implements ValidateComponent<'Date nodeDataSelector((picker) => picker(node)?.item?.format, [node]), currentLanguage, ); + const datePickerFormat = getDatepickerFormat(format).toUpperCase(); const validations: ComponentValidation[] = []; - const date = parseISO(dataAsString); - if (!isValid(date)) { + const date = strictParseISO(dataAsString); + if (!date) { validations.push({ - message: { key: 'date_picker.invalid_date_message', params: [format] }, + message: { key: 'date_picker.invalid_date_message', params: [datePickerFormat] }, severity: 'error', source: FrontendValidationSource.Component, category: ValidationMask.Component, }); } - if (isBefore(date, minDate)) { + if (date && isBefore(date, minDate)) { validations.push({ message: { key: 'date_picker.min_date_exeeded' }, severity: 'error', source: FrontendValidationSource.Component, category: ValidationMask.Component, }); - } else if (isAfter(date, maxDate)) { + } else if (date && isAfter(date, maxDate)) { validations.push({ message: { key: 'date_picker.max_date_exeeded' }, severity: 'error', diff --git a/src/utils/dateHelpers.test.ts b/src/utils/dateHelpers.test.ts index 2a47adf845..925c87bfc5 100644 --- a/src/utils/dateHelpers.test.ts +++ b/src/utils/dateHelpers.test.ts @@ -1,16 +1,15 @@ import { jest } from '@jest/globals'; -import { format, parseISO } from 'date-fns'; +import { formatISO, isValid, parseISO } from 'date-fns'; import { DateFlags } from 'src/types'; import { DatepickerMaxDateDefault, DatepickerMinDateDefault, - DatepickerSaveFormatTimestamp, formatISOString, getDateConstraint, getDateFormat, getSaveFormattedDateString, - parseISOString, + strictParseISO, } from 'src/utils/dateHelpers'; describe('dateHelpers', () => { @@ -81,41 +80,40 @@ describe('dateHelpers', () => { tests.forEach(({ props, expected }) => { it(`should return ${expected} when called with ${JSON.stringify(props)}`, () => { const result = getDateConstraint(...props); - expect(format(result, DatepickerSaveFormatTimestamp)).toEqual(expected); + expect(formatISO(result, { representation: 'complete' })).toEqual(expected); }); }); }); - describe('parseISOString', () => { + describe('strictParseISO', () => { const tests: { - props: Parameters; - expected: Omit, 'date'> & { date: string | null }; + props: Parameters; + expected: { isValid: boolean; date: string | null }; }[] = [ - { props: [undefined], expected: { isValid: false, date: null, input: '' } }, - { props: ['asdf'], expected: { isValid: false, date: null, input: 'asdf' } }, - { props: ['2023-45-01'], expected: { isValid: false, date: null, input: '2023-45-01' } }, - { props: ['2023-05-34'], expected: { isValid: false, date: null, input: '2023-05-34' } }, + { props: [undefined], expected: { isValid: false, date: null } }, + { props: ['asdf'], expected: { isValid: false, date: null } }, + { props: ['2023-45-01'], expected: { isValid: false, date: null } }, + { props: ['2023-05-34'], expected: { isValid: false, date: null } }, { props: ['2023-13-33T23:00:00.000Z'], - expected: { isValid: false, date: null, input: '2023-13-33T23:00:00.000Z' }, + expected: { isValid: false, date: null }, }, - { props: ['2023-07-07'], expected: { isValid: true, date: '2023-07-07T00:00:00.000Z', input: undefined } }, + { props: ['2023-07-07'], expected: { isValid: true, date: '2023-07-07T00:00:00.000Z' } }, { props: ['2023-07-07T00:00:00.000Z'], - expected: { isValid: true, date: '2023-07-07T00:00:00.000Z', input: undefined }, + expected: { isValid: true, date: '2023-07-07T00:00:00.000Z' }, }, { props: ['2023-12-31T23:00:00.000Z'], - expected: { isValid: true, date: '2023-12-31T23:00:00.000Z', input: undefined }, + expected: { isValid: true, date: '2023-12-31T23:00:00.000Z' }, }, ]; tests.forEach(({ props, expected }) => { it(`should return ${JSON.stringify(expected)} when called with ${JSON.stringify(props)}`, () => { - const { isValid, date, input } = parseISOString(...props); - expect(isValid).toEqual(expected.isValid); + const date = strictParseISO(...props); + expect(isValid(date)).toEqual(expected.isValid); const dateStr = date?.toISOString() ?? null; expect(dateStr).toEqual(expected.date); - expect(input).toEqual(expected.input); }); }); }); diff --git a/src/utils/dateHelpers.ts b/src/utils/dateHelpers.ts index 6111652560..ec6fa4fe38 100644 --- a/src/utils/dateHelpers.ts +++ b/src/utils/dateHelpers.ts @@ -1,4 +1,4 @@ -import { endOfDay, formatDate, formatISO, isValid, parseISO, startOfDay } from 'date-fns'; +import { endOfDay, format, formatDate, formatISO, isValid, parse, parseISO, startOfDay } from 'date-fns'; import type { Locale } from 'date-fns/locale'; import { DateFlags } from 'src/types'; @@ -7,9 +7,7 @@ import { locales } from 'src/utils/dateLocales'; export const DatepickerMinDateDefault = '1900-01-01T00:00:00Z'; export const DatepickerMaxDateDefault = '2100-01-01T23:59:59Z'; export const DatepickerFormatDefault = 'dd.MM.yyyy'; -export const DatepickerSaveFormatTimestamp = "yyyy-MM-dd'T'HH:mm:ssXXXXX"; export const PrettyDateAndTime = 'dd.MM.yyyy HH.mm.ss'; -export const DatepickerSaveFormatNoTimestamp = 'yyyy-MM-dd'; export type DateResult = | { @@ -63,8 +61,8 @@ export function getDateConstraint(dateOrFlag: string | DateFlags | undefined, co return shiftTime(new Date()); } - const { date, isValid } = parseISOString(dateOrFlag); - if (isValid) { + const date = strictParseISO(dateOrFlag); + if (date && isValid(date)) { return shiftTime(date); } if (constraint === 'min') { @@ -75,10 +73,10 @@ export function getDateConstraint(dateOrFlag: string | DateFlags | undefined, co } export function formatISOString(isoString: string | undefined, format: string, locale?: Locale): string | null { - const isoDate = parseISOString(isoString).date; + const date = strictParseISO(isoString); - if (isoDate) { - return formatDate(isoDate, format, { locale }); + if (date && isValid(date)) { + return formatDate(date, format, { locale }); } else { return null; } @@ -92,19 +90,31 @@ export function getLocale(language: string): Locale { return locales[language] ?? locales.nb; } -export function parseISOString(isoString: string | undefined): DateResult { - const date = parseISO(isoString ?? ''); - if (isValid(date)) { - return { - isValid: true, - date, - input: undefined, - }; - } else { - return { - isValid: false, - date: null, - input: isoString ?? '', - }; +/** + * The date-fns parseISO function is a bit too lax for us, and will parse e.g. '01' as the date '0100-01-01', + * this function requires at least a full date to parse successfully. + * This prevents the value in the Datepicker input from changing while typing. + * It returns either a valid date or null + */ +export function strictParseISO(isoString: string | undefined): Date | null { + const minimumDate = 'yyyy-MM-dd'; + if (!isoString || isoString.length < minimumDate.length) { + return null; + } + const date = parseISO(isoString); + return isValid(date) ? date : null; +} + +/** + * The format function is a bit too lax, and will parse '01/01/1' (format: 'dd/MM/yyyy', which requires full year) as '01/01/0001', + * this function requires that the parsed date when formatted using the same format is equal to the input. + * This prevents the value in the Datepicker input from changing while typing. + */ +export function strictParseFormat(formattedDate: string | undefined, formatString: string): Date | null { + if (!formattedDate) { + return null; } + const date = parse(formattedDate, formatString, new Date()); + const newFormattedDate = isValid(date) ? format(date, formatString) : undefined; + return newFormattedDate && newFormattedDate === formattedDate ? date : null; } diff --git a/src/utils/formatDateLocale.ts b/src/utils/formatDateLocale.ts index 404fea3dc8..4087fa0fb1 100644 --- a/src/utils/formatDateLocale.ts +++ b/src/utils/formatDateLocale.ts @@ -114,6 +114,31 @@ export function formatDateLocale(localeStr: string, date: Date, unicodeFormat?: }, ''); } +/** + * This function will massage locale date formats to require a fixed number of characters so that a pattern-format can be used on the text input + */ +export function getDatepickerFormat(unicodeFormat: string): string { + const tokens = unicodeFormat.split(UNICODE_TOKENS) as Token[]; + const separators: Separator[] = unicodeFormat.match(UNICODE_TOKENS) ?? []; + + return tokens.reduce((acc, token: Token, index) => { + if (['y', 'yy', 'yyy', 'yyyy', 'u', 'uu', 'uuu', 'uuuu'].includes(token)) { + return `${acc}yyyy${separators?.[index] ?? ''}`; + } + if (['M', 'MM', 'MMM', 'MMMM', 'MMMMM'].includes(token)) { + return `${acc}MM${separators?.[index] ?? ''}`; + } + if (['d', 'dd'].includes(token)) { + return `${acc}dd${separators?.[index] ?? ''}`; + } + return acc; + }, ''); +} + +export function getFormatPattern(datePickerFormat: string): string { + return datePickerFormat.replaceAll(/d|m|y/gi, '#'); +} + function selectPartToUse(parts: Intl.DateTimeFormatPart[], token: Token) { if (['G', 'GG', 'GGG', 'GGGG', 'GGGGG'].includes(token)) { return parts.find((part) => part.type === 'era');