Skip to content

Commit

Permalink
fix(DateInput,DateRangeInput): fix the issue of incorrect cursor posi…
Browse files Browse the repository at this point in the history
…tion in the input box (#3785)

* fix(DateInput,DateRangeInput): fix the issue of incorrect cursor position in the input box

* test: add tests for input continuously

* test: add tests for input continuously

* test: add tests for input continuously
  • Loading branch information
simonguo committed May 9, 2024
1 parent 32a0387 commit d606944
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 93 deletions.
4 changes: 3 additions & 1 deletion src/DateInput/DateField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ export const useDateField = (format: string, localize: Locale['localize'], date?
value = padNumber(value, pattern.length);
}

str = str.replace(pattern, value);
if (typeof value !== 'undefined') {
str = str.replace(pattern, value);
}
}
});

Expand Down
48 changes: 18 additions & 30 deletions src/DateInput/DateInput.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import React, { useState, useRef, useMemo } from 'react';
import React, { useRef, useMemo } from 'react';
import PropTypes from 'prop-types';
import Input, { InputProps } from '../Input';
import { mergeRefs, useCustom, useControlled, useEventCallback } from '../utils';
import { FormControlBaseProps } from '../@types/common';
import {
getInputSelectedState,
validateDateTime,
isFieldFullValue,
useInputSelection
} from './utils';

import useDateInputState from './useDateInputState';
import useKeyboardInputEvent from './useKeyboardInputEvent';
import useIsFocused from './useIsFocused';
import { getInputSelectedState, validateDateTime, useInputSelection } from './utils';
import useDateInputState from './hooks/useDateInputState';
import useKeyboardInputEvent from './hooks/useKeyboardInputEvent';
import useIsFocused from './hooks/useIsFocused';
import useFieldCursor from './hooks/useFieldCursor';
import useSelectedState from './hooks/useSelectedState';

