fix(core): handle composition event for Input component (#8351)

close AF-1065
This commit is contained in:
JimmFly
2024-09-25 02:02:23 +00:00
parent ed8e4e30f0
commit 2df2003bd7
10 changed files with 166 additions and 98 deletions

View File

@@ -1,3 +1,4 @@
export * from './input';
export * from './row-input';
import { Input } from './input';
export default Input;

View File

@@ -1,16 +1,14 @@
import clsx from 'clsx';
import type {
ChangeEvent,
CSSProperties,
ForwardedRef,
InputHTMLAttributes,
KeyboardEvent,
KeyboardEventHandler,
ReactNode,
} from 'react';
import { forwardRef, useCallback, useEffect } from 'react';
import { forwardRef } from 'react';
import { useAutoFocus, useAutoSelect } from '../../hooks';
import { RowInput } from './row-input';
import { input, inputWrapper } from './style.css';
export type InputProps = {
@@ -50,32 +48,6 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
}: InputProps,
upstreamRef: ForwardedRef<HTMLInputElement>
) {
const focusRef = useAutoFocus<HTMLInputElement>(autoFocus);
const selectRef = useAutoSelect<HTMLInputElement>(autoSelect);
const inputRef = (el: HTMLInputElement | null) => {
focusRef.current = el;
selectRef.current = el;
if (upstreamRef) {
if (typeof upstreamRef === 'function') {
upstreamRef(el);
} else {
upstreamRef.current = el;
}
}
};
// use native blur event to get event after unmount
// don't use useLayoutEffect here, because the cleanup function will be called before unmount
useEffect(() => {
if (!onBlur) return;
selectRef.current?.addEventListener('blur', onBlur as any);
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
selectRef.current?.removeEventListener('blur', onBlur as any);
};
}, [onBlur, selectRef]);
return (
<div
className={clsx(inputWrapper, className, {
@@ -96,29 +68,20 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
}}
>
{preFix}
<input
<RowInput
className={clsx(input, {
large: size === 'large',
'extra-large': size === 'extraLarge',
})}
ref={inputRef}
ref={upstreamRef}
disabled={disabled}
style={inputStyle}
onChange={useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
propsOnChange?.(e.target.value);
},
[propsOnChange]
)}
onKeyDown={useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onEnter?.();
}
onKeyDown?.(e);
},
[onKeyDown, onEnter]
)}
onChange={propsOnChange}
onEnter={onEnter}
onKeyDown={onKeyDown}
onBlur={onBlur}
autoFocus={autoFocus}
autoSelect={autoSelect}
{...otherProps}
/>
{endFix}

View File

@@ -0,0 +1,112 @@
import type {
ChangeEvent,
CompositionEventHandler,
CSSProperties,
ForwardedRef,
InputHTMLAttributes,
KeyboardEvent,
KeyboardEventHandler,
} from 'react';
import { forwardRef, useCallback, useEffect, useState } from 'react';
import { useAutoFocus, useAutoSelect } from '../../hooks';
export type RowInputProps = {
disabled?: boolean;
onChange?: (value: string) => void;
onBlur?: (ev: FocusEvent & { currentTarget: HTMLInputElement }) => void;
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
autoSelect?: boolean;
type?: HTMLInputElement['type'];
style?: CSSProperties;
onEnter?: () => void;
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'size' | 'onBlur'>;
// RowInput component that is used in the selector layout for search input
// handles composition events and enter key press
export const RowInput = forwardRef<HTMLInputElement, RowInputProps>(
function RowInput(
{
disabled,
onChange: propsOnChange,
className,
style = {},
onEnter,
onKeyDown,
onBlur,
autoFocus,
autoSelect,
...otherProps
}: RowInputProps,
upstreamRef: ForwardedRef<HTMLInputElement>
) {
const [composing, setComposing] = useState(false);
const focusRef = useAutoFocus<HTMLInputElement>(autoFocus);
const selectRef = useAutoSelect<HTMLInputElement>(autoSelect);
const inputRef = (el: HTMLInputElement | null) => {
focusRef.current = el;
selectRef.current = el;
if (upstreamRef) {
if (typeof upstreamRef === 'function') {
upstreamRef(el);
} else {
upstreamRef.current = el;
}
}
};
// use native blur event to get event after unmount
// don't use useLayoutEffect here, because the cleanup function will be called before unmount
useEffect(() => {
if (!onBlur) return;
selectRef.current?.addEventListener('blur', onBlur as any);
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
selectRef.current?.removeEventListener('blur', onBlur as any);
};
}, [onBlur, selectRef]);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
propsOnChange?.(e.target.value);
},
[propsOnChange]
);
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
onKeyDown?.(e);
if (e.key !== 'Enter' || composing) {
return;
}
onEnter?.();
},
[onKeyDown, composing, onEnter]
);
const handleCompositionStart: CompositionEventHandler<HTMLInputElement> =
useCallback(() => {
setComposing(true);
}, []);
const handleCompositionEnd: CompositionEventHandler<HTMLInputElement> =
useCallback(() => {
setComposing(false);
}, []);
return (
<input
className={className}
ref={inputRef}
disabled={disabled}
style={style}
onChange={handleChange}
onKeyDown={handleKeyDown}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
{...otherProps}
/>
);
}
);