mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
feat: SearchBox组件支持disabled & loading状态 (#8821)
This commit is contained in:
parent
02e2552f00
commit
c936aaedf2
@ -29,6 +29,161 @@ icon:
|
||||
}
|
||||
```
|
||||
|
||||
## 禁用样式
|
||||
|
||||
> `6.0.0`及以上版本
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"initApi": "/api/mock2/page/initData?keywords=${keywords}",
|
||||
"body": [
|
||||
{
|
||||
"type": "flex",
|
||||
"direction": "column",
|
||||
"justify": "flex-start",
|
||||
"alignItems": "flex-start",
|
||||
"items": [
|
||||
{
|
||||
"type": "search-box",
|
||||
"name": "keywords",
|
||||
"disabled": true,
|
||||
"style": {
|
||||
"marginBottom": "10px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "search-box",
|
||||
"name": "keywords",
|
||||
"mini": true,
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 加载状态
|
||||
|
||||
> `6.0.0` 及以上版本
|
||||
|
||||
设置`"loading": true`, 标识开关操作的异步任务仍在执行中。另外`loadingOn`支持表达式,配合`ajax`动作,实现搜索操作时的loading状态。
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"id": "demo-page",
|
||||
"data": {
|
||||
"isFetching": false,
|
||||
"fetched": false
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"type": "search-box",
|
||||
"name": "keywords",
|
||||
"clearable": true,
|
||||
"loadingOn": "${isFetching}",
|
||||
"className": "mb-2",
|
||||
"onEvent": {
|
||||
"search": {
|
||||
"actions": [
|
||||
{
|
||||
"actionType": "setValue",
|
||||
"componentId": "demo-page",
|
||||
"args": {
|
||||
"value": {
|
||||
"isFetching": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"actionType": "toast",
|
||||
"args": {
|
||||
"msgType": "warning",
|
||||
"msg": "开始检索..."
|
||||
}
|
||||
},
|
||||
{
|
||||
"actionType": "ajax",
|
||||
"api": {
|
||||
"url": "/api/mock2/sample?perPage=5&waitSeconds=2&keywords=${keywords}",
|
||||
"method": "get",
|
||||
"messages": {
|
||||
"success": "检索完成",
|
||||
"failed": "检索失败"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"actionType": "setValue",
|
||||
"componentId": "demo-page",
|
||||
"args": {
|
||||
"value": {
|
||||
"isFetching": false,
|
||||
"fetched": true,
|
||||
"total": "${event.data.responseResult.count}",
|
||||
"datasource": "${event.data.responseResult.rows}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "flex",
|
||||
"visibleOn": "${fetched && !isFetching}",
|
||||
"justify": "flex-start",
|
||||
"alignItems": "flex-start",
|
||||
"direction": "column",
|
||||
"items": [
|
||||
{
|
||||
"type": "flex",
|
||||
"className": "mb-2",
|
||||
"alignItems": "self-end",
|
||||
"items": [
|
||||
{
|
||||
"type": "status",
|
||||
"value": "success",
|
||||
"className": "mr-2",
|
||||
"labelMap": {
|
||||
"success": "检索成功:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tpl",
|
||||
"tpl": "总计${total}条,当前仅展示5条"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"source": "${datasource}",
|
||||
"listItem": {
|
||||
"body": [
|
||||
{
|
||||
"type": "hbox",
|
||||
"columns": [
|
||||
{
|
||||
"label": "Engine",
|
||||
"name": "engine"
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"label": "Version"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 加强样式
|
||||
|
||||
```schema
|
||||
@ -224,6 +379,8 @@ icon:
|
||||
| mini | `boolean` | | 是否为 mini 模式 |
|
||||
| searchImediately | `boolean` | | 是否立即搜索 |
|
||||
| clearAndSubmit | `boolean` | | 清空搜索框内容后立即执行搜索 | `2.8.0` |
|
||||
| disabled | `boolean` | `false` | 是否为禁用状态 | `6.0.0` |
|
||||
| loading | `boolean` | `false` | 是否处于加载状态 | `6.0.0` |
|
||||
|
||||
## 事件表
|
||||
|
||||
|
@ -826,6 +826,7 @@ $Table-strip-bg: transparent;
|
||||
--SearchBox-enhonce-disabled-color: var(--colors-neutral-text-9);
|
||||
--SearchBox-enhonce-disabled-search-color: var(--colors-neutral-text-6);
|
||||
--SearchBox-enhonce-clearable-gap: var(--borders-radius-4);
|
||||
--SearchBox-search-btn-color--disabled: var(--colors-neutral-fill-6);
|
||||
|
||||
--IconSelect-searchBox-width: #{px2rem(246px)};
|
||||
--IconSelect-type-item-height: #{px2rem(48px)};
|
||||
|
@ -41,6 +41,11 @@
|
||||
&-searchBtn {
|
||||
display: inline-block;
|
||||
padding: #{px2rem(5px)} #{px2rem(10px)};
|
||||
|
||||
&--loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-activeBtn,
|
||||
@ -65,7 +70,7 @@
|
||||
&.is-disabled &-activeBtn,
|
||||
&.is-disabled &-searchBtn,
|
||||
&.is-disabled &-cancelBtn {
|
||||
color: var(--icon-onDisabled-color);
|
||||
color: var(--SearchBox-search-btn-color--disabled);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,9 @@ import {autobind} from 'amis-core';
|
||||
import {LocaleProps, localeable} from 'amis-core';
|
||||
import chain from 'lodash/chain';
|
||||
import Input from './Input';
|
||||
import Spinner from './Spinner';
|
||||
|
||||
import {SpinnerExtraProps} from './Spinner';
|
||||
|
||||
export interface HistoryRecord {
|
||||
/** 历史记录值 */
|
||||
@ -28,7 +31,10 @@ export interface SearchHistoryOptions {
|
||||
dropdownClassName?: string;
|
||||
}
|
||||
|
||||
export interface SearchBoxProps extends ThemeProps, LocaleProps {
|
||||
export interface SearchBoxProps
|
||||
extends ThemeProps,
|
||||
LocaleProps,
|
||||
SpinnerExtraProps {
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
mini?: boolean;
|
||||
@ -42,13 +48,14 @@ export interface SearchBoxProps extends ThemeProps, LocaleProps {
|
||||
active?: boolean;
|
||||
defaultActive?: boolean;
|
||||
onActiveChange?: (active: boolean) => void;
|
||||
onSearch?: (value: string) => void;
|
||||
onSearch?: (value: string) => any;
|
||||
onCancel?: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
/** 历史记录配置 */
|
||||
history?: SearchHistoryOptions;
|
||||
clearAndSubmit?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchBoxState {
|
||||
@ -87,6 +94,7 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
|
||||
lazyEmitSearch = debounce(
|
||||
() => {
|
||||
const onSearch = this.props.onSearch;
|
||||
|
||||
onSearch?.(this.state.inputValue ?? '');
|
||||
},
|
||||
250,
|
||||
@ -276,6 +284,7 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
|
||||
renderInput(isHistoryMode?: boolean) {
|
||||
const {
|
||||
classnames: cx,
|
||||
classPrefix,
|
||||
active,
|
||||
name,
|
||||
className,
|
||||
@ -286,7 +295,9 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
|
||||
enhance,
|
||||
clearable,
|
||||
mobileUI,
|
||||
translate: __
|
||||
translate: __,
|
||||
loading,
|
||||
loadingConfig
|
||||
} = this.props;
|
||||
const {isFocused, inputValue} = this.state;
|
||||
const {enable} = this.getHistoryOptions();
|
||||
@ -325,8 +336,26 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
|
||||
) : null}
|
||||
|
||||
{!mini ? (
|
||||
<a className={cx('SearchBox-searchBtn')} onClick={this.handleSearch}>
|
||||
<Icon icon="search" className="icon" />
|
||||
<a
|
||||
className={cx('SearchBox-searchBtn', {
|
||||
'SearchBox-searchBtn--loading': loading
|
||||
})}
|
||||
onClick={this.handleSearch}
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner
|
||||
classnames={cx}
|
||||
classPrefix={classPrefix}
|
||||
className={cx('SearchBox-spinner')}
|
||||
spinnerClassName={cx('SearchBox-spinner-icon')}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
icon="loading-outline"
|
||||
loadingConfig={loadingConfig}
|
||||
/>
|
||||
) : (
|
||||
<Icon icon="search" className="icon" />
|
||||
)}
|
||||
</a>
|
||||
) : active ? (
|
||||
<a className={cx('SearchBox-cancelBtn')} onClick={this.handleCancel}>
|
||||
|
@ -249,3 +249,82 @@ test('6. Renderer: Searchbox is not supposed to be triggered with composition in
|
||||
keywords: 'test'
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('Renderer:Searchbox with searchImediately & className', async () => {
|
||||
const onQuery = jest.fn();
|
||||
const {container} = render(
|
||||
amisRender({
|
||||
type: 'search-box',
|
||||
name: 'keywords',
|
||||
mini: true,
|
||||
searchImediately: true,
|
||||
className: 'testClass',
|
||||
onQuery
|
||||
})
|
||||
);
|
||||
|
||||
expect(container.querySelector('.cxd-SearchBox')).toHaveClass('testClass');
|
||||
|
||||
const input = container.querySelector('.cxd-SearchBox input')!;
|
||||
fireEvent.change(input, {
|
||||
target: {value: 'aa'}
|
||||
});
|
||||
|
||||
await wait(400);
|
||||
expect(onQuery).toBeCalledTimes(1);
|
||||
expect(onQuery.mock.calls[0][0]).toEqual({
|
||||
keywords: 'aa'
|
||||
});
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: {value: 'aabb'}
|
||||
});
|
||||
|
||||
await wait(400);
|
||||
expect(onQuery).toBeCalledTimes(2);
|
||||
expect(onQuery.mock.calls[1][0]).toEqual({
|
||||
keywords: 'aabb'
|
||||
});
|
||||
});
|
||||
|
||||
test('Renderer: Searchbox with disbaled', async () => {
|
||||
const onQuery = jest.fn();
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'search-box',
|
||||
name: 'keywords',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
onQuery
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const inputEl = container.querySelector('.cxd-SearchBox input')!;
|
||||
expect(inputEl).toBeInTheDocument();
|
||||
/** Input元素上存在disabled attribute */
|
||||
expect((inputEl.attributes as any).disabled).not.toEqual(undefined);
|
||||
expect(inputEl.getAttribute('disabled')).toEqual('');
|
||||
});
|
||||
|
||||
test('Renderer: Searchbox with loading', async () => {
|
||||
const onQuery = jest.fn();
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'search-box',
|
||||
name: 'keywords',
|
||||
loading: true
|
||||
},
|
||||
{
|
||||
onQuery
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const spinner = container.querySelector('.cxd-SearchBox .cxd-SearchBox-spinner');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
createObject,
|
||||
IScopedContext,
|
||||
@ -5,13 +6,17 @@ import {
|
||||
RendererProps,
|
||||
resolveEventData,
|
||||
ScopedComponentType,
|
||||
ScopedContext
|
||||
ScopedContext,
|
||||
autobind,
|
||||
getPropValue,
|
||||
setVariable
|
||||
} from 'amis-core';
|
||||
import React from 'react';
|
||||
|
||||
import {BaseSchema, SchemaClassName} from '../Schema';
|
||||
import {SearchBox} from 'amis-ui';
|
||||
import {autobind, getPropValue, getVariable, setVariable} from 'amis-core';
|
||||
|
||||
import type {ListenerAction} from 'amis-core';
|
||||
import type {SpinnerExtraProps} from 'amis-ui';
|
||||
|
||||
/**
|
||||
* 搜索框渲染器
|
||||
@ -65,13 +70,18 @@ export interface SearchBoxSchema extends BaseSchema {
|
||||
* 是否开启清空内容后立即重新搜索
|
||||
*/
|
||||
clearAndSubmit?: boolean;
|
||||
|
||||
/** 是否处于加载状态 */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
interface SearchBoxProps
|
||||
extends RendererProps,
|
||||
Omit<SearchBoxSchema, 'type' | 'className'> {
|
||||
Omit<SearchBoxSchema, 'type' | 'className'>,
|
||||
SpinnerExtraProps {
|
||||
name: string;
|
||||
onQuery?: (query: {[propName: string]: string}) => any;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchBoxState {
|
||||
@ -199,18 +209,23 @@ export class SearchBoxRenderer extends React.Component<
|
||||
onChange,
|
||||
className,
|
||||
style,
|
||||
mobileUI
|
||||
mobileUI,
|
||||
loading,
|
||||
loadingConfig,
|
||||
onEvent
|
||||
} = this.props;
|
||||
|
||||
const value = this.state.value;
|
||||
/** 有可能通过Search事件处理 */
|
||||
const isDisabled = (!onQuery && !onEvent?.search) || disabled;
|
||||
|
||||
return (
|
||||
<SearchBox
|
||||
className={className}
|
||||
style={style}
|
||||
name={name}
|
||||
// disabled={!onQuery}
|
||||
disabled={disabled}
|
||||
disabled={isDisabled}
|
||||
loading={loading}
|
||||
loadingConfig={loadingConfig}
|
||||
defaultActive={!!value}
|
||||
defaultValue={onChange ? undefined : value}
|
||||
value={value}
|
||||
|
Loading…
Reference in New Issue
Block a user