Skip to content

Commit

Permalink
feat(search): update search
Browse files Browse the repository at this point in the history
update search style to v2, alignment vue mobile

feat Tencent#473
  • Loading branch information
slatejack authored and anlyyao committed Aug 21, 2024
1 parent ee54e86 commit b990ef5
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 126 deletions.
201 changes: 106 additions & 95 deletions src/search/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,137 +1,148 @@
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<SearchProps> = (props) => {
const {
className = '',
style = {},
clearable,
action = '',
center,
disabled,
focus,
label,
leftIcon,
placeholder,
rightIcon,
readonly,
shape = 'square',
value = '',
onActionClick,
onBlur,
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<HTMLInputElement>) => {
const { value } = e.target as HTMLInputElement;
setSearchValue(value, { trigger: 'input-change', e });
};

const handleInput = (e: FormEvent<HTMLInputElement>) => {
if (e instanceof InputEvent) {
// 中文输入的时候inputType是insertCompositionText所以中文输入的时候禁止触发。
const checkInputType = e.inputType && e.inputType === 'insertCompositionText';
if (e.isComposing || checkInputType) return;
}

function handleBlur(e: React.FocusEvent<HTMLInputElement, Element>) {
setFocus(false);
inputRef.current.blur();
const { value } = e.currentTarget;
isFunction(onBlur) && onBlur(value, { e });
}
inputValueChangeHandle(e);
};

function handleClear(e: React.MouseEvent<SVGSVGElement, MouseEvent>) {
const handleClear = (e: MouseEvent) => {
setSearchValue('', { trigger: 'input-change' });
setFocus(true);
isFunction(onClear) && onClear({ e });
isFunction(onChange) && onChange('');
}
};

function handleAction(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
isFunction(onActionClick) && onActionClick({ e });
}
const handleFocus = (e: FocusEvent) => {
isFunction(onFocus) && onFocus({ value: searchValue, e });
};

function handleChange(e: React.ChangeEvent<HTMLInputElement> | React.CompositionEvent<HTMLInputElement>) {
const { value } = e.currentTarget;
isFunction(onChange) && onChange(value, { e });
}
const handleBlur = (e: FocusEvent) => {
isFunction(onBlur) && onBlur({ value: searchValue, e });
};

function handleFocus(e: React.FocusEvent<HTMLInputElement, Element>) {
const { value } = e.currentTarget;
isFunction(onFocus) && onFocus(value, { e });
}
const handleCompositionend = (e: CompositionEvent) => {
inputValueChangeHandle(e as CompositionEvent<HTMLInputElement>);
};

function handleSubmit(e: React.FocusEvent<HTMLInputElement, Element>) {
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<HTMLDivElement> });
}
};

const shapeStyle = { borderRadius: shape === 'square' ? null : '50px' };
const renderLeftIcon = () => {
if (leftIcon === 'search') {
return <SearchIcon size="24px" />;
}
return parseTNode(leftIcon);
};

return (
<div
className={`${classPrefix}-search ${focusState ? `${classPrefix}-is-focused` : ''} ${className}`}
style={{ ...style }}
>
{label && (
<div
className={`${classPrefix}-search__label-text`}
style={{
marginLeft: '0px',
paddingRight: '8px',
color: 'rgba(0,0,0,0.9)',
whiteSpace: 'nowrap',
}}
>
{label}
const renderClear = () => {
if (clearable && searchValue) {
return (
<div className={`${rootClassName}__clear`} onClick={handleClear}>
<CloseCircleFilledIcon size="24" />
</div>
)}
<div className={`${classPrefix}-search__form`} style={{ ...shapeStyle }}>
<div className={`${classPrefix}-search__box`}>
<div className={`${classPrefix}-search__icon-search`}>{leftIcon}</div>
<input
style={{ textAlign: center ? 'center' : null }}
ref={inputRef}
type="text"
autoFocus={focusState}
disabled={disabled}
value={value}
placeholder={placeholder}
className={`${classPrefix}-search__input`}
onBlur={handleBlur}
onChange={handleChange}
onFocus={handleFocus}
onSubmit={handleSubmit}
/>
<div className={`${classPrefix}-search__icon-close`}>
{value.length > 0 && <CloseCircleFilledIcon onClick={handleClear} />}
{rightIcon}
</div>
);
}
return null;
};

const renderAction = () => {
if (action && searchValue) {
return (
<div className={`${rootClassName}__search-action`} onClick={handleAction}>
{parseTNode(action)}
</div>
<label className={`${classPrefix}-search__label`} style={{ ...shapeStyle }} onClick={handleClick}>
<div className={`${classPrefix}-search__label-icon-search`}>{leftIcon}</div>
<span className={`${classPrefix}-search__label-text`}>{placeholder}</span>
</label>
);
}
return null;
};

return (
<div className={`${rootClassName}`}>
<div className={`${boxClasses}`}>
{renderLeftIcon()}
<input
ref={inputRef}
value={searchValue}
type="search"
className={`${inputClasses}`}
autoFocus={focus}
placeholder={placeholder}
readOnly={readonly}
disabled={disabled}
onKeyPress={handleSearch}
onFocus={handleFocus}
onBlur={handleBlur}
onInput={handleInput}
onCompositionEnd={handleCompositionend}
/>
{renderClear()}
</div>
{focusState && (
<Button
className={`${classPrefix}-search__cancel-button`}
variant="text"
theme="primary"
onClick={handleAction}
>
{action}
</Button>
)}
{renderAction()}
</div>
);
};

