diff --git a/src/components/DateField/README.md b/src/components/DateField/README.md index 76228b49..c47c3df4 100644 --- a/src/components/DateField/README.md +++ b/src/components/DateField/README.md @@ -347,44 +347,70 @@ LANDING_BLOCK--> +### Custom Date Parser + +You can provide a custom parser function to handle pasted date strings through the `parseDateFromString` prop. This is useful when you need to support specific date formats or custom parsing logic that differs from the default behavior. + + + +```tsx +import {dateTime} from '@gravity-ui/date-utils'; + +const customParser = (dateStr: string, format: string, timeZone?: string) => { + // Custom parsing logic + // For example, handle DD/MM/YYYY format specifically + if (dateStr.match(/^\d{2}\/\d{2}\/\d{4}$/)) { + const [day, month, year] = dateStr.split('/'); + return dateTime({input: `${year}-${month}-${day}`, timeZone}); + } + // Fallback to default parsing + return dateTime({input: dateStr, format, timeZone}); +}; + +; +``` + + + ## Time zone `timeZone` is the property to set the time zone of the value in the input. [Learn more about time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) ## Properties -| Name | Description | Type | Default | -| :---------------- | :------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-----------------------: | -| aria-describedby | The control's `aria-describedby` attribute | `string` | | -| aria-details | The control's `aria-details` attribute | `string` | | -| aria-label | The control's `aria-label` attribute | `string` | | -| aria-labelledby | The control's `aria-labelledby` attribute | `string` | | -| autoFocus | The control's `autofocus` attribute | `boolean` | | -| className | The control's wrapper class name | `string` | | -| defaultValue | Sets the initial value for uncontrolled component. | `DateTime` | | -| disabled | Indicates that the user cannot interact with the control | `boolean` | `false` | -| errorMessage | Error text | `ReactNode` | | -| format | Format of the date when rendered in the input. [Available formats](https://day.js.org/docs/en/display/format) | `string` | | -| hasClear | Shows the icon for clearing control's value | `boolean` | `false` | -| id | The control's `id` attribute | `string` | | -| isDateUnavailable | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | `((date: DateTime) => boolean)` | | -| label | Help text rendered to the left of the input node | `string` | | -| startContent | The user`s node rendered before label and input | `React.ReactNode` | | -| maxValue | The maximum allowed date that a user may select. | `DateTime` | | -| minValue | The minimum allowed date that a user may select. | `DateTime` | | -| onBlur | Fires when the control lost focus. Provides focus event as a callback's argument | `((e: FocusEvent) => void)` | | -| onFocus | Fires when the control gets focus. Provides focus event as a callback's argument | `((e: FocusEvent) => void)` | | -| onKeyDown | Fires when a key is pressed. Provides keyboard event as a callback's argument | `((e: KeyboardEvent) => void)` | | -| onKeyUp | Fires when a key is released. Provides keyboard event as a callback's argument | `((e: KeyboardEvent) => void)` | | -| onUpdate | Fires when the value is changed by the user. Provides new value as an callback's argument | `((value: DateTime \| null) => void` | | -| pin | Corner rounding | `string` | `'round-round'` | -| placeholder | Text that appears in the control when it has no value set | `string` | | -| placeholderValue | A placeholder date that controls the default values of each segment when the user first interacts with them. | `DateTime` | `today's date at midnigh` | -| readOnly | Whether the component's value is immutable. | `boolean` | `false` | -| endContent | User`s node rendered after the input node and clear button | `React.ReactNode` | | -| size | The size of the control | `"s"` `"m"` `"l"` `"xl"` | `"m"` | -| style | Sets inline style for the element. | `CSSProperties` | | -| timeZone | Sets the time zone. [Learn more about time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) | `string` | | -| validationState | Validation state | `"invalid"` | | -| value | The value of the control | `DateTime` `null` | | -| view | The view of the control | `"normal"` `"clear"` | `"normal"` | +| Name | Description | Type | Default | +| :------------------ | :------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------: | :-----------------------: | +| aria-describedby | The control's `aria-describedby` attribute | `string` | | +| aria-details | The control's `aria-details` attribute | `string` | | +| aria-label | The control's `aria-label` attribute | `string` | | +| aria-labelledby | The control's `aria-labelledby` attribute | `string` | | +| autoFocus | The control's `autofocus` attribute | `boolean` | | +| className | The control's wrapper class name | `string` | | +| defaultValue | Sets the initial value for uncontrolled component. | `DateTime` | | +| disabled | Indicates that the user cannot interact with the control | `boolean` | `false` | +| errorMessage | Error text | `ReactNode` | | +| format | Format of the date when rendered in the input. [Available formats](https://day.js.org/docs/en/display/format) | `string` | | +| hasClear | Shows the icon for clearing control's value | `boolean` | `false` | +| id | The control's `id` attribute | `string` | | +| isDateUnavailable | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | `((date: DateTime) => boolean)` | | +| label | Help text rendered to the left of the input node | `string` | | +| startContent | The user`s node rendered before label and input | `React.ReactNode` | | +| maxValue | The maximum allowed date that a user may select. | `DateTime` | | +| minValue | The minimum allowed date that a user may select. | `DateTime` | | +| onBlur | Fires when the control lost focus. Provides focus event as a callback's argument | `((e: FocusEvent) => void)` | | +| onFocus | Fires when the control gets focus. Provides focus event as a callback's argument | `((e: FocusEvent) => void)` | | +| onKeyDown | Fires when a key is pressed. Provides keyboard event as a callback's argument | `((e: KeyboardEvent) => void)` | | +| onKeyUp | Fires when a key is released. Provides keyboard event as a callback's argument | `((e: KeyboardEvent) => void)` | | +| onUpdate | Fires when the value is changed by the user. Provides new value as an callback's argument | `((value: DateTime \| null) => void` | | +| parseDateFromString | Custom parser function for parsing pasted date strings. If not provided, the default parser will be used. | `((dateStr: string, format: string, timeZone?: string) => DateTime)` | | +| pin | Corner rounding | `string` | `'round-round'` | +| placeholder | Text that appears in the control when it has no value set | `string` | | +| placeholderValue | A placeholder date that controls the default values of each segment when the user first interacts with them. | `DateTime` | `today's date at midnigh` | +| readOnly | Whether the component's value is immutable. | `boolean` | `false` | +| endContent | User`s node rendered after the input node and clear button | `React.ReactNode` | | +| size | The size of the control | `"s"` `"m"` `"l"` `"xl"` | `"m"` | +| style | Sets inline style for the element. | `CSSProperties` | | +| timeZone | Sets the time zone. [Learn more about time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) | `string` | | +| validationState | Validation state | `"invalid"` | | +| value | The value of the control | `DateTime` `null` | | +| view | The view of the control | `"normal"` `"clear"` | `"normal"` | diff --git a/src/components/DateField/__tests__/parseDateFromString.ts b/src/components/DateField/__tests__/parseDateFromString.ts new file mode 100644 index 00000000..cb252e14 --- /dev/null +++ b/src/components/DateField/__tests__/parseDateFromString.ts @@ -0,0 +1,58 @@ +import {dateTime} from '@gravity-ui/date-utils'; +import {act, renderHook} from '@testing-library/react'; + +import {useDateFieldState} from '../hooks/useDateFieldState'; +import {parseDateFromString} from '../utils'; + +jest.mock('../utils', () => ({ + ...jest.requireActual('../utils'), + parseDateFromString: jest.fn(), +})); + +const mockedParseDateFromString = parseDateFromString as jest.MockedFunction< + typeof parseDateFromString +>; + +describe('DateField: parseDateFromString', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedParseDateFromString.mockImplementation((str, format, timeZone) => { + return dateTime({input: str, format, timeZone}); + }); + }); + + it('should call custom parseDateFromString when provided', () => { + const customParser = jest.fn().mockReturnValue(dateTime({input: '2024-01-15T00:00:00Z'})); + + const {result} = renderHook(() => + useDateFieldState({ + format: 'DD.MM.YYYY', + parseDateFromString: customParser, + }), + ); + + act(() => { + result.current.setValueFromString('15.01.2024'); + }); + + expect(customParser).toHaveBeenCalledWith('15.01.2024', 'DD.MM.YYYY', 'default'); + expect(mockedParseDateFromString).not.toHaveBeenCalled(); + }); + + it('should use default parseDateFromString when parseDateFromString is not provided', () => { + const validDate = dateTime({input: '2024-01-15T00:00:00Z'}); + mockedParseDateFromString.mockReturnValue(validDate); + + const {result} = renderHook(() => useDateFieldState({format: 'DD.MM.YYYY'})); + + act(() => { + result.current.setValueFromString('15.01.2024'); + }); + + expect(mockedParseDateFromString).toHaveBeenCalledWith( + '15.01.2024', + 'DD.MM.YYYY', + 'default', + ); + }); +}); diff --git a/src/components/DateField/hooks/useDateFieldState.ts b/src/components/DateField/hooks/useDateFieldState.ts index f47e3e6c..ea71d97d 100644 --- a/src/components/DateField/hooks/useDateFieldState.ts +++ b/src/components/DateField/hooks/useDateFieldState.ts @@ -194,7 +194,8 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState } function setValueFromString(str: string) { - const date = parseDateFromString(str, format, timeZone); + const parseDate = props.parseDateFromString ?? parseDateFromString; + const date = parseDate(str, format, timeZone); if (date.isValid()) { handleUpdateDate(date); return true; diff --git a/src/components/DatePicker/README.md b/src/components/DatePicker/README.md index 57ca10de..251049be 100644 --- a/src/components/DatePicker/README.md +++ b/src/components/DatePicker/README.md @@ -301,6 +301,31 @@ LANDING_BLOCK--> +### Custom Date Parser + +You can provide a custom parser function to handle pasted date strings through the `parseDateFromString` prop. This is useful when you need to support specific date formats or custom parsing logic that differs from the default behavior. + + + +```tsx +import {dateTime} from '@gravity-ui/date-utils'; + +const customParser = (dateStr: string, format: string, timeZone?: string) => { + // Custom parsing logic + // For example, handle DD/MM/YYYY format specifically + if (dateStr.match(/^\d{2}\/\d{2}\/\d{4}$/)) { + const [day, month, year] = dateStr.split('/'); + return dateTime({input: `${year}-${month}-${day}`, timeZone}); + } + // Fallback to default parsing + return dateTime({input: dateStr, format, timeZone}); +}; + +; +``` + + + ## Time zone `timeZone` is the property to set the time zone of the value in the input. [Learn more about time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) @@ -321,36 +346,37 @@ LANDING_BLOCK--> ## Properties -| Name | Description | Type | Default | -| :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-----------------------: | -| aria-describedby | The control's `aria-describedby`. Identifies the element (or elements) that describes the object. attribute | `string` | | -| aria-details | The control's `aria-details`. Identifies the element (or elements) that provide a detailed, extended description for the object. attribute | `string` | | -| aria-label | The control's `aria-label`. Defines a string value that labels the current element. attribute | `string` | | -| aria-labelledby | The control's `aria-labelledby`. Identifies the element (or elements) that labels the current element. attribute | `string` | | -| autoFocus | The control's `autofocus`. Whether the element should receive focus on render. attribute | `boolean` | | -| className | The control's wrapper class name | `string` | | -| [defaultValue](#datepicker) | Sets the initial value for uncontrolled component. | `DateTime` | | -| [disabled](#disabled) | Indicates that the user cannot interact with the control | `boolean` | `false` | -| [errorMessage](#error) | Error text | `ReactNode` | | -| [format](#format) | Format of the date when rendered in the input. [Available formats](https://day.js.org/docs/en/display/format) | `string` | | -| [hasClear](#clear-button) | Shows the icon for clearing control's value | `boolean` | `false` | -| id | The control's `id` attribute | `string` | | -| isDateUnavailable | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | `((date: DateTime) => boolean)` | | -| [label](#label) | Help text rendered to the left of the input node | `string` | | -| [maxValue](#min-and-max-value) | The maximum allowed date that a user may select. | `DateTime` | | -| [minValue](#min-and-max-value) | The minimum allowed date that a user may select. | `DateTime` | | -| onBlur | Fires when the control lost focus. Provides focus event as a callback's argument | `((e: FocusEvent) => void)` | | -| onFocus | Fires when the control gets focus. Provides focus event as a callback's argument | `((e: FocusEvent) => void)` | | -| onKeyDown | Fires when a key is pressed. Provides keyboard event as a callback's argument | `((e: KeyboardEvent) => void)` | | -| onKeyUp | Fires when a key is released. Provides keyboard event as a callback's argument | `((e: KeyboardEvent) => void)` | | -| onUpdate | Fires when the value is changed by the user. Provides new value as an callback's argument | `((value: DateTime \| null) => void` | | -| [pin](#pin) | Corner rounding | `TextInputPin` | `'round-round'` | -| [placeholder](#placeholder) | Text that appears in the control when it has no value set | `string` | | -| placeholderValue | A placeholder date that controls the default values of each segment when the user first interacts with them. | `DateTime` | `today's date at midnigh` | -| [readOnly](#readonly) | Whether the component's value is immutable. | `boolean` | `false` | -| [size](#size) | The size of the control | `"s"` `"m"` `"l"` `"xl"` | `"m"` | -| style | Sets inline style for the element. | `CSSProperties` | | -| [timeZone](#time-zone) | Sets the time zone. [Learn more about time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) | `string` | | -| [validationState](#error) | Validation state | `"invalid"` | | -| [value](#datepicker) | The value of the control | `DateTime` `null` | | -| [view](#view) | The view of the control | `"normal"` `"clear"` | `"normal"` | +| Name | Description | Type | Default | +| :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------: | :-----------------------: | +| aria-describedby | The control's `aria-describedby`. Identifies the element (or elements) that describes the object. attribute | `string` | | +| aria-details | The control's `aria-details`. Identifies the element (or elements) that provide a detailed, extended description for the object. attribute | `string` | | +| aria-label | The control's `aria-label`. Defines a string value that labels the current element. attribute | `string` | | +| aria-labelledby | The control's `aria-labelledby`. Identifies the element (or elements) that labels the current element. attribute | `string` | | +| autoFocus | The control's `autofocus`. Whether the element should receive focus on render. attribute | `boolean` | | +| className | The control's wrapper class name | `string` | | +| [defaultValue](#datepicker) | Sets the initial value for uncontrolled component. | `DateTime` | | +| [disabled](#disabled) | Indicates that the user cannot interact with the control | `boolean` | `false` | +| [errorMessage](#error) | Error text | `ReactNode` | | +| [format](#format) | Format of the date when rendered in the input. [Available formats](https://day.js.org/docs/en/display/format) | `string` | | +| [hasClear](#clear-button) | Shows the icon for clearing control's value | `boolean` | `false` | +| id | The control's `id` attribute | `string` | | +| isDateUnavailable | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | `((date: DateTime) => boolean)` | | +| [label](#label) | Help text rendered to the left of the input node | `string` | | +| [maxValue](#min-and-max-value) | The maximum allowed date that a user may select. | `DateTime` | | +| [minValue](#min-and-max-value) | The minimum allowed date that a user may select. | `DateTime` | | +| onBlur | Fires when the control lost focus. Provides focus event as a callback's argument | `((e: FocusEvent) => void)` | | +| onFocus | Fires when the control gets focus. Provides focus event as a callback's argument | `((e: FocusEvent) => void)` | | +| onKeyDown | Fires when a key is pressed. Provides keyboard event as a callback's argument | `((e: KeyboardEvent) => void)` | | +| onKeyUp | Fires when a key is released. Provides keyboard event as a callback's argument | `((e: KeyboardEvent) => void)` | | +| onUpdate | Fires when the value is changed by the user. Provides new value as an callback's argument | `((value: DateTime \| null) => void` | | +| parseDateFromString | Custom parser function for parsing pasted date strings. If not provided, the default parser will be used. | `((dateStr: string, format: string, timeZone?: string) => DateTime)` | | +| [pin](#pin) | Corner rounding | `TextInputPin` | `'round-round'` | +| [placeholder](#placeholder) | Text that appears in the control when it has no value set | `string` | | +| placeholderValue | A placeholder date that controls the default values of each segment when the user first interacts with them. | `DateTime` | `today's date at midnigh` | +| [readOnly](#readonly) | Whether the component's value is immutable. | `boolean` | `false` | +| [size](#size) | The size of the control | `"s"` `"m"` `"l"` `"xl"` | `"m"` | +| style | Sets inline style for the element. | `CSSProperties` | | +| [timeZone](#time-zone) | Sets the time zone. [Learn more about time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) | `string` | | +| [validationState](#error) | Validation state | `"invalid"` | | +| [value](#datepicker) | The value of the control | `DateTime` `null` | | +| [view](#view) | The view of the control | `"normal"` `"clear"` | `"normal"` | diff --git a/src/components/RangeDateField/__tests__/parseDateFromString.ts b/src/components/RangeDateField/__tests__/parseDateFromString.ts new file mode 100644 index 00000000..380ca868 --- /dev/null +++ b/src/components/RangeDateField/__tests__/parseDateFromString.ts @@ -0,0 +1,74 @@ +import {dateTime} from '@gravity-ui/date-utils'; +import {act, renderHook} from '@testing-library/react'; + +import {parseDateFromString} from '../../DateField/utils'; +import {useRangeDateFieldState} from '../hooks/useRangeDateFieldState'; + +jest.mock('../../DateField/utils', () => ({ + ...jest.requireActual('../../DateField/utils'), + parseDateFromString: jest.fn(), +})); + +const mockedParseDateFromString = parseDateFromString as jest.MockedFunction< + typeof parseDateFromString +>; + +describe('RangeDateField: parseDateFromString', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedParseDateFromString.mockImplementation((str, format, timeZone) => { + return dateTime({input: str, format, timeZone}); + }); + }); + + it('should call custom parseDateFromString when provided for range dates', () => { + const customParser = jest + .fn() + .mockReturnValueOnce(dateTime({input: '2024-01-15T00:00:00Z'})) + .mockReturnValueOnce(dateTime({input: '2024-01-20T00:00:00Z'})); + + const {result} = renderHook(() => + useRangeDateFieldState({ + format: 'DD.MM.YYYY', + parseDateFromString: customParser, + }), + ); + + act(() => { + result.current.setValueFromString('15.01.2024 — 20.01.2024'); + }); + + expect(customParser).toHaveBeenCalledTimes(2); + expect(customParser).toHaveBeenNthCalledWith(1, '15.01.2024', 'DD.MM.YYYY', 'default'); + expect(customParser).toHaveBeenNthCalledWith(2, '20.01.2024', 'DD.MM.YYYY', 'default'); + expect(mockedParseDateFromString).not.toHaveBeenCalled(); + }); + + it('should use default parseDateFromString when parseDateFromString is not provided', () => { + const validStartDate = dateTime({input: '2024-01-15T00:00:00Z'}); + const validEndDate = dateTime({input: '2024-01-20T00:00:00Z'}); + mockedParseDateFromString + .mockReturnValueOnce(validStartDate) + .mockReturnValueOnce(validEndDate); + + const {result} = renderHook(() => useRangeDateFieldState({format: 'DD.MM.YYYY'})); + + act(() => { + result.current.setValueFromString('15.01.2024 — 20.01.2024'); + }); + + expect(mockedParseDateFromString).toHaveBeenCalledTimes(2); + expect(mockedParseDateFromString).toHaveBeenNthCalledWith( + 1, + '15.01.2024', + 'DD.MM.YYYY', + 'default', + ); + expect(mockedParseDateFromString).toHaveBeenNthCalledWith( + 2, + '20.01.2024', + 'DD.MM.YYYY', + 'default', + ); + }); +}); diff --git a/src/components/RangeDateField/hooks/useRangeDateFieldState.ts b/src/components/RangeDateField/hooks/useRangeDateFieldState.ts index 4bc88b92..494cb99e 100644 --- a/src/components/RangeDateField/hooks/useRangeDateFieldState.ts +++ b/src/components/RangeDateField/hooks/useRangeDateFieldState.ts @@ -224,8 +224,9 @@ export function useRangeDateFieldState(props: RangeDateFieldStateOptions): Range function setValueFromString(str: string) { const list = str.split(delimiter); - const start = parseDateFromString(list?.[0], format, timeZone); - const end = parseDateFromString(list?.[1], format, timeZone); + const parseDate = props.parseDateFromString ?? parseDateFromString; + const start = parseDate(list?.[0], format, timeZone); + const end = parseDate(list?.[1], format, timeZone); const range = {start, end}; if (range.start.isValid() && range.end.isValid()) { handleUpdateRange(range); diff --git a/src/components/types/datePicker.ts b/src/components/types/datePicker.ts index 57e6fd9d..eb1afed1 100644 --- a/src/components/types/datePicker.ts +++ b/src/components/types/datePicker.ts @@ -19,6 +19,8 @@ export interface DateFieldBase extends ValueBase, InputB * @default The timezone of the `value` or `defaultValue` or `placeholderValue`, 'default' otherwise. */ timeZone?: string; + /** Custom parser function for parsing pasted date strings. If not provided, the default parser will be used. */ + parseDateFromString?: (dateStr: string, format: string, timeZone?: string) => DateTime; } export interface PopupTriggerProps {