mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:58:07 +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
|
```schema
|
||||||
@ -224,6 +379,8 @@ icon:
|
|||||||
| mini | `boolean` | | 是否为 mini 模式 |
|
| mini | `boolean` | | 是否为 mini 模式 |
|
||||||
| searchImediately | `boolean` | | 是否立即搜索 |
|
| searchImediately | `boolean` | | 是否立即搜索 |
|
||||||
| clearAndSubmit | `boolean` | | 清空搜索框内容后立即执行搜索 | `2.8.0` |
|
| 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-color: var(--colors-neutral-text-9);
|
||||||
--SearchBox-enhonce-disabled-search-color: var(--colors-neutral-text-6);
|
--SearchBox-enhonce-disabled-search-color: var(--colors-neutral-text-6);
|
||||||
--SearchBox-enhonce-clearable-gap: var(--borders-radius-4);
|
--SearchBox-enhonce-clearable-gap: var(--borders-radius-4);
|
||||||
|
--SearchBox-search-btn-color--disabled: var(--colors-neutral-fill-6);
|
||||||
|
|
||||||
--IconSelect-searchBox-width: #{px2rem(246px)};
|
--IconSelect-searchBox-width: #{px2rem(246px)};
|
||||||
--IconSelect-type-item-height: #{px2rem(48px)};
|
--IconSelect-type-item-height: #{px2rem(48px)};
|
||||||
|
@ -41,6 +41,11 @@
|
|||||||
&-searchBtn {
|
&-searchBtn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: #{px2rem(5px)} #{px2rem(10px)};
|
padding: #{px2rem(5px)} #{px2rem(10px)};
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-activeBtn,
|
&-activeBtn,
|
||||||
@ -65,7 +70,7 @@
|
|||||||
&.is-disabled &-activeBtn,
|
&.is-disabled &-activeBtn,
|
||||||
&.is-disabled &-searchBtn,
|
&.is-disabled &-searchBtn,
|
||||||
&.is-disabled &-cancelBtn {
|
&.is-disabled &-cancelBtn {
|
||||||
color: var(--icon-onDisabled-color);
|
color: var(--SearchBox-search-btn-color--disabled);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ import {autobind} from 'amis-core';
|
|||||||
import {LocaleProps, localeable} from 'amis-core';
|
import {LocaleProps, localeable} from 'amis-core';
|
||||||
import chain from 'lodash/chain';
|
import chain from 'lodash/chain';
|
||||||
import Input from './Input';
|
import Input from './Input';
|
||||||
|
import Spinner from './Spinner';
|
||||||
|
|
||||||
|
import {SpinnerExtraProps} from './Spinner';
|
||||||
|
|
||||||
export interface HistoryRecord {
|
export interface HistoryRecord {
|
||||||
/** 历史记录值 */
|
/** 历史记录值 */
|
||||||
@ -28,7 +31,10 @@ export interface SearchHistoryOptions {
|
|||||||
dropdownClassName?: string;
|
dropdownClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchBoxProps extends ThemeProps, LocaleProps {
|
export interface SearchBoxProps
|
||||||
|
extends ThemeProps,
|
||||||
|
LocaleProps,
|
||||||
|
SpinnerExtraProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
mini?: boolean;
|
mini?: boolean;
|
||||||
@ -42,13 +48,14 @@ export interface SearchBoxProps extends ThemeProps, LocaleProps {
|
|||||||
active?: boolean;
|
active?: boolean;
|
||||||
defaultActive?: boolean;
|
defaultActive?: boolean;
|
||||||
onActiveChange?: (active: boolean) => void;
|
onActiveChange?: (active: boolean) => void;
|
||||||
onSearch?: (value: string) => void;
|
onSearch?: (value: string) => any;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
/** 历史记录配置 */
|
/** 历史记录配置 */
|
||||||
history?: SearchHistoryOptions;
|
history?: SearchHistoryOptions;
|
||||||
clearAndSubmit?: boolean;
|
clearAndSubmit?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchBoxState {
|
export interface SearchBoxState {
|
||||||
@ -87,6 +94,7 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
|
|||||||
lazyEmitSearch = debounce(
|
lazyEmitSearch = debounce(
|
||||||
() => {
|
() => {
|
||||||
const onSearch = this.props.onSearch;
|
const onSearch = this.props.onSearch;
|
||||||
|
|
||||||
onSearch?.(this.state.inputValue ?? '');
|
onSearch?.(this.state.inputValue ?? '');
|
||||||
},
|
},
|
||||||
250,
|
250,
|
||||||
@ -276,6 +284,7 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
|
|||||||
renderInput(isHistoryMode?: boolean) {
|
renderInput(isHistoryMode?: boolean) {
|
||||||
const {
|
const {
|
||||||
classnames: cx,
|
classnames: cx,
|
||||||
|
classPrefix,
|
||||||
active,
|
active,
|
||||||
name,
|
name,
|
||||||
className,
|
className,
|
||||||
@ -286,7 +295,9 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
|
|||||||
enhance,
|
enhance,
|
||||||
clearable,
|
clearable,
|
||||||
mobileUI,
|
mobileUI,
|
||||||
translate: __
|
translate: __,
|
||||||
|
loading,
|
||||||
|
loadingConfig
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {isFocused, inputValue} = this.state;
|
const {isFocused, inputValue} = this.state;
|
||||||
const {enable} = this.getHistoryOptions();
|
const {enable} = this.getHistoryOptions();
|
||||||
@ -325,8 +336,26 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!mini ? (
|
{!mini ? (
|
||||||
<a className={cx('SearchBox-searchBtn')} onClick={this.handleSearch}>
|
<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" />
|
<Icon icon="search" className="icon" />
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
) : active ? (
|
) : active ? (
|
||||||
<a className={cx('SearchBox-cancelBtn')} onClick={this.handleCancel}>
|
<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'
|
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 {
|
import {
|
||||||
createObject,
|
createObject,
|
||||||
IScopedContext,
|
IScopedContext,
|
||||||
@ -5,13 +6,17 @@ import {
|
|||||||
RendererProps,
|
RendererProps,
|
||||||
resolveEventData,
|
resolveEventData,
|
||||||
ScopedComponentType,
|
ScopedComponentType,
|
||||||
ScopedContext
|
ScopedContext,
|
||||||
|
autobind,
|
||||||
|
getPropValue,
|
||||||
|
setVariable
|
||||||
} from 'amis-core';
|
} from 'amis-core';
|
||||||
import React from 'react';
|
|
||||||
import {BaseSchema, SchemaClassName} from '../Schema';
|
import {BaseSchema, SchemaClassName} from '../Schema';
|
||||||
import {SearchBox} from 'amis-ui';
|
import {SearchBox} from 'amis-ui';
|
||||||
import {autobind, getPropValue, getVariable, setVariable} from 'amis-core';
|
|
||||||
import type {ListenerAction} 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;
|
clearAndSubmit?: boolean;
|
||||||
|
|
||||||
|
/** 是否处于加载状态 */
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SearchBoxProps
|
interface SearchBoxProps
|
||||||
extends RendererProps,
|
extends RendererProps,
|
||||||
Omit<SearchBoxSchema, 'type' | 'className'> {
|
Omit<SearchBoxSchema, 'type' | 'className'>,
|
||||||
|
SpinnerExtraProps {
|
||||||
name: string;
|
name: string;
|
||||||
onQuery?: (query: {[propName: string]: string}) => any;
|
onQuery?: (query: {[propName: string]: string}) => any;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchBoxState {
|
export interface SearchBoxState {
|
||||||
@ -199,18 +209,23 @@ export class SearchBoxRenderer extends React.Component<
|
|||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
mobileUI
|
mobileUI,
|
||||||
|
loading,
|
||||||
|
loadingConfig,
|
||||||
|
onEvent
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const value = this.state.value;
|
const value = this.state.value;
|
||||||
|
/** 有可能通过Search事件处理 */
|
||||||
|
const isDisabled = (!onQuery && !onEvent?.search) || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchBox
|
<SearchBox
|
||||||
className={className}
|
className={className}
|
||||||
style={style}
|
style={style}
|
||||||
name={name}
|
name={name}
|
||||||
// disabled={!onQuery}
|
disabled={isDisabled}
|
||||||
disabled={disabled}
|
loading={loading}
|
||||||
|
loadingConfig={loadingConfig}
|
||||||
defaultActive={!!value}
|
defaultActive={!!value}
|
||||||
defaultValue={onChange ? undefined : value}
|
defaultValue={onChange ? undefined : value}
|
||||||
value={value}
|
value={value}
|
||||||
|
Loading…
Reference in New Issue
Block a user