Search.defaultProps = searchDefaultProps;

export default Search;
12 changes: 11 additions & 1 deletion src/search/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
31 changes: 31 additions & 0 deletions src/search/search.en-US.md
Original file line number Diff line number Diff line change
@@ -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<AutocompleteOption>` `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`<br/> | N
onBlur | Function | | Typescript:`(context: { value: string; e: FocusEvent }) => void`<br/> | N
onChange | Function | | Typescript:`(value: string, context: { trigger: 'input-change' \| 'option-click'; e?: InputEvent \| MouseEvent }) => void`<br/> | N
onClear | Function | | Typescript:`(context: { e: MouseEvent }) => void`<br/> | N
onFocus | Function | | Typescript:`(context: { value: string; e: FocusEvent }) => void`<br/> | N
onSearch | Function | | Typescript:`(context?: { value: string; trigger: 'submit' \| 'option-click' \| 'clear'; e?: InputEvent \| MouseEvent }) => void`<br/> | N
onSubmit | Function | | Typescript:`(context: { value: string; e: KeyboardEvent }) => void`<br/> | N
29 changes: 17 additions & 12 deletions src/search/search.md
Original file line number Diff line number Diff line change
@@ -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<AutocompleteOption>` `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`<br/>点击右侧操作按钮文字时触发时触发 | N
onBlur | Function | | TS 类型:`(value: string, context: { e: FocusEvent }) => void`<br/>失去焦点时触发 | N
onChange | Function | | TS 类型:`(value: string, context?: { e?: InputEvent | MouseEvent }) => void`<br/>值发生变化时触发 | 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`<br/>点击右侧操作按钮文字时触发 | N
onBlur | Function | | TS 类型:`(context: { value: string; e: FocusEvent }) => void`<br/>失去焦点时触发 | N
onChange | Function | | TS 类型:`(value: string, context: { trigger: 'input-change' \| 'option-click'; e?: InputEvent \| MouseEvent }) => void`<br/>搜索关键词发生变化时触发,可能场景有:搜索框内容发生变化、点击联想词 | N
onClear | Function | | TS 类型:`(context: { e: MouseEvent }) => void`<br/>点击清除时触发 | N
onFocus | Function | | TS 类型:`(value: string, context: { e: FocusEvent }) => void`<br/>获得焦点时触发 | N
onSubmit | Function | | TS 类型:`(value: string, context: { e: KeyboardEvent }) => void`<br/>提交时触发 | N
onFocus | Function | | TS 类型:`(context: { value: string; e: FocusEvent }) => void`<br/>获得焦点时触发 | N
onSearch | Function | | TS 类型:`(context?: { value: string; trigger: 'submit' \| 'option-click' \| 'clear'; e?: InputEvent \| MouseEvent }) => void`<br/>【讨论中】搜索触发,包含:手机键盘提交健、联想关键词点击、清空按钮点击等 | N
onSubmit | Function | | TS 类型:`(context: { value: string; e: KeyboardEvent }) => void`<br/>提交时触发,如:手机键盘提交按钮点击 | N
2 changes: 1 addition & 1 deletion src/search/style/index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import '../../_common/style/mobile/components/search/_index.less';
import '../../_common/style/mobile/components/search/v2/_index.less';
Loading

0 comments on commit b990ef5

Please sign in to comment.