diff --git a/.github/workflows/manual-release-from-pr.yml b/.github/workflows/manual-release-from-pr.yml index 1b775c13ff..f2642d213c 100644 --- a/.github/workflows/manual-release-from-pr.yml +++ b/.github/workflows/manual-release-from-pr.yml @@ -16,8 +16,6 @@ jobs: run: npm ci - name: Set up project run: npm run bootstrap - - name: Run tests - run: npm run test:patchset - name: Release to NPM env: NPM_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/packages/ui-calendar/src/Calendar/index.tsx b/packages/ui-calendar/src/Calendar/index.tsx index a0c8e7a1cb..df960501f6 100644 --- a/packages/ui-calendar/src/Calendar/index.tsx +++ b/packages/ui-calendar/src/Calendar/index.tsx @@ -166,7 +166,7 @@ class Calendar extends Component { this.state.visibleMonth .clone() .locale('en') - .subtract({ months: 1 }) + .add({ months: 1 }) .format('YYYY') ) return ( @@ -318,6 +318,7 @@ class Calendar extends Component { **Important:** You can now use are updated version [`DateInput2`](/#DateInput2) which is easier to configure for developers, has a better UX, better accessibility features and a year picker. We recommend using that instead of `DateInput` which will be deprecated in the future. +> *Note:* you can now try the updated (but still experimental) [`DateInput2`](/#DateInput2) which is easier to configure for developers, has a better UX, better accessibility features and a year picker. We recommend using that instead of `DateInput` which will be deprecated in the future. The `DateInput` component provides a visual interface for inputting date data. diff --git a/packages/ui-date-input/src/DateInput2/README.md b/packages/ui-date-input/src/DateInput2/README.md index 86c7a2d837..926e5edc9c 100644 --- a/packages/ui-date-input/src/DateInput2/README.md +++ b/packages/ui-date-input/src/DateInput2/README.md @@ -2,28 +2,37 @@ describes: DateInput2 --- -This component is an updated version of [`DateInput`](/#DateInput) that's easier to configure for developers, has a better UX, better accessibility features and a year picker. We recommend using this instead of `DateInput` which will be deprecated in the future. +> *Warning*: `DateInput2` is an **experimental** upgrade to the existing [`DateInput`](/#DateInput) component, offering easier configuration, better UX, improved accessibility, and a year picker. While it addresses key limitations of `DateInput`, it's still in the experimental phase, with some missing unit tests and potential API changes. ### Minimal config - ```js class Example extends React.Component { - state = { value: '' } + state = { inputValue: '', dateString: '' } render() { return ( - this.setState({ value })} - invalidDateErrorMessage="Invalid date" - /> +
+ { + this.setState({ dateString, inputValue }) + }} + invalidDateErrorMessage="Invalid date" + /> +

+ Input Value: {this.state.inputValue} +
+ UTC Date String: {this.state.dateString} +

+
) } } @@ -33,8 +42,59 @@ This component is an updated version of [`DateInput`](/#DateInput) that's easier - ```js const Example = () => { - const [value, setValue] = useState('') + const [inputValue, setInputValue] = useState('') + const [dateString, setDateString] = useState('') return ( +
+ { + setInputValue(inputValue) + setDateString(dateString) + }} + invalidDateErrorMessage="Invalid date" + /> +

+ Input Value: {inputValue} +
+ UTC Date String: {dateString} +

+
+ ) + } + + render() + ``` + +### Parsing and formatting dates + +When typing in a date manually (instead of using the included picker), the component tries to parse the date as you type it in. By default parsing is based on the user's locale which determines the order of day, month and year (e.g.: a user with US locale will have MONTH/DAY/YEAR order, and someone with GB locale will have DAY/MONTH/YEAR order). + +Any of the following separators can be used when typing a date: `,`, `-`, `.`, `/` or a whitespace however on blur the date will be formatted according to the locale and separators will be changed and leading zeros also adjusted. + +If you want different parsing and formatting then the current locale you can use the `dateFormat` prop which accepts either a string with a name of a different locale (so you can use US date format even if the user is France) or a parser and formatter functions. + +The default parser also has a limitation of not working with years before `1000` and after `9999`. These values are invalid by default but not with custom parsers. + +```js +--- +type: example +--- +const Example = () => { + const [value, setValue] = useState('') + const [value2, setValue2] = useState('') + const [value3, setValue3] = useState('') + + return ( +
+

US locale with default format:

setValue(value)} - invalidDateErrorMessage="Invalid date" /> - ) - } +