export interface DateInputProps
extends Omit<InputProps, 'value' | 'onChange' | 'defaultValue'>,
Expand Down Expand Up @@ -51,15 +47,7 @@ const DateInput = React.forwardRef((props: DateInputProps, ref) => {

const inputRef = useRef<HTMLInputElement>();

const [selectedState, setSelectedState] = useState<{
selectedPattern: string;
selectionStart: number;
selectionEnd: number;
}>({
selectedPattern: 'y',
selectionStart: 0,
selectionEnd: 0
});
const { selectedState, setSelectedState } = useSelectedState();

const { locale } = useCustom('Calendar');

Expand All @@ -73,6 +61,8 @@ const DateInput = React.forwardRef((props: DateInputProps, ref) => {
isControlledDate: isControlled
});

const { isMoveCursor, increment, reset } = useFieldCursor(formatStr, valueProp);

const dateString = toDateString();
const keyPressOptions = useMemo(
() => ({
Expand Down Expand Up @@ -101,8 +91,9 @@ const DateInput = React.forwardRef((props: DateInputProps, ref) => {

const state = getInputSelectedState({ ...keyPressOptions, input, direction });

setSelectionRange(state.selectionStart, state.selectionEnd);
setSelectedState(state);
setSelectionRange(state.selectionStart, state.selectionEnd);
reset();
}
);

Expand All @@ -128,6 +119,8 @@ const DateInput = React.forwardRef((props: DateInputProps, ref) => {
return;
}

increment();

const field = getDateField(pattern);
const value = parseInt(key, 10);
const padValue = parseInt(`${field.value || ''}${key}`, 10);
Expand All @@ -139,11 +132,6 @@ const DateInput = React.forwardRef((props: DateInputProps, ref) => {
newValue = padValue;
}

if (pattern === 'M') {
// Month cannot be less than 1.
newValue = Math.max(1, newValue);
}

setDateField(pattern, newValue, date => handleChange(date, event));

// The currently selected month will be retained as a parameter of getInputSelectedState,
Expand All @@ -155,24 +143,24 @@ const DateInput = React.forwardRef((props: DateInputProps, ref) => {
setSelectionRange(nextState.selectionStart, nextState.selectionEnd);

// If the field is full value, move the cursor to the next field
if (
isFieldFullValue(formatStr, newValue, pattern) &&
input.selectionEnd !== input.value.length
) {
if (isMoveCursor(newValue, pattern) && input.selectionEnd !== input.value.length) {
onSegmentChange(event, 'right');
}
}
);

const onSegmentValueRemove = useEventCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
const input = event.target as HTMLInputElement;

if (selectedState.selectedPattern) {
const nextState = getInputSelectedState({ ...keyPressOptions, input, valueOffset: null });

setSelectedState(nextState);
setSelectionRange(nextState.selectionStart, nextState.selectionEnd);

setDateField(selectedState.selectedPattern, null, date => handleChange(date, event));

reset();
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
isLastDayOfMonth,
lastDayOfMonth,
isValid
} from '../utils/dateUtils';
import { useDateField, patternMap } from './DateField';
} from '../../utils/dateUtils';
import { useDateField, patternMap } from '../DateField';
import type { Locale } from 'date-fns';

interface DateInputState {
Expand Down
55 changes: 55 additions & 0 deletions src/DateInput/hooks/useFieldCursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useCallback, useRef } from 'react';
import { getPatternGroups } from '../utils';
import useUpdateEffect from '../../utils/useUpdateEffect';
export function useFieldCursor<V = Date | null>(format: string, value?: V) {
const typeCount = useRef(0);

const increment = () => {
typeCount.current += 1;
};

const reset = () => {
typeCount.current = 0;
};

const isMoveCursor = useCallback(
(value: number, pattern: string) => {
const patternGroup = getPatternGroups(format, pattern);

if (value.toString().length === patternGroup.length) {
return true;
} else if (pattern === 'y' && typeCount.current === 4) {
return true;
} else if (pattern !== 'y' && typeCount.current === 2) {
return true;
}

switch (pattern) {
case 'M':
return parseInt(`${value}0`) > 12;
case 'd':
return parseInt(`${value}0`) > 31;
case 'H':
return parseInt(`${value}0`) > 23;
case 'h':
return parseInt(`${value}0`) > 12;
case 'm':
case 's':
return parseInt(`${value}0`) > 59;
default:
return false;
}
},
[format]
);

useUpdateEffect(() => {
if (!value) {
reset();
}
}, [value]);

return { increment, reset, isMoveCursor };
}

export default useFieldCursor;
File renamed without changes.
File renamed without changes.
19 changes: 19 additions & 0 deletions src/DateInput/hooks/useSelectedState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useState } from 'react';

const defaultSelectedState = {
selectedPattern: 'y',
selectionStart: 0,
selectionEnd: 0
};

export function useSelectedState() {
const [selectedState, setSelectedState] = useState<{
selectedPattern: string;
selectionStart: number;
selectionEnd: number;
}>(defaultSelectedState);

return { selectedState, setSelectedState };
}

export default useSelectedState;
8 changes: 5 additions & 3 deletions src/DateInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import DateInput from './DateInput';
export { useDateInputState } from './useDateInputState';
export { useKeyboardInputEvent } from './useKeyboardInputEvent';
export { useIsFocused } from './useIsFocused';
export { useDateInputState } from './hooks/useDateInputState';
export { useKeyboardInputEvent } from './hooks/useKeyboardInputEvent';
export { useIsFocused } from './hooks/useIsFocused';
export { useSelectedState } from './hooks/useSelectedState';
export { useFieldCursor } from './hooks/useFieldCursor';
export * from './utils';
export type { DateInputProps } from './DateInput';
export default DateInput;
35 changes: 33 additions & 2 deletions src/DateInput/test/DateInputSpec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import CustomProvider from '../../CustomProvider';
import zhCN from '../../locales/zh_CN';
import { keyPressTests } from './testUtils';

const { testKeyPress, testContinuousKeyPress } = keyPressTests(DateInput);
const { testKeyPress, testKeyPressAsync, testContinuousKeyPress } = keyPressTests(DateInput);

describe('DateInput', () => {
testStandardProps(<DateInput />, { sizes: ['lg', 'md', 'sm', 'xs'] });
Expand Down Expand Up @@ -72,7 +72,7 @@ describe('DateInput', () => {

it('Should call `onChange` with the new value', () => {
const onChange = sinon.spy();
render(<DateInput onChange={onChange} format="yyyy-MM-dd" />);
render(<DateInput onChange={onChange} format="yyyy-MM-dd" data-test="true" />);

const input = screen.getByRole('textbox') as HTMLInputElement;

Expand Down Expand Up @@ -456,5 +456,36 @@ describe('DateInput', () => {
]
});
});

it('Should be able to enter key input continuously', async () => {
await testKeyPressAsync({
keys: '20240101'.split(''),
expectedValue: '2024-01-01'
});
});

it('Should be able to enter key input continuously with 24 hour format', async () => {
await testKeyPressAsync({
format: 'MM/dd/yyyy HH:mm:ss',
keys: '01012024 120130'.split(''),
expectedValue: '01/01/2024 12:01:30'
});
});

it('Should be able to enter key input continuously with 12 hour format', async () => {
await testKeyPressAsync({
format: 'MM/dd/yyyy hh:mm:ss',
keys: '01012024 140130'.split(''),
expectedValue: '01/01/2024 02:01:30'
});
});

it('Should be able to enter key input continuously with abbreviated month', async () => {
await testKeyPressAsync({
format: 'MMM dd,yyyy',
keys: '01012024'.split(''),
expectedValue: 'Jan 01,2024'
});
});
});
});
63 changes: 58 additions & 5 deletions src/DateInput/test/testUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import sinon from 'sinon';
import userEvent from '@testing-library/user-event';
import isMatch from 'date-fns/isMatch';
Expand All @@ -18,20 +18,66 @@ export function keyPressTests(TestComponent: React.FC<any>) {
key: string;
}) {
const onChange = sinon.spy();
render(<TestComponent onChange={onChange} format={format} defaultValue={defaultValue} />);
render(
<TestComponent
onChange={onChange}
format={format}
defaultValue={defaultValue}
data-test="true"
/>
);

const input = screen.getByRole('textbox') as HTMLInputElement;

userEvent.click(input);
userEvent.type(input, key);

userEvent.type(input, key);
expect(input).to.value(expectedValue);

if (isMatch(expectedValue, format)) {
expect(formatDate(onChange.args[0][0], format)).to.equal(expectedValue);
}
}

async function testKeyPressAsync({
defaultValue,
format = 'yyyy-MM-dd',
expectedValue,
keys
}: {
defaultValue?: Date | [Date | null, Date | null] | null;
format?: string;
expectedValue: string;
keys: string[];
}) {
const onChange = sinon.spy();
render(<TestComponent onChange={onChange} format={format} defaultValue={defaultValue} />);

const input = screen.getByRole('textbox') as HTMLInputElement;

userEvent.click(input);

let timeout = 0;

const actions = keys.map(key => {
timeout += 100;
return new Promise<void>(resolve => {
setTimeout(() => {
fireEvent.keyDown(input, { key });
resolve();
}, timeout);
});
});

await Promise.all(actions);

expect(input).to.value(expectedValue);

if (isMatch(expectedValue, format)) {
expect(formatDate(onChange.lastCall.firstArg, format)).to.equal(expectedValue);
}
}

function testContinuousKeyPress({
keySequences,
defaultValue,
Expand All @@ -45,7 +91,14 @@ export function keyPressTests(TestComponent: React.FC<any>) {
}[];
}) {
const onChange = sinon.spy();
render(<TestComponent onChange={onChange} format={format} defaultValue={defaultValue} />);
render(
<TestComponent
onChange={onChange}
format={format}
defaultValue={defaultValue}
data-test="true"
/>
);

const input = screen.getByRole('textbox') as HTMLInputElement;

Expand All @@ -57,5 +110,5 @@ export function keyPressTests(TestComponent: React.FC<any>) {
}
}

return { testKeyPress, testContinuousKeyPress };
return { testKeyPress, testKeyPressAsync, testContinuousKeyPress };
}

0 comments on commit d606944

Please sign in to comment.