Skip to content

Commit

Permalink
FCT-1196 - DateInput & DateTimeInput bug (#2944)
Browse files Browse the repository at this point in the history
* fix(date-input): manually entered, valid dates should be emitted

* fix(date-time-input): prevent accidental clearing of input when attempting to submit malformatted datetimestring

* fix: manually deleting the whole input value should result in clearing

* fix(date-time-input): allow valid datetime strings to be emitted on blur

* fix(date-time-input): added test for onBlur behavior

* chore: add changeset
  • Loading branch information
misama-ct authored Oct 9, 2024
1 parent a453440 commit 20f74cc
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .changeset/fluffy-phones-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@commercetools-uikit/date-time-input': patch
'@commercetools-uikit/date-input': patch
---

emit manually entered and properly formatted dates & datetimes when the calendar-ui blur event is triggered
36 changes: 35 additions & 1 deletion packages/components/inputs/date-input/src/date-input.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import { screen, render, fireEvent } from '../../../../../test/test-utils';
import {
screen,
render,
fireEvent,
waitFor,
} from '../../../../../test/test-utils';
import DateInput from './date-input';

// This component is used to enable easy testing.
Expand Down Expand Up @@ -221,3 +226,32 @@ describe('date picker keyboard navigation', () => {
});
});
});

it('should only emit valid dates from manually entered datestrings', async () => {
// Render the input with an initial value
renderDateInput({ value: '2020-09-15', 'data-testid': 'onblurtest' });
const htmlInputElement = screen.getByTestId('onblurtest');

// verify it got formatted for display
await waitFor(() =>
expect(htmlInputElement).toHaveDisplayValue('09/15/2020')
);

// enter a valid formatted date
await fireEvent.change(htmlInputElement, { target: { value: '03/28/2024' } });
await fireEvent.blur(htmlInputElement);

// no change is expected
await waitFor(() =>
expect(htmlInputElement).toHaveDisplayValue('03/28/2024')
);

// enter an invalid date
await fireEvent.change(htmlInputElement, { target: { value: '33/28/2024' } });
await fireEvent.blur(htmlInputElement);

// should reset to the most recent valid date
await waitFor(() =>
expect(htmlInputElement).toHaveDisplayValue('03/28/2024')
);
});
26 changes: 25 additions & 1 deletion packages/components/inputs/date-input/src/date-input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import DateInput from './date-input';
import { useEffect, useState } from 'react';

const meta: Meta<typeof DateInput> = {
title: 'Form/Inputs/DateInput',
Expand All @@ -15,9 +16,32 @@ const meta: Meta<typeof DateInput> = {
export default meta;

type Story = StoryObj<typeof DateInput>;

/**
* > **Important:** Make sure the `value` property always reflects the most recent
* > application-/form-state, otherwise the calendar-ui will be out of sync
*/
export const BasicExample: Story = {
render: (args) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [value, setValue] = useState<string>(args.value);
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
setValue(args.value || '');
}, [args.value]);

return (
<div>
<DateInput
{...args}
value={value}
onChange={(e) => setValue(e.target.value || '')}
/>
</div>
);
},
args: {
id: 'date-input',
horizontalConstraint: 7,
value: '',
},
};
14 changes: 14 additions & 0 deletions packages/components/inputs/date-input/src/date-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@ const DateInput = (props: TDateInput) => {
setHighlightedIndex(dayToHighlight);
};

/**
* If the user manually enters a value in the text-input field,
* attempt to parse the value and emit it to the consumer if it's valid and in range.
*/
const onInputBlur = (event: TCustomEvent) => {
const inputValue = event.target.value || '';
const date = parseInputToDate(inputValue, intl.locale);
const inRange = getIsDateInRange(date, props.minValue, props.maxValue);
if (inputValue.length === 0) emit(inputValue);
if (!date || !inRange) return;
emit(date);
};

return (
<Constraints.Horizontal max={props.horizontalConstraint}>
<Downshift
Expand Down Expand Up @@ -347,6 +360,7 @@ const DateInput = (props: TDateInput) => {
}
}
},
onBlur: onInputBlur,
// we only do this for readOnly because the input
// doesn't ignore these events, unlike when its disabled
onClick: props.isReadOnly ? undefined : openMenu,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import { screen, render, fireEvent } from '../../../../../test/test-utils';
import {
screen,
render,
fireEvent,
waitFor,
} from '../../../../../test/test-utils';
import DateTimeInput from './date-time-input';

// This component is used to enable easy testing.
Expand Down Expand Up @@ -189,3 +194,40 @@ describe('date picker defaultDaySelectionTime prop', () => {
expect(screen.getByDisplayValue('11:10 AM')).toBeInTheDocument();
});
});

it('should only emit valid datetimes from manually entered datestrings', async () => {
// Render the input with an initial value
renderDateTimeInput({
defaultDaySelectionTime: '11:10',
value: '2024-10-16T07:30:00.000Z',
'data-testid': 'onblurtest',
});
const htmlInputElement = screen.getByTestId('onblurtest');

// verify it got formatted for display
await waitFor(() =>
expect(htmlInputElement).toHaveDisplayValue('10/16/2024 7:30 AM')
);

// enter a valid formatted datetime
fireEvent.change(htmlInputElement, {
target: { value: '12/17/1985 5:30 AM' },
});
fireEvent.blur(htmlInputElement);

// should stay unchanged
await waitFor(() =>
expect(htmlInputElement).toHaveDisplayValue('12/17/1985 5:30 AM')
);

// enter an invalid formatted datetime
fireEvent.change(htmlInputElement, {
target: { value: '14/17/1985 5:30 AM' },
});
fireEvent.blur(htmlInputElement);

// should reset to the most recent valid value
await waitFor(() =>
expect(htmlInputElement).toHaveDisplayValue('12/17/1985 5:30 AM')
);
});
22 changes: 20 additions & 2 deletions packages/components/inputs/date-time-input/src/date-time-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,15 @@ const preventDownshiftDefault = (event: TPreventDownshiftDefaultEvent) => {
// This keeps the menu open when the user focuses the time input (thereby
// blurring the regular input/toggle button)
const createBlurHandler =
(timeInputRef: RefObject<HTMLInputElement>) =>
(timeInputRef: RefObject<HTMLInputElement>, cb: () => void = () => {}) =>
(event: TCreateBlurHandlerEvent) => {
event.persist();

if (event.relatedTarget === timeInputRef.current) {
preventDownshiftDefault(event);
}

cb();
};

type TCustomEvent = {
Expand Down Expand Up @@ -440,6 +443,10 @@ class DateTimeInput extends Component<
this.props.timeZone
);

// If there is no parsed date, don't clear and submit. Instead, give
// the user a chance to fix the value.
if (!parsedDate) return;

this.emit(parsedDate);

closeMenu();
Expand Down Expand Up @@ -477,7 +484,18 @@ class DateTimeInput extends Component<
}
},
onClick: this.props.isReadOnly ? undefined : openMenu,
onBlur: createBlurHandler(this.timeInputRef),
// validate the input on blur, and emit the value if it's valid
onBlur: createBlurHandler(this.timeInputRef, () => {
const inputValue = this.inputRef.current?.value || '';
const parsedDate = parseInputText(
inputValue,
this.props.intl.locale,
this.props.timeZone
);

if (inputValue.length > 0 && !parsedDate) return;
this.emit(parsedDate);
}),
onChange: (event: TCustomEvent) => {
// keep timeInput and regular input in sync when user
// types into regular input
Expand Down

0 comments on commit 20f74cc

Please sign in to comment.