diff --git a/.changeset/eight-beds-smash.md b/.changeset/eight-beds-smash.md new file mode 100644 index 0000000000..d53de18815 --- /dev/null +++ b/.changeset/eight-beds-smash.md @@ -0,0 +1,72 @@ +--- +'@leafygreen-ui/date-picker': major +--- + +Changes the behavior of segments. + +## Backspace + +### New Behavior +- Pressing `backspace` will always clear the segment and keep the focus on that segment +- Pressing `backspace` twice will clear the segment and move the focus to the previous segment + +### Old Behavior +- Pressing `backspace` deletes characters before the cursor one by one +- After all characters are deleted, the focus moves to the previous segment + +## Space + +### New Behavior +- Pressing `space` will always clear the segment and keep the focus on that segment + +### Old Behavior +- Pressing `space` does not change the current value + +## Clicking + +### New Behavior +#### When initially clicking on a segment with a value, the segment will select the value: +- Typing a digit will clear the segment and populate the segment with that new value. +- Pressing the `backspace` key will clear the segment +- Pressing the `backspace` key twice will clear the segment and move the focus to the previous segment +- Pressing the `space` key will clear the segment + +#### When initially clicking on a segment without a value, the segment will show a cursor: +- Typing a digit will start to populate the segment +- Pressing the `backspace` key will move the focus to the previous segment +- Pressing the `space` key will keep the focus on that segment + +#### When a segment is already selected, clicking on the segment a second time will deselect the segment, and a cursor will appear: +- Typing a digit will clear the segment and populate the segment with that new value. +- Pressing the `backspace` key will clear the segment +- Pressing the `backspace` key twice will clear the segment and move the focus to the previous segment +- Pressing the `space` key will clear the segment + +### Old Behavior +- Clicking on a segment will make the cursor appear in the clicked spot. +- If the segment is full, typing will not change the value +- If the segment is not full, typing will not add a new character after the cursor + +## Tabbing and Left/Right arrows + +### New behavior +#### When when using the arrow keys or tabbing into a segment with a value, the segment will select the value: +- Typing a digit will reset the segment and populate the segment with that new value. +- Pressing the `backspace` key will clear the segment +- Pressing the `backspace` key twice will clear the segment and move the focus to the previous segment +- Pressing the `space` key will clear the segment + +#### When using the arrow keys or tabbing into a segment without a value, the segment will show a cursor: +- Typing a digit will start to populate the segment +- Pressing the `backspace` key will move the focus to the previous segment +- Pressing the `space` key will keep the focus on that segment + +## Tabbing +### Old Behavior +- Tabbing into a segment will select the value, but pressing `space` does not reset the value + +## Left/Right arrows +### Old Behavior +- When in a segment, `left` or `right` arrow keys navigates through each character instead of selecting the value. +- If the segment is full, typing does not update the value +- If the segment is not full, typing will add a new character in that spot \ No newline at end of file diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index 908aebba13..e53d15a64c 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -112,7 +112,7 @@ export const LiveExample: StoryFn = props => { onDateChange={v => { // eslint-disable-next-line no-console console.log('Storybook: onDateChange', { - value: v!.toUTCString(), + value: v?.toUTCString(), 'value with local browser timezone': v, }); setValue(v); @@ -123,6 +123,10 @@ export const LiveExample: StoryFn = props => { 'date with local browser timezone': date, }) } + onChange={e => + // eslint-disable-next-line no-console + console.log('Storybook: onChange🚨', { value: e.target.value }) + } />
Current value diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 9c49d7485f..d0903ce78c 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1420,19 +1420,31 @@ describe('packages/date-picker', () => { }); describe('Backspace key', () => { - test('deletes any value in the input', () => { + test('fires segment change handler after typing a value', () => { + const onChange = jest.fn(); + const { dayInput } = renderDatePicker({ onChange }); + userEvent.type(dayInput, '26{backspace}'); + expect(onChange).toHaveBeenCalledWith(eventContainingTargetValue('')); + }); + + test('resets the input', () => { const { dayInput } = renderDatePicker(); userEvent.type(dayInput, '26{backspace}'); - expect(dayInput.value).toBe('2'); - userEvent.tab(); - expect(dayInput.value).toBe('02'); + expect(dayInput.value).toBe(''); }); - test('deletes the whole value on multiple presses', () => { + test('keeps the focus in the current input', () => { const { monthInput } = renderDatePicker(); userEvent.type(monthInput, '11'); + userEvent.type(monthInput, '{backspace}'); + expect(monthInput).toHaveFocus(); + }); + + test('focuses the previous segment after pressing backspace twice', () => { + const { monthInput, yearInput } = renderDatePicker(); + userEvent.type(monthInput, '11'); userEvent.type(monthInput, '{backspace}{backspace}'); - expect(monthInput.value).toBe(''); + expect(yearInput).toHaveFocus(); }); test('focuses the previous segment if current segment is empty', () => { @@ -1455,39 +1467,12 @@ describe('packages/date-picker', () => { expect(yearInput).toHaveFocus(); }); - test('moves the cursor when the segment has a value', () => { - const { monthInput } = renderDatePicker({ - value: testToday, - }); - userEvent.click(monthInput); - userEvent.keyboard('{arrowleft}'); - expect(monthInput).toHaveFocus(); - }); - - test('moves the cursor when the value starts with 0', () => { - const { monthInput } = renderDatePicker({}); - userEvent.type(monthInput, '04{arrowleft}{arrowleft}'); - expect(monthInput).toHaveFocus(); - }); - - test('moves the cursor when the value is 0', () => { - const { monthInput } = renderDatePicker({}); - userEvent.type(monthInput, '0{arrowleft}'); - expect(monthInput).toHaveFocus(); - }); - - test('moves the cursor to the previous segment when the value is 0', () => { - const { yearInput, monthInput } = renderDatePicker({}); - userEvent.type(monthInput, '0{arrowleft}{arrowleft}'); - expect(yearInput).toHaveFocus(); - }); - - test('focuses the previous segment if the cursor is at the start of the input text', () => { + test('focuses the previous segment when the segment has a value', () => { const { yearInput, monthInput } = renderDatePicker({ value: testToday, }); userEvent.click(monthInput); - userEvent.keyboard('{arrowleft}{arrowleft}{arrowleft}'); + userEvent.keyboard('{arrowleft}'); expect(yearInput).toHaveFocus(); }); }); @@ -1500,7 +1485,7 @@ describe('packages/date-picker', () => { expect(monthInput).toHaveFocus(); }); - test('focuses the next segment if the cursor is at the start of the input text', () => { + test('focuses the next segment when the segment has a value', () => { const { yearInput, monthInput } = renderDatePicker({ value: testToday, }); @@ -1509,13 +1494,10 @@ describe('packages/date-picker', () => { expect(monthInput).toHaveFocus(); }); - test('moves the cursor when the segment has a value', () => { - const { yearInput } = renderDatePicker({ - value: testToday, - }); - userEvent.click(yearInput); - userEvent.keyboard('{arrowleft}{arrowright}'); - expect(yearInput).toHaveFocus(); + test('focuses the next segment when the value starts with 0', () => { + const { monthInput, dayInput } = renderDatePicker({}); + userEvent.type(monthInput, '0{arrowright}'); + expect(dayInput).toHaveFocus(); }); }); @@ -2559,29 +2541,6 @@ describe('packages/date-picker', () => { describe('typing space', () => { describe('single space', () => { describe('does not fire a segment value change', () => { - test('when the value prop is set', () => { - const onChange = jest.fn(); - - const { yearInput } = renderDatePicker({ - onChange, - value: newUTC(2023, Month.December, 25), - }); - userEvent.type(yearInput, '{space}'); - expect(onChange).not.toHaveBeenCalled(); - }); - - test('when typing another digit', () => { - const onChange = jest.fn(); - - const { yearInput } = renderDatePicker({ - onChange, - }); - userEvent.type(yearInput, '{space}2'); - expect(onChange).not.toHaveBeenCalledWith( - expect.objectContaining({ value: ' 2' }), - ); - }); - test('when there is no value', () => { const onChange = jest.fn(); @@ -2607,11 +2566,12 @@ describe('packages/date-picker', () => { test('at the end of a value', () => { const onChange = jest.fn(); - const { yearInput } = renderDatePicker({ + const { yearInput, monthInput } = renderDatePicker({ onChange, }); userEvent.type(yearInput, '2023{space}'); expect(yearInput.value).toBe('2023'); + expect(monthInput).toHaveFocus(); }); test('between a value', () => { @@ -2621,7 +2581,7 @@ describe('packages/date-picker', () => { onChange, }); userEvent.type(yearInput, '202{space}3'); - expect(yearInput.value).toBe('2023'); + expect(yearInput.value).toBe('3'); }); test('in multiple spots', () => { @@ -2631,7 +2591,7 @@ describe('packages/date-picker', () => { onChange, }); userEvent.type(yearInput, '2{space}0{space}2{space}3{space}'); - expect(yearInput.value).toBe('2023'); + expect(yearInput.value).toBe(''); }); }); @@ -2645,17 +2605,18 @@ describe('packages/date-picker', () => { describe('double space', () => { describe('does not fire a segment value change', () => { - test('when the value prop is set', () => { + test('when there is no value', () => { const onChange = jest.fn(); const { yearInput } = renderDatePicker({ onChange, - value: newUTC(2023, Month.December, 25), }); userEvent.type(yearInput, '{space}{space}'); expect(onChange).not.toHaveBeenCalled(); }); + }); + describe('fires a segment value change', () => { test('when typing another digit', () => { const onChange = jest.fn(); @@ -2663,54 +2624,57 @@ describe('packages/date-picker', () => { onChange, }); userEvent.type(yearInput, '{space}{space}2'); - expect(onChange).not.toHaveBeenCalledWith( - expect.objectContaining({ value: ' 2' }), + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue('2'), ); }); - test('when there is no value', () => { + test('when the value prop is set', () => { const onChange = jest.fn(); const { yearInput } = renderDatePicker({ onChange, + value: newUTC(2023, Month.December, 25), }); userEvent.type(yearInput, '{space}{space}'); - expect(onChange).not.toHaveBeenCalled(); + expect(yearInput.value).toBe(''); + expect(onChange).toHaveBeenCalled(); }); + }); - test('in multiple spots', () => { + describe('renders the correct value when the space is', () => { + test('at the start of a value', () => { const onChange = jest.fn(); const { yearInput } = renderDatePicker({ onChange, }); - userEvent.type( - yearInput, - '2{space}{space}0{space}{space}2{space}{space}3{space}{space}', - ); + userEvent.type(yearInput, '{space}{space}2023'); expect(yearInput.value).toBe('2023'); }); - }); - describe('renders the correct value when the space is', () => { - test('at the start of a value', () => { + test('at the end of a value', () => { const onChange = jest.fn(); - const { yearInput } = renderDatePicker({ + const { yearInput, monthInput } = renderDatePicker({ onChange, }); - userEvent.type(yearInput, '{space}{space}2023'); + userEvent.type(yearInput, '2023{space}{space}'); expect(yearInput.value).toBe('2023'); + expect(monthInput).toHaveFocus(); }); - test('at the end of a value', () => { + test('in multiple spots', () => { const onChange = jest.fn(); const { yearInput } = renderDatePicker({ onChange, }); - userEvent.type(yearInput, '2023{space}{space}'); - expect(yearInput.value).toBe('2023'); + userEvent.type( + yearInput, + '2{space}{space}0{space}{space}2{space}{space}3{space}{space}', + ); + expect(yearInput.value).toBe(''); }); test('between a value', () => { @@ -2720,7 +2684,7 @@ describe('packages/date-picker', () => { onChange, }); userEvent.type(yearInput, '202{space}{space}3'); - expect(yearInput.value).toBe('2023'); + expect(yearInput.value).toBe('3'); }); }); }); @@ -2998,7 +2962,6 @@ describe('packages/date-picker', () => { userEvent.type(monthInput, '7'); userEvent.type(dayInput, '4'); - yearInput.setSelectionRange(0, 4); userEvent.type(yearInput, '{backspace}'); expect(yearInput).toHaveValue(''); }); @@ -3029,7 +2992,6 @@ describe('packages/date-picker', () => { userEvent.type(monthInput, '7'); userEvent.type(dayInput, '4'); - yearInput.setSelectionRange(0, 4); userEvent.type(yearInput, '{backspace}'); userEvent.type(yearInput, '2'); expect(yearInput).toHaveValue('2'); @@ -3041,27 +3003,35 @@ describe('packages/date-picker', () => { userEvent.type(monthInput, '7'); userEvent.type(dayInput, '4'); - userEvent.type(yearInput, '{backspace}{backspace}'); - expect(yearInput).toHaveValue('20'); + userEvent.type(yearInput, '{backspace}'); + expect(yearInput).toHaveValue(''); }); }); describe('typing new characters', () => { - test('even if the resulting value is valid, keeps the input as-is', async () => { + test('updates the value', async () => { const { monthInput } = renderDatePicker({}); userEvent.type(monthInput, '1'); userEvent.tab(); await waitFor(() => expect(monthInput).toHaveValue('01')); userEvent.type(monthInput, '2'); - await waitFor(() => expect(monthInput).toHaveValue('01')); + await waitFor(() => expect(monthInput).toHaveValue('02')); }); - test('if the resulting value is not valid, keeps the input as-is', async () => { + test('if the resulting value is incomplete and invalid, clears the input', async () => { const { monthInput } = renderDatePicker({}); - userEvent.type(monthInput, '6'); - await waitFor(() => expect(monthInput).toHaveValue('06')); - userEvent.type(monthInput, '9'); - await waitFor(() => expect(monthInput).toHaveValue('06')); + userEvent.type(monthInput, '0'); + userEvent.tab(); + await waitFor(() => expect(monthInput).toHaveValue('')); + }); + + test('if the resulting value is invalid, formats the first digit and the second digit is inputted into the next input', async () => { + const { monthInput, dayInput } = renderDatePicker({}); + userEvent.type(monthInput, '32'); + await waitFor(() => { + expect(monthInput).toHaveValue('03'); + expect(dayInput).toHaveValue('2'); + }); }); }); }); @@ -3551,15 +3521,13 @@ describe('packages/date-picker', () => { userEvent.tab(); errorElement = queryByTestId('lg-form_field-error_message'); expect(errorElement).toHaveTextContent( - '2020-02-30 is not a valid date', + '2020-02- is not a valid date', ); userEvent.type(dayInput, '{backspace}{backspace}'); userEvent.tab(); errorElement = queryByTestId('lg-form_field-error_message'); - expect(errorElement).toHaveTextContent( - '2020-02- is not a valid date', - ); + expect(errorElement).toHaveTextContent('2020-- is not a valid date'); }); test('Clearing the input after an invalid date error message is displayed removes the message', () => { diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index e1bb117b43..7954a8df4f 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -27,6 +27,9 @@ import { getSegmentToFocus } from '../utils/getSegmentToFocus'; import { DatePickerInputProps } from './DatePickerInput.types'; +/** + * @internal + */ export const DatePickerInput = forwardRef( ( { @@ -84,6 +87,7 @@ export const DatePickerInput = forwardRef( }); segmentToFocus?.focus(); + segmentToFocus?.select(); } }; @@ -108,40 +112,38 @@ export const DatePickerInput = forwardRef( const isSegmentEmpty = !target.value; - const { selectionStart, selectionEnd } = target; - switch (key) { case keyMap.ArrowLeft: { + // Without this, the input ignores `.select()` + e.preventDefault(); // if input is empty, - // or the cursor is at the beginning of the input - // set focus to prev. input (if it exists) - if (selectionStart === 0) { - const segmentToFocus = getRelativeSegmentRef('prev', { - segment: target, - formatParts, - segmentRefs, - }); - - segmentToFocus?.current?.focus(); - } + // set focus to prev input (if it exists) + const segmentToFocus = getRelativeSegmentRef('prev', { + segment: target, + formatParts, + segmentRefs, + }); + + segmentToFocus?.current?.focus(); + segmentToFocus?.current?.select(); // otherwise, use default behavior break; } case keyMap.ArrowRight: { + // Without this, the input ignores `.select()` + e.preventDefault(); // if input is empty, - // or the cursor is at the end of the input // set focus to next. input (if it exists) - if (selectionEnd === target.value.length) { - const segmentToFocus = getRelativeSegmentRef('next', { - segment: target, - formatParts, - segmentRefs, - }); - - segmentToFocus?.current?.focus(); - } + const segmentToFocus = getRelativeSegmentRef('next', { + segment: target, + formatParts, + segmentRefs, + }); + + segmentToFocus?.current?.focus(); + segmentToFocus?.current?.select(); // otherwise, use default behavior break; @@ -157,12 +159,14 @@ export const DatePickerInput = forwardRef( if (isSegmentEmpty) { // prevent the backspace in the previous segment e.preventDefault(); + const segmentToFocus = getRelativeSegmentRef('prev', { segment: target, formatParts, segmentRefs, }); segmentToFocus?.current?.focus(); + segmentToFocus?.current?.select(); } break; } @@ -205,7 +209,7 @@ export const DatePickerInput = forwardRef( */ const handleSegmentChange: DateInputSegmentChangeEventHandler = segmentChangeEvent => { - const { segment } = segmentChangeEvent; + const { segment, value } = segmentChangeEvent; /** * Fire a simulated `change` event @@ -213,6 +217,9 @@ export const DatePickerInput = forwardRef( const target = segmentRefs[segment].current; if (target) { + // At this point, the target stored in segmentRefs has a stale value. + // To fix this we update the value of the target with the up-to-date value from `segmentChangeEvent`. + target.value = value; const changeEvent = new Event('change'); const reactEvent = createSyntheticEvent< ChangeEvent diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts index cb5efb645d..aafbed88f6 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts @@ -24,7 +24,7 @@ export const getSegmentToFocus = ({ target, formatParts, segmentRefs, -}: GetSegmentToFocusProps): HTMLElement | undefined | null => { +}: GetSegmentToFocusProps): HTMLInputElement | undefined | null => { if ( isUndefined(target) || isUndefined(formatParts) || diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx index 158a55da63..13e6e8d751 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx @@ -214,18 +214,18 @@ describe('packages/date-picker/shared/date-input-box', () => { expect(dayInput.value).toBe('02'); }); - test('backspace deletes characters', () => { + test('backspace resets the input', () => { const { dayInput, yearInput } = renderDateInputBox( { value: null }, testContext, ); userEvent.type(dayInput, '21'); userEvent.type(dayInput, '{backspace}'); - expect(dayInput.value).toBe('2'); + expect(dayInput.value).toBe(''); userEvent.type(yearInput, '1993'); userEvent.type(yearInput, '{backspace}'); - expect(yearInput.value).toBe('199'); + expect(yearInput.value).toBe(''); }); test('segment change handler is called when typing into a segment', () => { @@ -255,17 +255,19 @@ describe('packages/date-picker/shared/date-input-box', () => { userEvent.type(dayInput, '21'); userEvent.type(dayInput, '{backspace}'); expect(onSegmentChange).toHaveBeenCalledWith( - expect.objectContaining({ value: '2' }), + expect.objectContaining({ value: '' }), ); }); - test('value setter is not called when deleting from a single segment', () => { + test('value setter is called when pressing backspace in a single segment', () => { const setValue = jest.fn(); const { dayInput } = renderDateInputBox({ setValue }, testContext); userEvent.type(dayInput, '21'); userEvent.type(dayInput, '{backspace}'); - expect(setValue).not.toHaveBeenCalled(); + expect(setValue).toHaveBeenCalledWith( + expect.objectContaining({ value: null }), + ); }); }); @@ -313,11 +315,11 @@ describe('packages/date-picker/shared/date-input-box', () => { }, testContext, ); - userEvent.type(dayInput, '{backspace}5'); + userEvent.type(dayInput, '{backspace}'); expect(setValue).toHaveBeenCalledWith( - expect.objectContaining(newUTC(1993, Month.December, 25)), + expect.objectContaining(new Date('invalid')), ); - expect(dayInput).toHaveValue('25'); + expect(dayInput).toHaveValue(''); }); test('value setter is _not_ called when new input is ambiguous', () => { @@ -330,8 +332,8 @@ describe('packages/date-picker/shared/date-input-box', () => { testContext, ); userEvent.type(dayInput, '{backspace}'); - expect(setValue).not.toHaveBeenCalled(); - expect(dayInput).toHaveValue('2'); + expect(setValue).toHaveBeenCalled(); + expect(dayInput).toHaveValue(''); }); test('value setter is called when the input is cleared', () => { diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx index adcd4791c9..f0851e022b 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx @@ -147,6 +147,7 @@ export const DateInputBox = React.forwardRef( if (nextSegmentName) { const nextSegmentRef = segmentRefs[nextSegmentName]; nextSegmentRef?.current?.focus(); + nextSegmentRef?.current?.select(); } } diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx index 3997c91569..8f56fb113f 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx @@ -204,35 +204,34 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('does not allow additional characters that create an invalid value', () => { + test('resets the value when the value is complete', () => { const { input } = renderSegment({ value: '26', onChange: onChangeHandler, }); - userEvent.type(input, '6'); - expect(onChangeHandler).not.toHaveBeenCalled(); + userEvent.type(input, '4'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ value: '4' }), + ); }); }); }); describe('Keyboard', () => { describe('Backspace', () => { - test('deletes value in the input', () => { + test('does not call the onChangeHandler when the value is initially empty', () => { const { input } = renderSegment({ - value: '26', onChange: onChangeHandler, }); userEvent.type(input, '{backspace}'); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ value: '2' }), - ); + expect(onChangeHandler).not.toHaveBeenCalled(); }); - test('fully clears the input', () => { + test('clears the input when there is a value', () => { const { input } = renderSegment({ - value: '2', + value: '26', onChange: onChangeHandler, }); @@ -691,7 +690,8 @@ describe('packages/date-picker/shared/date-input-segment', () => { userEvent.type(input, '{space}'); expect(onChangeHandler).not.toHaveBeenCalled(); }); - + }); + describe('calls the onChangeHandler', () => { test('when the input has a value', () => { const { input } = renderSegment({ onChange: onChangeHandler, @@ -699,7 +699,11 @@ describe('packages/date-picker/shared/date-input-segment', () => { }); userEvent.type(input, '{space}'); - expect(onChangeHandler).not.toHaveBeenCalled(); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: '', + }), + ); }); }); }); @@ -714,7 +718,9 @@ describe('packages/date-picker/shared/date-input-segment', () => { userEvent.type(input, '{space}{space}'); expect(onChangeHandler).not.toHaveBeenCalled(); }); + }); + describe('calls the onChangeHandler', () => { test('when the input has a value', () => { const { input } = renderSegment({ onChange: onChangeHandler, @@ -722,7 +728,11 @@ describe('packages/date-picker/shared/date-input-segment', () => { }); userEvent.type(input, '{space}{space}'); - expect(onChangeHandler).not.toHaveBeenCalled(); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ + value: '', + }), + ); }); }); }); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx index c722fe5515..df30f5303f 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx @@ -28,10 +28,13 @@ import { DateInputSegmentProps } from './DateInputSegment.types'; import { getNewSegmentValueFromInputValue } from './utils'; /** + * Controlled component + * * Renders a single date segment with the * appropriate character padding/truncation. * - * Only fires a change handler when the input is blurred + * + * @internal */ export const DateInputSegment = React.forwardRef< HTMLInputElement, @@ -95,10 +98,21 @@ export const DateInputSegment = React.forwardRef< /** Handle keydown presses that don't natively fire a change event */ const handleKeyDown: KeyboardEventHandler = e => { - const { key } = e as React.KeyboardEvent & { + const { key, target } = e as React.KeyboardEvent & { target: HTMLInputElement; }; + // A key press can be an `arrow`, `enter`, `space`, etc so we check for number presses + // We also check for `space` because Number(' ') returns true + const isNumber = Number(key) && key !== keyMap.Space; + + if (isNumber) { + // if the value length is equal to the charsPerSegment, reset the input + if (target.value.length === charsPerSegment[segment]) { + target.value = ''; + } + } + switch (key) { case keyMap.ArrowUp: case keyMap.ArrowDown: { @@ -122,23 +136,38 @@ export const DateInputSegment = React.forwardRef< break; } + // On backspace the value is reset case keyMap.Backspace: { - const numChars = value.length; + // Don't fire change event if the input is initially empty + if (value) { + // Prevent the onKeyDown handler inside `DatePickerInput` from firing. Because we reset the value on backspace, that will trigger the previous segment to focus but we want the focus to remain inside the current segment. + e.stopPropagation(); - // If we've cleared the input with backspace, - // fire the custom change event - if (numChars === 1) { + /** Fire a custom change event when the backspace key is pressed */ onChange({ segment, value: '', meta: { key }, }); } + break; } + // On space the value is reset case keyMap.Space: { e.preventDefault(); + + // Don't fire change event if the input is initially empty + if (value) { + /** Fire a custom change event when the space key is pressed */ + onChange({ + segment, + value: '', + meta: { key }, + }); + } + break; } @@ -161,7 +190,6 @@ export const DateInputSegment = React.forwardRef< ref={inputRef} type="text" pattern={pattern} - maxLength={charsPerSegment[segment]} role="spinbutton" value={value} min={min}