Skip to content

Commit

Permalink
feat(datepicker): add parseDate prop, improve default date parsing (#…
Browse files Browse the repository at this point in the history
…15918)

* feat(datepicker): improve date parsing for default format, add parseDate as prop

* chore(datepicker): fix typescript warnings

* fix(datepicker): improve parseDate logic and comments

* test(datepicker): cover parseDate functionality

* test(datepicker): cover totally invalid date
  • Loading branch information
tay1orjones committed Mar 13, 2024
1 parent 77abccb commit 4890639
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Expand Up @@ -2651,6 +2651,9 @@ Map {
"onOpen": Object {
"type": "func",
},
"parseDate": Object {
"type": "func",
},
"readOnly": Object {
"args": Array [
Array [
Expand Down
121 changes: 121 additions & 0 deletions packages/react/src/components/DatePicker/DatePicker-test.js
Expand Up @@ -178,6 +178,127 @@ describe('DatePicker', () => {

expect(screen.getByRole('button')).toHaveClass(`${prefix}--slug__button`);
});

it('should respect parseDate prop', async () => {
const parseDate = jest.fn();
parseDate.mockReturnValueOnce(new Date('1989/01/20'));
render(
<DatePicker
onChange={() => {}}
datePickerType="single"
parseDate={parseDate}>
<DatePickerInput
id="date-picker-input-id-start"
placeholder="mm/dd/yyyy"
labelText="Date Picker label"
data-testid="input-value"
/>
</DatePicker>
);
await userEvent.type(
screen.getByLabelText('Date Picker label'),
'01/20/1989{enter}'
);
expect(parseDate).toHaveBeenCalled();
});

it('invalid date month/day is correctly parsed when using the default format', async () => {
render(
<DatePicker onChange={() => {}} datePickerType="single">
<DatePickerInput
id="date-picker-input-id-start"
placeholder="mm/dd/yyyy"
labelText="Date Picker label"
data-testid="input-value"
/>
</DatePicker>
);

expect(screen.getByLabelText('Date Picker label')).toHaveValue('');

// Invalid month
await userEvent.type(
screen.getByLabelText('Date Picker label'),
'99/20/1989{enter}'
);
expect(screen.getByLabelText('Date Picker label')).toHaveValue(
'01/20/1989'
);
await userEvent.clear(screen.getByLabelText('Date Picker label'));

// Invalid day
await userEvent.type(
screen.getByLabelText('Date Picker label'),
'01/99/1989{enter}'
);
expect(screen.getByLabelText('Date Picker label')).toHaveValue(
'01/01/1989'
);
await userEvent.clear(screen.getByLabelText('Date Picker label'));

// Invalid month and day
await userEvent.type(
screen.getByLabelText('Date Picker label'),
'99/99/1989{enter}'
);
expect(screen.getByLabelText('Date Picker label')).toHaveValue(
'01/01/1989'
);
await userEvent.clear(screen.getByLabelText('Date Picker label'));

expect(screen.getByLabelText('Date Picker label')).toHaveValue('');
});

it('invalid date month/day is parsed by flatpickr when using a custom format', async () => {
render(
<DatePicker
onChange={() => {}}
datePickerType="single"
dateFormat="d/m/Y">
<DatePickerInput
id="date-picker-input-id-start"
placeholder="mm/dd/yyyy"
labelText="Date Picker label"
data-testid="input-value"
/>
</DatePicker>
);

expect(screen.getByLabelText('Date Picker label')).toHaveValue('');

await userEvent.type(
screen.getByLabelText('Date Picker label'),
'34/34/3434{enter}'
);
// More on how this value is calculated by flatpickr:
// https://github.com/carbon-design-system/carbon/issues/15432#issuecomment-1967447677
expect(screen.getByLabelText('Date Picker label')).toHaveValue(
'03/10/3436'
);
await userEvent.clear(screen.getByLabelText('Date Picker label'));
});

it('the input is cleared when given a completely invalid date', async () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
render(
<DatePicker onChange={() => {}} datePickerType="single">
<DatePickerInput
id="date-picker-input-id-start"
placeholder="mm/dd/yyyy"
labelText="Date Picker label"
data-testid="input-value"
/>
</DatePicker>
);

await userEvent.type(
screen.getByLabelText('Date Picker label'),
'a1/0a/a999{enter}'
);
expect(warn).toHaveBeenCalled();
expect(screen.getByLabelText('Date Picker label')).toHaveValue('');
warn.mockRestore();
});
});

describe('Simple date picker', () => {
Expand Down
51 changes: 51 additions & 0 deletions packages/react/src/components/DatePicker/DatePicker.tsx
Expand Up @@ -364,6 +364,11 @@ interface DatePickerProps {
*/
onOpen?: flatpickr.Options.Hook;

/**
* flatpickr prop passthrough. Controls how dates are parsed.
*/
parseDate?: (date: string) => Date | false;

/**
* whether the DatePicker is to be readOnly
* if boolean applies to all inputs
Expand Down Expand Up @@ -419,6 +424,7 @@ const DatePicker = React.forwardRef(function DatePicker(
readOnly = false,
short = false,
value,
parseDate: parseDateProp,
...rest
}: DatePickerProps,
ref: ForwardedRef<HTMLDivElement>
Expand Down Expand Up @@ -557,6 +563,45 @@ const DatePicker = React.forwardRef(function DatePicker(
localeData = l10n[locale];
}

/**
* parseDate is called before the date is actually set.
* It attempts to parse the input value and return a valid date string.
* Flatpickr's default parser results in odd dates when given invalid
* values, so instead here we normalize the month/day to `1` if given
* a value outside the acceptable range.
*/
let parseDate;
if (!parseDateProp && dateFormat === 'm/d/Y') {
// This function only supports the default dateFormat.
parseDate = (date) => {
// Month must be 1-12. If outside these bounds, `1` should be used.
const month =
date.split('/')[0] <= 12 && date.split('/')[0] > 0
? parseInt(date.split('/')[0])
: 1;
const year = parseInt(date.split('/')[2]);

if (month && year) {
// The month and year must be provided to be able to determine
// the number of days in the month.
const daysInMonth = new Date(year, month, 0).getDate();
// If the day does not fall within the days in the month, `1` should be used.
const day =
date.split('/')[1] <= daysInMonth && date.split('/')[1] > 0
? parseInt(date.split('/')[1])
: 1;

return new Date(`${year}/${month}/${day}`);
} else {
// With no month and year, we cannot calculate anything.
// Returning false gives flatpickr an invalid date, which will clear the input
return false;
}
};
} else if (parseDateProp) {
parseDate = parseDateProp;
}

const { current: start } = startInputField;
const { current: end } = endInputField;
const flatpickerconfig: any = {
Expand All @@ -571,6 +616,7 @@ const DatePicker = React.forwardRef(function DatePicker(
[enableOrDisable]: enableOrDisableArr,
minDate: minDate,
maxDate: maxDate,
parseDate: parseDate,
plugins: [
datePickerType === 'range'
? carbonFlatpickrRangePlugin({
Expand Down Expand Up @@ -998,6 +1044,11 @@ DatePicker.propTypes = {
*/
onOpen: PropTypes.func,

/**
* flatpickr prop passthrough. Controls how dates are parsed.
*/
parseDate: PropTypes.func,

/**
* whether the DatePicker is to be readOnly
* if boolean applies to all inputs
Expand Down

0 comments on commit 4890639

Please sign in to comment.