From 2df2003bd799f7d0a83ca28d9cebfb0be41a9210 Mon Sep 17 00:00:00 2001 From: JimmFly Date: Wed, 25 Sep 2024 02:02:23 +0000 Subject: [PATCH] fix(core): handle composition event for Input component (#8351) close AF-1065 --- .../components/auth-components/auth-input.tsx | 6 +- .../frontend/component/src/ui/input/index.ts | 1 + .../frontend/component/src/ui/input/input.tsx | 57 ++------- .../component/src/ui/input/row-input.tsx | 112 ++++++++++++++++++ .../page-properties/tags-inline-editor.tsx | 34 +++--- .../general-setting/editor/general.tsx | 8 +- .../page-list/docs/page-list-header.tsx | 12 +- .../page-list/selector/selector-layout.tsx | 8 +- .../modules/create-workspace/views/dialog.tsx | 16 +-- .../find-in-page/view/find-in-page-modal.tsx | 10 +- 10 files changed, 166 insertions(+), 98 deletions(-) create mode 100644 packages/frontend/component/src/ui/input/row-input.tsx diff --git a/packages/frontend/component/src/components/auth-components/auth-input.tsx b/packages/frontend/component/src/components/auth-components/auth-input.tsx index e0cdce4b83..3732364828 100644 --- a/packages/frontend/component/src/components/auth-components/auth-input.tsx +++ b/packages/frontend/component/src/components/auth-components/auth-input.tsx @@ -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 ? ( diff --git a/packages/frontend/component/src/ui/input/index.ts b/packages/frontend/component/src/ui/input/index.ts index faaa2f016e..24d79b804f 100644 --- a/packages/frontend/component/src/ui/input/index.ts +++ b/packages/frontend/component/src/ui/input/index.ts @@ -1,3 +1,4 @@ export * from './input'; +export * from './row-input'; import { Input } from './input'; export default Input; diff --git a/packages/frontend/component/src/ui/input/input.tsx b/packages/frontend/component/src/ui/input/input.tsx index 79f52a467d..2877313f0f 100644 --- a/packages/frontend/component/src/ui/input/input.tsx +++ b/packages/frontend/component/src/ui/input/input.tsx @@ -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(function Input( }: InputProps, upstreamRef: ForwardedRef ) { - const focusRef = useAutoFocus(autoFocus); - const selectRef = useAutoSelect(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 (
(function Input( }} > {preFix} - ) => { - propsOnChange?.(e.target.value); - }, - [propsOnChange] - )} - onKeyDown={useCallback( - (e: KeyboardEvent) => { - if (e.key === 'Enter') { - onEnter?.(); - } - onKeyDown?.(e); - }, - [onKeyDown, onEnter] - )} + onChange={propsOnChange} + onEnter={onEnter} + onKeyDown={onKeyDown} + onBlur={onBlur} + autoFocus={autoFocus} + autoSelect={autoSelect} {...otherProps} /> {endFix} diff --git a/packages/frontend/component/src/ui/input/row-input.tsx b/packages/frontend/component/src/ui/input/row-input.tsx new file mode 100644 index 0000000000..d04049d859 --- /dev/null +++ b/packages/frontend/component/src/ui/input/row-input.tsx @@ -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; + autoSelect?: boolean; + type?: HTMLInputElement['type']; + style?: CSSProperties; + onEnter?: () => void; +} & Omit, '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( + function RowInput( + { + disabled, + onChange: propsOnChange, + className, + style = {}, + onEnter, + onKeyDown, + onBlur, + autoFocus, + autoSelect, + ...otherProps + }: RowInputProps, + upstreamRef: ForwardedRef + ) { + const [composing, setComposing] = useState(false); + const focusRef = useAutoFocus(autoFocus); + const selectRef = useAutoSelect(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) => { + propsOnChange?.(e.target.value); + }, + [propsOnChange] + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + onKeyDown?.(e); + if (e.key !== 'Enter' || composing) { + return; + } + onEnter?.(); + }, + [onKeyDown, composing, onEnter] + ); + + const handleCompositionStart: CompositionEventHandler = + useCallback(() => { + setComposing(true); + }, []); + + const handleCompositionEnd: CompositionEventHandler = + useCallback(() => { + setComposing(false); + }, []); + + return ( + + ); + } +); diff --git a/packages/frontend/core/src/components/affine/page-properties/tags-inline-editor.tsx b/packages/frontend/core/src/components/affine/page-properties/tags-inline-editor.tsx index dbf5cdbc5f..3b70ccb413 100644 --- a/packages/frontend/core/src/components/affine/page-properties/tags-inline-editor.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/tags-inline-editor.tsx @@ -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) => { - 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) => { - 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} > - { > {tagOptions.map((tag, idx) => { const commonProps = { - focused: safeFocusedIndex === idx, + ...(safeFocusedIndex === idx ? { focused: 'true' } : {}), onClick: () => onSelectTagOption(tag), onMouseEnter: () => setFocusedIndex(idx), ['data-testid']: 'tag-selector-item', diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/general.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/general.tsx index fe3fdd6480..7e341bf2a8 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/general.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/general.tsx @@ -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) => { - systemFontFamily.search(e.target.value); + (value: string) => { + systemFontFamily.search(value); }, [systemFontFamily] ); @@ -187,7 +187,7 @@ const FontMenuItems = ({ onSelect }: { onSelect: (font: string) => void }) => {
- { inputValue ? tagList.filterTagsByName$(inputValue) : tagList.tags$ ); - const onInputChange = useCallback( - (e: React.ChangeEvent) => { - setInputValue(e.target.value); - }, - [] - ); + const onInputChange = useCallback((value: string) => { + setInputValue(value); + }, []); const handleClick = useCallback(() => { setInputValue(''); @@ -301,7 +299,7 @@ export const SwitchTag = ({ onClick }: SwitchTagProps) => {
- ) => { - onSearch?.(e.target.value); + (value: string) => { + onSearch?.(value); }, [onSearch] ); @@ -46,7 +46,7 @@ export const SelectorLayout = ({ return (
- ) => { - 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 = ({ { toggle(visible); }, [visible]); - const handleValueChange: ChangeEventHandler = 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 = () => { >
-