Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 48 additions & 23 deletions src/PickerInput/Selector/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ const Input = React.forwardRef<InputRef, InputProps>((props, ref) => {
const inputRef = React.useRef<HTMLInputElement>(null);
// When mousedown get focus, defer selection to mouseUp so click position is used
const mouseDownRef = React.useRef(false);
// Android virtual keyboards may report `Unidentified` on keydown. In that
// case, use the following native input event to recover the intended key.
const nativeInputRef = React.useRef(false);

React.useImperativeHandle(ref, () => ({
nativeElement: holderRef.current,
Expand Down Expand Up @@ -140,18 +143,6 @@ const Input = React.forwardRef<InputRef, InputProps>((props, ref) => {
onModify(text);
});

// Directly trigger `onChange` if `format` is empty
const onInternalChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
// Hack `onChange` with format to do nothing
if (!format) {
const text = event.target.value;

onModify(text);
setInputValue(text);
onChange(text);
}
};

const onFormatPaste: React.ClipboardEventHandler<HTMLInputElement> = (event) => {
// Block paste until selection is set (after mouseUp when focus was by mousedown)
if (mouseDownRef.current) {
Expand Down Expand Up @@ -203,6 +194,7 @@ const Input = React.forwardRef<InputRef, InputProps>((props, ref) => {

const onFormatBlur: React.FocusEventHandler<HTMLInputElement> = (event) => {
setFocused(false);
nativeInputRef.current = false;

onSharedBlur(event);
};
Expand All @@ -224,17 +216,7 @@ const Input = React.forwardRef<InputRef, InputProps>((props, ref) => {
onKeyDown?.(event);
};

const onFormatKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
// Block key input until selection is set (after mouseUp when focus was by mousedown)
if (mouseDownRef.current) {
event.preventDefault();
return;
}

onSharedKeyDown(event);

const { key } = event;

const triggerFormatKey = (key: string) => {
// Save the cache with cell text
let nextCellText: string = null;

Expand Down Expand Up @@ -340,6 +322,49 @@ const Input = React.forwardRef<InputRef, InputProps>((props, ref) => {
forceSelectionSync({});
};

const onFormatKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
// Block key input until selection is set (after mouseUp when focus was by mousedown)
if (mouseDownRef.current) {
event.preventDefault();
return;
}

onSharedKeyDown(event);

const { key } = event;
nativeInputRef.current = key === 'Unidentified';

if (!nativeInputRef.current) {
triggerFormatKey(key);
}
};

// Directly trigger `onChange` if `format` is empty. Masked inputs normally
// use keydown, but Android IMEs expose the inserted text on the input event.
const onInternalChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
if (!format) {
const text = event.target.value;

onModify(text);
setInputValue(text);
onChange(text);
return;
}

if (nativeInputRef.current) {
nativeInputRef.current = false;

const nativeEvent = event.nativeEvent as InputEvent;
if (nativeEvent.inputType === 'deleteContentBackward') {
triggerFormatKey('Backspace');
} else if (nativeEvent.inputType === 'deleteContentForward') {
triggerFormatKey('Delete');
} else if (nativeEvent.data) {
triggerFormatKey(nativeEvent.data);
}
}
};

// ======================== Format ========================
const rafRef = React.useRef<number | undefined>(undefined);

Expand Down
56 changes: 56 additions & 0 deletions tests/keyboard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,62 @@ describe('Picker.Keyboard', () => {
expect(onChange).toHaveBeenCalledWith(expect.anything(), '2000-03-03');
});

it('accepts masked input from Android IME keyboards', () => {
const onChange = jest.fn();
const { container } = render(
<DayPicker
format={{
format: 'YYYYMMDD',
type: 'mask',
}}
onChange={onChange}
/>,
);
const input = container.querySelector('input');

triggerFocus(input);
'20000303'.split('').forEach((key) => {
fireEvent.keyDown(input, { key: 'Unidentified' });
fireEvent.input(input, {
target: { value: key },
inputType: 'insertText',
data: key,
});
});
fireEvent.keyDown(input, { key: 'Enter' });

expect(onChange).toHaveBeenCalledWith(expect.anything(), '20000303');
});

it('accepts masked range input from Android IME keyboards', () => {
const onChange = jest.fn();
const { container } = render(
<DayRangePicker
format={{
format: 'YYYYMMDD',
type: 'mask',
}}
onChange={onChange}
/>,
);
const inputs = container.querySelectorAll('input');

['20000303', '20000305'].forEach((value, index) => {
triggerFocus(inputs[index]);
value.split('').forEach((key) => {
fireEvent.keyDown(inputs[index], { key: 'Unidentified' });
fireEvent.input(inputs[index], {
target: { value: key },
inputType: 'insertText',
data: key,
});
});
fireEvent.keyDown(inputs[index], { key: 'Enter' });
});

expect(onChange).toHaveBeenCalledWith(expect.anything(), ['20000303', '20000305']);
});

// Coverage case: replace these tests if a clearer interaction can cover the
// same behavior. / 覆盖率用例:若有更清晰的交互覆盖相同行为,可直接替换。
it('should submit typed value on Tab without trapping focus', () => {
Expand Down