From b990ef58bc7c6e7237547f50784e48758f450a04 Mon Sep 17 00:00:00 2001 From: slatejack Date: Wed, 21 Aug 2024 00:51:11 +0800 Subject: [PATCH] feat(search): update search update search style to v2, alignment vue mobile feat #473 --- src/search/Search.tsx | 201 +++++++++++++++++++------------------ src/search/defaultProps.ts | 12 ++- src/search/search.en-US.md | 31 ++++++ src/search/search.md | 29 +++--- src/search/style/index.js | 2 +- src/search/type.ts | 54 ++++++---- 6 files changed, 203 insertions(+), 126 deletions(-) create mode 100644 src/search/search.en-US.md diff --git a/src/search/Search.tsx b/src/search/Search.tsx index 9a3c359d..55e4e0fc 100644 --- a/src/search/Search.tsx +++ b/src/search/Search.tsx @@ -1,26 +1,29 @@ -import React, { FC, useState, useRef } from 'react'; -import { CloseCircleFilledIcon } from 'tdesign-icons-react'; +import React, { useState, useRef } from 'react'; +import type { FC, FormEvent, CompositionEvent, MouseEvent, KeyboardEvent, FocusEvent } from 'react'; +import { CloseCircleFilledIcon, SearchIcon } from 'tdesign-icons-react'; import isFunction from 'lodash/isFunction'; -import { Button } from '../button'; +import classNames from 'classnames'; +import useDefault from '../_util/useDefault'; +import parseTNode from '../_util/parseTNode'; import useConfig from '../_util/useConfig'; import type { TdSearchProps } from './type'; import type { StyledProps } from '../common'; -import { searchDefaultProps } from './defaultProps' +import { searchDefaultProps } from './defaultProps'; +import { ENTER_REG } from '../_common/js/common'; +import useDefaultProps from '../hooks/useDefaultProps'; export interface SearchProps extends TdSearchProps, StyledProps {} const Search: FC = (props) => { const { - className = '', - style = {}, + clearable, action = '', center, disabled, focus, - label, leftIcon, placeholder, - rightIcon, + readonly, shape = 'square', value = '', onActionClick, @@ -28,110 +31,118 @@ const Search: FC = (props) => { onChange, onClear, onFocus, - onSubmit, - } = props; + } = useDefaultProps(props, searchDefaultProps); + const [focusState, setFocus] = useState(focus); + const inputRef = useRef(null); + const [searchValue, setSearchValue] = useDefault(value, '', onChange); + const { classPrefix } = useConfig(); + const rootClassName = `${classPrefix}-search`; + const boxClasses = classNames(`${rootClassName}__input-box`, `${rootClassName}__input-box--${shape}`, { + [`${classPrefix}-is-focused`]: focusState, + }); + const inputClasses = classNames(`${classPrefix}-input__keyword`, { + [`${rootClassName}--center`]: center, + }); - const inputRef = useRef(null); - const [focusState, setFocus] = useState(focus); + const inputValueChangeHandle = (e: FormEvent) => { + const { value } = e.target as HTMLInputElement; + setSearchValue(value, { trigger: 'input-change', e }); + }; + + const handleInput = (e: FormEvent) => { + if (e instanceof InputEvent) { + // 中文输入的时候inputType是insertCompositionText所以中文输入的时候禁止触发。 + const checkInputType = e.inputType && e.inputType === 'insertCompositionText'; + if (e.isComposing || checkInputType) return; + } - function handleBlur(e: React.FocusEvent) { - setFocus(false); - inputRef.current.blur(); - const { value } = e.currentTarget; - isFunction(onBlur) && onBlur(value, { e }); - } + inputValueChangeHandle(e); + }; - function handleClear(e: React.MouseEvent) { + const handleClear = (e: MouseEvent) => { + setSearchValue('', { trigger: 'input-change' }); + setFocus(true); isFunction(onClear) && onClear({ e }); - isFunction(onChange) && onChange(''); - } + }; - function handleAction(e: React.MouseEvent) { - isFunction(onActionClick) && onActionClick({ e }); - } + const handleFocus = (e: FocusEvent) => { + isFunction(onFocus) && onFocus({ value: searchValue, e }); + }; - function handleChange(e: React.ChangeEvent | React.CompositionEvent) { - const { value } = e.currentTarget; - isFunction(onChange) && onChange(value, { e }); - } + const handleBlur = (e: FocusEvent) => { + isFunction(onBlur) && onBlur({ value: searchValue, e }); + }; - function handleFocus(e: React.FocusEvent) { - const { value } = e.currentTarget; - isFunction(onFocus) && onFocus(value, { e }); - } + const handleCompositionend = (e: CompositionEvent) => { + inputValueChangeHandle(e as CompositionEvent); + }; - function handleSubmit(e: React.FocusEvent) { - const { value } = e.currentTarget; - isFunction(onSubmit) && onSubmit(value, { e }); - } + const handleAction = (e: MouseEvent) => { + isFunction(onActionClick) && onActionClick({ e }); + }; - function handleClick() { - inputRef.current.focus(); - setFocus(true); - } + const handleSearch = (e: KeyboardEvent) => { + // 如果按的是 enter 键, 13是 enter + if (ENTER_REG.test(e.code) || ENTER_REG.test(e.key)) { + e.preventDefault(); + props.onSubmit?.({ value: searchValue, e: e as KeyboardEvent }); + } + }; - const shapeStyle = { borderRadius: shape === 'square' ? null : '50px' }; + const renderLeftIcon = () => { + if (leftIcon === 'search') { + return ; + } + return parseTNode(leftIcon); + }; - return ( -
- {label && ( -
- {label} + const renderClear = () => { + if (clearable && searchValue) { + return ( +
+
- )} -
-
-
{leftIcon}
- -
- {value.length > 0 && } - {rightIcon} -
+ ); + } + return null; + }; + + const renderAction = () => { + if (action && searchValue) { + return ( +
+ {parseTNode(action)}
- + ); + } + return null; + }; + + return ( +
+
+ {renderLeftIcon()} + + {renderClear()}
- {focusState && ( - - )} + {renderAction()}
); }; -Search.defaultProps = searchDefaultProps; - export default Search; diff --git a/src/search/defaultProps.ts b/src/search/defaultProps.ts index 6b4fa88e..63202208 100644 --- a/src/search/defaultProps.ts +++ b/src/search/defaultProps.ts @@ -4,4 +4,14 @@ import { TdSearchProps } from './type'; -export const searchDefaultProps: TdSearchProps = { center: false, disabled: false, focus: false, shape: 'square' }; +export const searchDefaultProps: TdSearchProps = { + action: '', + center: false, + clearable: true, + disabled: false, + focus: false, + leftIcon: 'search', + placeholder: '', + readonly: false, + shape: 'square', +}; diff --git a/src/search/search.en-US.md b/src/search/search.en-US.md new file mode 100644 index 00000000..0fb98f78 --- /dev/null +++ b/src/search/search.en-US.md @@ -0,0 +1,31 @@ +:: BASE_DOC :: + +## API + +### Search Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | className of component | N +style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N +action | TNode | '' | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +autocompleteOptions | Array | - | autocomplete words list。Typescript:`Array` `type AutocompleteOption = string \| { label: string \| TNode; group?: boolean }`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/search/type.ts) | N +center | Boolean | false | \- | N +clearable | Boolean | true | \- | N +disabled | Boolean | false | \- | N +focus | Boolean | false | \- | N +leftIcon | TNode | 'search' | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +placeholder | String | '' | \- | N +prefixIcon | TElement | - | `deprecated`。Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +readonly | Boolean | false | \- | N +shape | String | 'square' | options: square/round | N +suffixIcon | TElement | - | `deprecated`。Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +value | String | - | \- | N +defaultValue | String | - | uncontrolled property | N +onActionClick | Function | | Typescript:`({}) => void`
| N +onBlur | Function | | Typescript:`(context: { value: string; e: FocusEvent }) => void`
| N +onChange | Function | | Typescript:`(value: string, context: { trigger: 'input-change' \| 'option-click'; e?: InputEvent \| MouseEvent }) => void`
| N +onClear | Function | | Typescript:`(context: { e: MouseEvent }) => void`
| N +onFocus | Function | | Typescript:`(context: { value: string; e: FocusEvent }) => void`
| N +onSearch | Function | | Typescript:`(context?: { value: string; trigger: 'submit' \| 'option-click' \| 'clear'; e?: InputEvent \| MouseEvent }) => void`
| N +onSubmit | Function | | Typescript:`(context: { value: string; e: KeyboardEvent }) => void`
| N diff --git a/src/search/search.md b/src/search/search.md index dae4b813..0df50531 100644 --- a/src/search/search.md +++ b/src/search/search.md @@ -1,26 +1,31 @@ :: BASE_DOC :: ## API + ### Search Props -名称 | 类型 | 默认值 | 说明 | 必传 +名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N -action | TNode | '' | 自定义右侧操作按钮文字。TS 类型:`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +action | TNode | '' | 自定义右侧操作按钮文字。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +autocompleteOptions | Array | - | 【讨论中】联想词列表,如果不存在或长度为 0 则不显示联想框。可以使用函数 `label` 自定义联想词为任意内容;也可使用插槽 `option` 定义联想词内容,插槽参数为 `{ option: AutocompleteOption; index: number }`。如果 `group` 值为 `true` 则表示当前项为分组标题。TS 类型:`Array` `type AutocompleteOption = string \| { label: string \| TNode; group?: boolean }`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/search/type.ts) | N center | Boolean | false | 是否居中 | N +clearable | Boolean | true | 是否启用清除控件 | N disabled | Boolean | false | 是否禁用 | N focus | Boolean | false | 是否聚焦 | N -label | String | '' | 左侧文本 | N -leftIcon | TElement | - | 左侧图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +leftIcon | TNode | 'search' | 左侧图标。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N placeholder | String | '' | 占位符 | N -rightIcon | TElement | - | 右侧图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +prefixIcon | TElement | - | 已废弃。前置图标,默认为搜索图标。值为 `null` 时则不显示。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +readonly | Boolean | false | 只读状态 | N shape | String | 'square' | 搜索框形状。可选项:square/round | N -value | String | '' | 值 | N -defaultValue | String | '' | 值。非受控属性 | N -onActionClick | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
点击右侧操作按钮文字时触发时触发 | N -onBlur | Function | | TS 类型:`(value: string, context: { e: FocusEvent }) => void`
失去焦点时触发 | N -onChange | Function | | TS 类型:`(value: string, context?: { e?: InputEvent | MouseEvent }) => void`
值发生变化时触发 | N +suffixIcon | TElement | - | 已废弃。后置图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +value | String | - | 值 | N +defaultValue | String | - | 值。非受控属性 | N +onActionClick | Function | | TS 类型:`({}) => void`
点击右侧操作按钮文字时触发 | N +onBlur | Function | | TS 类型:`(context: { value: string; e: FocusEvent }) => void`
失去焦点时触发 | N +onChange | Function | | TS 类型:`(value: string, context: { trigger: 'input-change' \| 'option-click'; e?: InputEvent \| MouseEvent }) => void`
搜索关键词发生变化时触发,可能场景有:搜索框内容发生变化、点击联想词 | N onClear | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
点击清除时触发 | N -onFocus | Function | | TS 类型:`(value: string, context: { e: FocusEvent }) => void`
获得焦点时触发 | N -onSubmit | Function | | TS 类型:`(value: string, context: { e: KeyboardEvent }) => void`
提交时触发 | N +onFocus | Function | | TS 类型:`(context: { value: string; e: FocusEvent }) => void`
获得焦点时触发 | N +onSearch | Function | | TS 类型:`(context?: { value: string; trigger: 'submit' \| 'option-click' \| 'clear'; e?: InputEvent \| MouseEvent }) => void`
【讨论中】搜索触发,包含:手机键盘提交健、联想关键词点击、清空按钮点击等 | N +onSubmit | Function | | TS 类型:`(context: { value: string; e: KeyboardEvent }) => void`
提交时触发,如:手机键盘提交按钮点击 | N diff --git a/src/search/style/index.js b/src/search/style/index.js index d3550302..413313b3 100644 --- a/src/search/style/index.js +++ b/src/search/style/index.js @@ -1 +1 @@ -import '../../_common/style/mobile/components/search/_index.less'; +import '../../_common/style/mobile/components/search/v2/_index.less'; diff --git a/src/search/type.ts b/src/search/type.ts index 6044bd4c..02ad2faa 100644 --- a/src/search/type.ts +++ b/src/search/type.ts @@ -4,7 +4,7 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TNode, TElement } from '../common'; +import { TNode } from '../common'; import { MouseEvent, KeyboardEvent, FocusEvent, FormEvent } from 'react'; export interface TdSearchProps { @@ -13,11 +13,20 @@ export interface TdSearchProps { * @default '' */ action?: TNode; + /** + * 【讨论中】联想词列表,如果不存在或长度为 0 则不显示联想框。可以使用函数 `label` 自定义联想词为任意内容;也可使用插槽 `option` 定义联想词内容,插槽参数为 `{ option: AutocompleteOption; index: number }`。如果 `group` 值为 `true` 则表示当前项为分组标题 + */ + autocompleteOptions?: Array; /** * 是否居中 * @default false */ center?: boolean; + /** + * 是否启用清除控件 + * @default true + */ + clearable?: boolean; /** * 是否禁用 * @default false @@ -28,24 +37,21 @@ export interface TdSearchProps { * @default false */ focus?: boolean; - /** - * 左侧文本 - * @default '' - */ - label?: string; /** * 左侧图标 + * @default 'search' */ - leftIcon?: TElement; + leftIcon?: TNode; /** * 占位符 * @default '' */ placeholder?: string; /** - * 右侧图标 + * 只读状态 + * @default false */ - rightIcon?: TElement; + readonly?: boolean; /** * 搜索框形状 * @default 'square' @@ -62,20 +68,23 @@ export interface TdSearchProps { */ defaultValue?: string; /** - * 点击右侧操作按钮文字时触发时触发 + * 点击右侧操作按钮文字时触发 * @default '' */ - onActionClick?: (context: { e: MouseEvent }) => void; + onActionClick?: ({}) => void; /** * 失去焦点时触发 * @default '' */ - onBlur?: (value: string, context: { e: FocusEvent }) => void; + onBlur?: (context: { value: string; e: FocusEvent }) => void; /** - * 值发生变化时触发 + * 搜索关键词发生变化时触发,可能场景有:搜索框内容发生变化、点击联想词 * @default '' */ - onChange?: (value: string, context?: { e?: FormEvent | MouseEvent }) => void; + onChange?: ( + value: string, + context: { trigger: 'input-change' | 'option-click'; e?: FormEvent | MouseEvent }, + ) => void; /** * 点击清除时触发 * @default '' @@ -85,10 +94,21 @@ export interface TdSearchProps { * 获得焦点时触发 * @default '' */ - onFocus?: (value: string, context: { e: FocusEvent }) => void; + onFocus?: (context: { value: string; e: FocusEvent }) => void; + /** + * 【讨论中】搜索触发,包含:手机键盘提交健、联想关键词点击、清空按钮点击等 + * @default '' + */ + onSearch?: (context?: { + value: string; + trigger: 'submit' | 'option-click' | 'clear'; + e?: FormEvent | MouseEvent; + }) => void; /** - * 提交时触发 + * 提交时触发,如:手机键盘提交按钮点击 * @default '' */ - onSubmit?: (value: string, context: { e: KeyboardEvent }) => void; + onSubmit?: (context: { value: string; e: KeyboardEvent }) => void; } + +export type AutocompleteOption = string | { label: string | TNode; group?: boolean };