US locale with german date format:

+ setValue2(value)} + /> +

US locale with ISO date format:

+ { + // split input on '.', whitespace, '/', ',' or '-' using regex: /[.\s/.-]+/ + // the '+' allows splitting on consecutive delimiters + const [year, month, day] = input.split(/[,.\s/.-]+/) + const newDate = new Date(year, month-1, day) + return isNaN(newDate) ? '' : newDate + }, + formatter: (date) => { + // vanilla js formatter but you could use a date library instead + const year = date.getFullYear() + // month is zero indexed so add 1 + const month = `${date.getMonth() + 1}`.padStart(2, '0') + const day = `${date.getDate()}`.padStart(2, '0') + return `${year}-${month}-${day}` + } + }} + onChange={(e, value) => setValue3(value)} + /> +
+ ) +} - render() - ``` +render() +``` + +### Timezones + +In the examples above you can see that the `onChange` callback also return a UTC date string. This means it is timezone adjusted. If the timezone is not set via the `timezone` prop, it is calculated/assumed from the user's machine. So if a user chooses September 10th 2024 with the timezone 'Europe/Budapest', the `onChange` function will return `2024-09-09T22:00:00.000Z` because Budapest is two hours ahead of UTC (summertime). ### With year picker - ```js class Example extends React.Component { - state = { value: '' } + state = { inputValue: '', dateString: '' } render() { return ( - this.setState({ value })} - invalidDateErrorMessage="Invalid date" - withYearPicker={{ - screenReaderLabel: 'Year picker', - startYear: 1900, - endYear: 2024 - }} - /> +
+ { + this.setState({ dateString, inputValue }) + }} + invalidDateErrorMessage="Invalid date" + withYearPicker={{ + screenReaderLabel: 'Year picker', + startYear: 1900, + endYear: 2024 + }} + /> +

+ Input Value: {this.state.inputValue} +
+ UTC Date String: {this.state.dateString} +

+
) } } @@ -87,26 +205,36 @@ This component is an updated version of [`DateInput`](/#DateInput) that's easier - ```js const Example = () => { - const [value, setValue] = useState('') - + const [inputValue, setInputValue] = useState('') + const [dateString, setDateString] = useState('') return ( - setValue(value)} - invalidDateErrorMessage="Invalid date" - withYearPicker={{ - screenReaderLabel: 'Year picker', - startYear: 1900, - endYear: 2024 - }} - /> +
+ { + setInputValue(inputValue) + setDateString(dateString) + }} + invalidDateErrorMessage="Invalid date" + withYearPicker={{ + screenReaderLabel: 'Year picker', + startYear: 1900, + endYear: 2024 + }} + /> +

+ Input Value: {inputValue} +
+ UTC Date String: {dateString} +

