mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
fix(core): handle composition event for Input component (#8351)
close AF-1065
This commit is contained in:
@@ -30,11 +30,7 @@ export const AuthInput = ({
|
||||
className={clsx(className)}
|
||||
size="extraLarge"
|
||||
status={error ? 'error' : 'default'}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
onEnter?.();
|
||||
}
|
||||
}}
|
||||
onEnter={onEnter}
|
||||
{...inputProps}
|
||||
/>
|
||||
{error && errorHint && !withoutHint ? (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './input';
|
||||
export * from './row-input';
|
||||
import { Input } from './input';
|
||||
export default Input;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
+19
-15
@@ -1,5 +1,11 @@
|
||||
import type { MenuProps } from '@affine/component';
|
||||
import { IconButton, Input, Menu, Scrollable } from '@affine/component';
|
||||
import {
|
||||
IconButton,
|
||||
Input,
|
||||
Menu,
|
||||
RowInput,
|
||||
Scrollable,
|
||||
} from '@affine/component';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { WorkspaceLegacyProperties } from '@affine/core/modules/properties';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
@@ -239,12 +245,9 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||
[setOpen, setSelectedTagIds]
|
||||
);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const onInputChange = useCallback((value: string) => {
|
||||
setInputValue(value);
|
||||
}, []);
|
||||
|
||||
const onToggleTag = useCallback(
|
||||
(id: string) => {
|
||||
@@ -297,14 +300,15 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||
},
|
||||
[onCreateTag, onToggleTag, focusInput, tagIds.length]
|
||||
);
|
||||
const onEnter = useCallback(() => {
|
||||
if (safeFocusedIndex >= 0) {
|
||||
onSelectTagOption(tagOptions[safeFocusedIndex]);
|
||||
}
|
||||
}, [onSelectTagOption, safeFocusedIndex, tagOptions]);
|
||||
|
||||
const onInputKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (safeFocusedIndex >= 0) {
|
||||
onSelectTagOption(tagOptions[safeFocusedIndex]);
|
||||
}
|
||||
} else if (e.key === 'Backspace' && inputValue === '' && tagIds.length) {
|
||||
if (e.key === 'Backspace' && inputValue === '' && tagIds.length) {
|
||||
const tagToRemove =
|
||||
safeInlineFocusedIndex < 0 || safeInlineFocusedIndex >= tagIds.length
|
||||
? tagIds.length - 1
|
||||
@@ -341,7 +345,6 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||
inputValue,
|
||||
tagIds,
|
||||
safeFocusedIndex,
|
||||
onSelectTagOption,
|
||||
tagOptions,
|
||||
safeInlineFocusedIndex,
|
||||
tags,
|
||||
@@ -358,11 +361,12 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||
focusedIndex={safeInlineFocusedIndex}
|
||||
onRemove={focusInput}
|
||||
>
|
||||
<input
|
||||
<RowInput
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onKeyDown={onInputKeyDown}
|
||||
onEnter={onEnter}
|
||||
autoFocus
|
||||
className={styles.searchInput}
|
||||
placeholder="Type here ..."
|
||||
@@ -380,7 +384,7 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
|
||||
>
|
||||
{tagOptions.map((tag, idx) => {
|
||||
const commonProps = {
|
||||
focused: safeFocusedIndex === idx,
|
||||
...(safeFocusedIndex === idx ? { focused: 'true' } : {}),
|
||||
onClick: () => onSelectTagOption(tag),
|
||||
onMouseEnter: () => setFocusedIndex(idx),
|
||||
['data-testid']: 'tag-selector-item',
|
||||
|
||||
+4
-4
@@ -6,6 +6,7 @@ import {
|
||||
MenuTrigger,
|
||||
RadioGroup,
|
||||
type RadioItem,
|
||||
RowInput,
|
||||
Scrollable,
|
||||
Switch,
|
||||
useConfirmModal,
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
type ChangeEvent,
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
type PropsWithChildren,
|
||||
@@ -171,8 +171,8 @@ const FontMenuItems = ({ onSelect }: { onSelect: (font: string) => void }) => {
|
||||
const searchText = useLiveData(systemFontFamily.searchText$);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
systemFontFamily.search(e.target.value);
|
||||
(value: string) => {
|
||||
systemFontFamily.search(value);
|
||||
},
|
||||
[systemFontFamily]
|
||||
);
|
||||
@@ -187,7 +187,7 @@ const FontMenuItems = ({ onSelect }: { onSelect: (font: string) => void }) => {
|
||||
<div>
|
||||
<div className={styles.InputContainer}>
|
||||
<SearchIcon className={styles.searchIcon} />
|
||||
<input
|
||||
<RowInput
|
||||
value={searchText ?? ''}
|
||||
onChange={onInputChange}
|
||||
onKeyDown={onInputKeyDown}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Button,
|
||||
Divider,
|
||||
Menu,
|
||||
RowInput,
|
||||
Scrollable,
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
@@ -285,12 +286,9 @@ export const SwitchTag = ({ onClick }: SwitchTagProps) => {
|
||||
inputValue ? tagList.filterTagsByName$(inputValue) : tagList.tags$
|
||||
);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const onInputChange = useCallback((value: string) => {
|
||||
setInputValue(value);
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setInputValue('');
|
||||
@@ -301,7 +299,7 @@ export const SwitchTag = ({ onClick }: SwitchTagProps) => {
|
||||
<div className={styles.tagsEditorRoot}>
|
||||
<div className={styles.tagsEditorSelectedTags}>
|
||||
<SearchIcon className={styles.searchIcon} />
|
||||
<input
|
||||
<RowInput
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
autoFocus
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { Button, RowInput } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { type PropsWithChildren, type ReactNode, useCallback } from 'react';
|
||||
|
||||
@@ -37,8 +37,8 @@ export const SelectorLayout = ({
|
||||
const t = useI18n();
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onSearch?.(e.target.value);
|
||||
(value: string) => {
|
||||
onSearch?.(value);
|
||||
},
|
||||
[onSearch]
|
||||
);
|
||||
@@ -46,7 +46,7 @@ export const SelectorLayout = ({
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<header className={styles.header}>
|
||||
<input
|
||||
<RowInput
|
||||
className={styles.search}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={onSearchChange}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
@@ -76,14 +75,11 @@ const NameWorkspaceContent = ({
|
||||
);
|
||||
}, [enable, onConfirmName, workspaceName]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter' && workspaceName) {
|
||||
handleCreateWorkspace();
|
||||
}
|
||||
},
|
||||
[handleCreateWorkspace, workspaceName]
|
||||
);
|
||||
const onEnter = useCallback(() => {
|
||||
if (workspaceName) {
|
||||
handleCreateWorkspace();
|
||||
}
|
||||
}, [handleCreateWorkspace, workspaceName]);
|
||||
|
||||
// Currently, when we create a new workspace and upload an avatar at the same time,
|
||||
// an error occurs after the creation is successful: get blob 404 not found
|
||||
@@ -117,7 +113,7 @@ const NameWorkspaceContent = ({
|
||||
<Input
|
||||
autoFocus
|
||||
data-testid="create-workspace-input"
|
||||
onKeyDown={handleKeyDown}
|
||||
onEnter={onEnter}
|
||||
placeholder={t['com.affine.nameWorkspace.placeholder']()}
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton, observeResize } from '@affine/component';
|
||||
import { IconButton, observeResize, RowInput } from '@affine/component';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
ArrowUpSmallIcon,
|
||||
@@ -10,7 +10,6 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
type ChangeEventHandler,
|
||||
type CompositionEventHandler,
|
||||
type KeyboardEventHandler,
|
||||
type SetStateAction,
|
||||
@@ -102,9 +101,8 @@ export const FindInPageModal = () => {
|
||||
toggle(visible);
|
||||
}, [visible]);
|
||||
|
||||
const handleValueChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
e => {
|
||||
const value = e.target.value;
|
||||
const handleValueChange = useCallback(
|
||||
(value: string) => {
|
||||
setValue(value);
|
||||
if (!composing) {
|
||||
findInPage.findInPage(value);
|
||||
@@ -227,7 +225,7 @@ export const FindInPageModal = () => {
|
||||
>
|
||||
<SearchIcon className={styles.searchIcon} />
|
||||
<div className={styles.inputMain}>
|
||||
<input
|
||||
<RowInput
|
||||
type="text"
|
||||
autoFocus
|
||||
value={value}
|
||||
|
||||
Reference in New Issue
Block a user