+
) } @@ -115,9 +243,9 @@ This component is an updated version of [`DateInput`](/#DateInput) that's easier ### Date validation -By default `DateInput2` only does date validation if the `invalidDateErrorMessage` prop is provided. This uses the browser's `Date` object to try an parse the user provided date and displays the error message if it fails. Validation is only triggered on the blur event of the input field. +By default `DateInput2` only does date validation if the `invalidDateErrorMessage` prop is provided. Validation is triggered on the blur event of the input field. Invalid dates are determined current locale. -If you want to do a more complex validation than the above (e.g. only allow a subset of dates) you can use the `onRequestValidateDate` prop to pass a validation function. This function will run on blur or on selecting the date from the picker. The result of the internal validation will be passed to this function. Then you have to set the error messages accordingly. Check the following example for more details: +If you want to do more complex validation (e.g. only allow a subset of dates) you can use the `onRequestValidateDate` and `messages` props. ```js --- @@ -125,18 +253,24 @@ type: example --- const Example = () => { const [value, setValue] = useState('') + const [dateString, setDateString] = useState('') const [messages, setMessages] = useState([]) - const handleDateValidation = (dateString, isValidDate) => { - if (!isValidDate) { + const handleDateValidation = (e, inputValue, utcIsoDate) => { + // utcIsoDate will be an empty string if the input cannot be parsed as a date + + const date = new Date(utcIsoDate) + + // don't validate empty input + if (!utcIsoDate && inputValue.length > 0) { setMessages([{ type: 'error', text: 'This is not a valid date' }]) - } else if (new Date(dateString) < new Date('January 1, 1900')) { + } else if (date < new Date('1990-01-01')) { setMessages([{ type: 'error', - text: 'Use date after January 1, 1900' + text: 'Select date after January 1, 1990' }]) } else { setMessages([]) @@ -145,7 +279,7 @@ const Example = () => { return ( { render() ``` -### Date formatting - -The display format of the dates can be set via the `formatDate` property. It will be applied if the user clicks on a date in the date picker of after blur event from the input field. -Something to pay attention to is that the date string passed back in the callback function **is in UTC timezone**. - -```js ---- -type: example ---- -const Example = () => { - const [value1, setValue1] = useState('') - const [value2, setValue2] = useState('') - const [value3, setValue3] = useState('') - - const shortDateFormatFn = (dateString, locale, timezone) => { - return new Date(dateString).toLocaleDateString(locale, { - month: 'numeric', - year: 'numeric', - day: 'numeric', - timeZone: timezone, - }) - } - - const isoDateFormatFn = (dateString, locale, timezone) => { - // this is a simple way to get ISO8601 date in a specific timezone but should not be used in production - // please use a proper date library instead like date-fns, luxon or dayjs - const localeDate = new Date(dateString).toLocaleDateString('sv', { - month: 'numeric', - year: 'numeric', - day: 'numeric', - timeZone: timezone, - }) - - return localeDate - } +### Date format hint - return ( -
- setValue1(value)} - withYearPicker={{ - screenReaderLabel: 'Year picker', - startYear: 1900, - endYear: 2024 - }} - /> - setValue2(value)} - formatDate={shortDateFormatFn} - withYearPicker={{ - screenReaderLabel: 'Year picker', - startYear: 1900, - endYear: 2024 - }} - /> - setValue3(value)} - formatDate={isoDateFormatFn} - withYearPicker={{ - screenReaderLabel: 'Year picker', - startYear: 1900, - endYear: 2024 - }} - /> -
- ) -} - -render() -``` +If the `placeholder` property is undefined it will display a hint for the date format (like `DD/MM/YYYY`). Usually it is recommended to leave it as it is for a better user experience. diff --git a/packages/ui-date-input/src/DateInput2/index.tsx b/packages/ui-date-input/src/DateInput2/index.tsx index 9f56e5ac68..6c9cf1d791 100644 --- a/packages/ui-date-input/src/DateInput2/index.tsx +++ b/packages/ui-date-input/src/DateInput2/index.tsx @@ -44,35 +44,93 @@ import type { DateInput2Props } from './props' import type { FormMessage } from '@instructure/ui-form-field' import type { Moment } from '@instructure/ui-i18n' -function parseDate(dateString: string): string { - const date = new Date(dateString) - return isNaN(date.getTime()) ? '' : date.toISOString() -} +function parseLocaleDate(dateString: string = '', locale: string, timeZone: string): Date | null { + // This function may seem complicated but it basically does one thing: + // Given a dateString, a locale and a timeZone. The dateString is assumed to be formatted according + // to the locale. So if the locale is `en-us` the dateString is expected to be in the format of M/D/YYYY. + // The dateString is also assumed to be in the given timeZone, so "1/1/2020" in "America/Los_Angeles" timezone is + // expected to be "2020-01-01T08:00:00.000Z" in UTC time. + // This function tries to parse the dateString taking these variables into account and return a javascript Date object + // that is adjusted to be in UTC. + + // Split string on '.', whitespace, '/', ',' or '-' using regex: /[.\s/.-]+/. + // The '+' allows splitting on consecutive delimiters. + // `.filter(Boolean)` is needed because some locales have a delimeter at the end (e.g.: hungarian dates are formatted as `2024. 09. 19.`) + const splitDate = dateString.split(/[,.\s/.-]+/).filter(Boolean) + + // create a locale formatted new date to later extract the order and delimeter information + const localeDate = new Intl.DateTimeFormat(locale).formatToParts(new Date()) -function defaultDateFormatter( - dateString: string, - locale: string, - timezone: string -) { - return new Date(dateString).toLocaleDateString(locale, { - month: 'long', - year: 'numeric', - day: 'numeric', - timeZone: timezone + let index = 0 + let day: number | undefined, month: number | undefined, year: number | undefined + localeDate.forEach((part) => { + if (part.type === 'month') { + month = parseInt(splitDate[index], 10) + index++ + } else if (part.type === 'day') { + day = parseInt(splitDate[index], 10) + index++ + } else if (part.type === 'year') { + year = parseInt(splitDate[index], 10) + index++ + } }) + + // sensible limitations + if (!year || !month || !day || year < 1000 || year > 9999) return null + + // create utc date from year, month (zero indexed) and day + const date = new Date(Date.UTC(year, month - 1, day)) + + if (date.getMonth() !== month - 1 || date.getDate() !== day) { + // Check if the Date object adjusts the values. If it does, the input is invalid. + return null + } + + // Format date string in the provided timezone. The locale here is irrelevant, we only care about how to time is adjusted for the timezone. + const parts = new Intl.DateTimeFormat('en-US', { + timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }).formatToParts(date) + + // Extract the date and time parts from the formatted string + const dateStringInTimezone: { + [key: string]: number + } = parts.reduce((acc, part) => { + return part.type === 'literal' ? acc : { + ...acc, + [part.type]: part.value, + } + }, {}) + + // Create a date string in the format 'YYYY-MM-DDTHH:mm:ss' + const dateInTimezone = `${dateStringInTimezone.year}-${dateStringInTimezone.month}-${dateStringInTimezone.day}T${dateStringInTimezone.hour}:${dateStringInTimezone.minute}:${dateStringInTimezone.second}` + + // Calculate time difference for timezone offset + const timeDiff = new Date(dateInTimezone + 'Z').getTime() - date.getTime() + const utcTime = new Date(date.getTime() - timeDiff) + // Return the UTC Date corresponding to the time in the specified timezone + return utcTime } /** --- category: components --- + +@module experimental **/ const DateInput2 = ({ renderLabel, screenReaderLabels, isRequired = false, interaction = 'enabled', - size = 'medium', isInline = false, value, messages, @@ -80,114 +138,137 @@ const DateInput2 = ({ onChange, onBlur, withYearPicker, - onRequestValidateDate, invalidDateErrorMessage, locale, timezone, placeholder, - formatDate = defaultDateFormatter, + dateFormat, + onRequestValidateDate, // margin, TODO enable this prop ...rest }: DateInput2Props) => { - const [selectedDate, setSelectedDate] = useState('') + const localeContext = useContext(ApplyLocaleContext) + + const getLocale = () => { + if (locale) { + return locale + } else if (localeContext.locale) { + return localeContext.locale + } + // default to the system's locale + return Locale.browserLocale() + } + + const getTimezone = () => { + if (timezone) { + return timezone + } else if (localeContext.timezone) { + return localeContext.timezone + } + // default to the system's timezone + return Intl.DateTimeFormat().resolvedOptions().timeZone + } + const [inputMessages, setInputMessages] = useState( messages || [] ) const [showPopover, setShowPopover] = useState(false) - const localeContext = useContext(ApplyLocaleContext) useEffect(() => { - // when `value` is changed, validation removes the error message if passes - // but it's NOT adding error message if validation fails for better UX - validateInput(true) - }, [value]) - - useEffect(() => { - setInputMessages(messages || []) + // don't set input messages if there is an error set already + if (!inputMessages) { + setInputMessages(messages || []) + } }, [messages]) useEffect(() => { - setSelectedDate(parseDate(value || '')) - }, []) + const [, utcIsoDate] = parseDate(value) + // clear error messages if date becomes valid + if (utcIsoDate || !value) { + setInputMessages(messages || []) + } + }, [value]) - const handleInputChange = ( - e: SyntheticEvent, - newValue: string, - parsedDate: string = '' - ) => { - // blur event formats the input which shouldn't trigger parsing - if (e.type !== 'blur') { - setSelectedDate(parseDate(newValue)) + const parseDate = (dateString: string = ''): [string, string] => { + let date: Date | null = null + if (dateFormat) { + if (typeof dateFormat === 'string') { + // use dateFormat instead of the user locale + date = parseLocaleDate(dateString, dateFormat, getTimezone()) + } else if (dateFormat.parser) { + date = dateFormat.parser(dateString) + } + } else { + // no dateFormat prop passed, use locale for formatting + date = parseLocaleDate(dateString, getLocale(), getTimezone()) } - onChange?.(e, newValue, parsedDate) + return date ? [formatDate(date), date.toISOString()] : ['', ''] } - const handleDateSelected = ( - dateString: string, - _momentDate: Moment, - e: SyntheticEvent - ) => { - const formattedDate = formatDate(dateString, getLocale(), getTimezone()) - const parsedDate = parseDate(dateString) - setSelectedDate(parsedDate) - handleInputChange(e, formattedDate, parsedDate) - setShowPopover(false) - onRequestValidateDate?.(dateString, true) + const formatDate = (date: Date): string => { + // use formatter function if provided + if (typeof dateFormat !== 'string' && dateFormat?.formatter) { + return dateFormat.formatter(date) + } + // if dateFormat set to a locale, use that, otherwise default to the user's locale + return date.toLocaleDateString(typeof dateFormat === 'string' ? dateFormat : getLocale(), {timeZone: getTimezone(), calendar: 'gregory', numberingSystem: 'latn'}) } - // onlyRemoveError is used to remove the error msg immediately when the user inputs a valid date (and don't wait for blur event) - const validateInput = (onlyRemoveError = false): boolean => { - // don't validate empty input - if (!value || parseDate(value) || selectedDate) { - setInputMessages(messages || []) - return true - } - // only show error if there is no user provided validation callback - if ( - !onlyRemoveError && - typeof invalidDateErrorMessage === 'string' && - !onRequestValidateDate - ) { - setInputMessages([ - { - type: 'error', - text: invalidDateErrorMessage - } - ]) + const getDateFromatHint = () => { + const exampleDate = new Date('2024-09-01') + const formattedDate = formatDate(exampleDate) + + // Create a regular expression to find the exact match of the number + const regex = (n: string) => { + return new RegExp(`(? 'Y'.repeat(match.length)) + .replace(regex(month), (match) => 'M'.repeat(match.length)) + .replace(regex(day), (match) => 'D'.repeat(match.length)) } - const getLocale = () => { - if (locale) { - return locale - } else if (localeContext.locale) { - return localeContext.locale - } - return Locale.browserLocale() + const handleInputChange = (e: SyntheticEvent, newValue: string) => { + const [, utcIsoDate] = parseDate(newValue) + onChange?.(e, newValue, utcIsoDate) } - const getTimezone = () => { - if (timezone) { - return timezone - } else if (localeContext.timezone) { - return localeContext.timezone - } - // default to the system's timezone - return Intl.DateTimeFormat().resolvedOptions().timeZone + const handleDateSelected = ( + dateString: string, + _momentDate: Moment, + e: SyntheticEvent + ) => { + setShowPopover(false) + const newValue = formatDate(new Date(dateString)) + onChange?.( + e, + newValue, + dateString + ) + onRequestValidateDate?.(e, newValue, dateString) } const handleBlur = (e: SyntheticEvent) => { - const isInputValid = validateInput(false) - if (isInputValid && selectedDate) { - const formattedDate = formatDate(selectedDate, getLocale(), getTimezone()) - handleInputChange(e, formattedDate, selectedDate) + const [localeDate, utcIsoDate] = parseDate(value) + if (localeDate) { + if (localeDate !== value) { + onChange?.(e, localeDate, utcIsoDate) + } + } else if (value && invalidDateErrorMessage) { + setInputMessages([ + {type: 'error', text: invalidDateErrorMessage} + ]) } - onRequestValidateDate?.(value, isInputValid) - onBlur?.(e) + onRequestValidateDate?.(e, value || '', utcIsoDate) + onBlur?.(e, value || '', utcIsoDate) } + const selectedDate = parseDate(value)[1] return ( diff --git a/packages/ui-date-input/src/DateInput2/props.ts b/packages/ui-date-input/src/DateInput2/props.ts index 71e0732e56..b6906edffe 100644 --- a/packages/ui-date-input/src/DateInput2/props.ts +++ b/packages/ui-date-input/src/DateInput2/props.ts @@ -47,14 +47,9 @@ type DateInput2OwnProps = { /** * Specifies the input value. */ - value?: string // TODO: controllable(PropTypes.string) + value?: string /** - * Specifies the input size. - */ - size?: 'small' | 'medium' | 'large' - /** - * Html placeholder text to display when the input has no value. This should - * be hint text, not a label replacement. + * Placeholder text for the input field. If it's left undefined it will display a hint for the date format (like `DD/MM/YYYY`). */ placeholder?: string /** @@ -63,12 +58,12 @@ type DateInput2OwnProps = { onChange?: ( event: React.SyntheticEvent, inputValue: string, - dateString: string + utcDateString: string ) => void /** * Callback executed when the input fires a blur event. */ - onBlur?: (event: React.SyntheticEvent) => void + onBlur?: (event: React.SyntheticEvent, value: string, utcDateString: string) => void /** * Specifies if interaction with the input is enabled, disabled, or readonly. * When "disabled", the input changes visibly to indicate that it cannot @@ -98,24 +93,6 @@ type DateInput2OwnProps = { * }` */ messages?: FormMessage[] - /** - * Callback fired requesting the calendar be shown. - */ - onRequestShowCalendar?: (event: SyntheticEvent) => void - /** - * Callback fired requesting the calendar be hidden. - */ - onRequestHideCalendar?: (event: SyntheticEvent) => void - /** - * Callback fired when the input is blurred. Feedback should be provided - * to the user when this function is called if the selected date or input - * value is invalid. The component has an internal check whether the date can - * be parsed to a valid date. - */ - onRequestValidateDate?: ( - value?: string, - internalValidationPassed?: boolean - ) => void | FormMessage[] /** * The message shown to the user when the date is invalid. If this prop is not set, validation is bypassed. * If it's set to an empty string, validation happens and the input border changes to red if validation hasn't passed. @@ -143,7 +120,7 @@ type DateInput2OwnProps = { * This property can also be set via a context property and if both are set * then the component property takes precedence over the context property. * - * The web browser's timezone will be used if no value is set via a component + * The system timezone will be used if no value is set via a component * property or a context property. **/ timezone?: string @@ -165,18 +142,19 @@ type DateInput2OwnProps = { startYear: number endYear: number } - /** - * Formatting function for how the date should be displayed inside the input field. It will be applied if the user clicks on a date in the date picker of after blur event from the input field. - */ - formatDate?: (isoDate: string, locale: string, timezone: string) => string + * By default the date format is determined by the locale but can be changed via this prop to an alternate locale (passing it in as a string) or a custom parser and formatter (both as functions) + */ + dateFormat?: { + parser: (input: string) => Date | null + formatter: (date: Date) => string + } | string /** - * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`, - * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via - * familiar CSS-like shorthand. For example: `margin="small auto large"`. + * Callback executed when the input fires a blur event or a date is selected from the picker. */ - // margin?: Spacing TODO enable this prop + onRequestValidateDate?: (event: React.SyntheticEvent, value: string, utcDateString: string) => void + // margin?: Spacing // TODO enable this prop } type PropKeys = keyof DateInput2OwnProps @@ -191,7 +169,6 @@ const propTypes: PropValidators = { renderLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, screenReaderLabels: PropTypes.object.isRequired, value: controllable(PropTypes.string), - size: PropTypes.oneOf(['small', 'medium', 'large']), placeholder: PropTypes.string, onChange: PropTypes.func, onBlur: PropTypes.func, @@ -200,9 +177,6 @@ const propTypes: PropValidators = { isInline: PropTypes.bool, width: PropTypes.string, messages: PropTypes.arrayOf(FormPropTypes.message), - onRequestShowCalendar: PropTypes.func, - onRequestHideCalendar: PropTypes.func, - onRequestValidateDate: PropTypes.func, invalidDateErrorMessage: PropTypes.oneOfType([ PropTypes.func, PropTypes.string @@ -210,7 +184,11 @@ const propTypes: PropValidators = { locale: PropTypes.string, timezone: PropTypes.string, withYearPicker: PropTypes.object, - formatDate: PropTypes.func + dateFormat: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ]), + onRequestValidateDate: PropTypes.func, } export type { DateInput2Props }