mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
feat: InputRange组件min,max,step支持变量
This commit is contained in:
parent
b159a650e3
commit
13d9d6838e
@ -56,7 +56,7 @@ order: 38
|
||||
|
||||
## 控制调整的粒度
|
||||
|
||||
使用 `step` 可以控制调整粒度,默认是 1。
|
||||
使用 `step` 可以控制调整粒度,默认是 1。`3.3.0`版本后支持使用变量。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
@ -255,14 +255,14 @@ order: 38
|
||||
|
||||
当做选择器表单项使用时,除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
|
||||
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
||||
| className | `string` | | css 类名 |
|
||||
| value | `number` or `string` or `{min: number, max: number}` or `[number, number]` | | |
|
||||
| min | `number` | `0` | 最小值 |
|
||||
| max | `number` | `100` | 最大值 |
|
||||
| min | `number \| string` | `0` | 最小值,支持变量 | `3.3.0`后支持变量 |
|
||||
| max | `number \| string` | `100` | 最大, 支持变量值 | `3.3.0`后支持变量 |
|
||||
| disabled | `boolean` | `false` | 是否禁用 |
|
||||
| step | `number` | `1` | 步长 |
|
||||
| step | `number \| string` | `1` | 步长,支持变量 | `3.3.0`后支持变量 |
|
||||
| showSteps | `boolean` | `false` | 是否显示步长 |
|
||||
| parts | `number` or `number[]` | `1` | 分割的块数<br/>主持数组传入分块的节点 |
|
||||
| marks | <code>{ [number | string]: ReactNode }</code> or <code>{ [number | string]: { style: CSSProperties, label: ReactNode } }</code> | | 刻度标记<br/>- 支持自定义样式<br/>- 设置百分比 |
|
||||
|
@ -1,5 +1,306 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Renderer: range with min & max & step by variables 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="cxd-Page"
|
||||
>
|
||||
<div
|
||||
class="cxd-Page-content"
|
||||
>
|
||||
<div
|
||||
class="cxd-Page-main"
|
||||
>
|
||||
<div
|
||||
class="cxd-Page-body"
|
||||
role="page-body"
|
||||
>
|
||||
<div
|
||||
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Panel-heading"
|
||||
>
|
||||
<h3
|
||||
class="cxd-Panel-title"
|
||||
>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
表单
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Panel-body"
|
||||
>
|
||||
<form
|
||||
class="cxd-Form cxd-Form--normal"
|
||||
novalidate=""
|
||||
>
|
||||
<input
|
||||
style="display: none;"
|
||||
type="submit"
|
||||
/>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
滑块
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-RangeControl cxd-InputRange cxd-Form-control"
|
||||
>
|
||||
<div
|
||||
class="cxd-InputRange-wrap"
|
||||
>
|
||||
<div
|
||||
class="cxd-InputRange-track cxd-InputRange-track--background"
|
||||
>
|
||||
<div
|
||||
class="cxd-InputRange-track-active"
|
||||
style="width: 10%; left: 0%;"
|
||||
/>
|
||||
<div
|
||||
class="cxd-InputRange-handle"
|
||||
style="left: 10%; z-index: 1; position: relative;"
|
||||
>
|
||||
<div
|
||||
class="cxd-InputRange-handle-icon"
|
||||
>
|
||||
<icon-mock
|
||||
classname="icon icon-slider-handle"
|
||||
icon="slider-handle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-InputRange-label pos-top"
|
||||
style="position: relative;"
|
||||
>
|
||||
<span>
|
||||
12
|
||||
</span>
|
||||
<div
|
||||
class="resize-sensor"
|
||||
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-expand"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-shrink"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-appear"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resize-sensor"
|
||||
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-expand"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-shrink"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-appear"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-InputRange-input"
|
||||
>
|
||||
<div
|
||||
class="cxd-Number cxd-Number--borderFull"
|
||||
>
|
||||
<div
|
||||
class="cxd-Number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="cxd-Number-handler cxd-Number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
class="cxd-Number-handler-up-inner"
|
||||
unselectable="on"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="cxd-Number-handler cxd-Number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
class="cxd-Number-handler-down-inner"
|
||||
unselectable="on"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Number-input-wrap"
|
||||
>
|
||||
<input
|
||||
aria-valuemax="66"
|
||||
aria-valuemin="6"
|
||||
aria-valuenow="12"
|
||||
autocomplete="off"
|
||||
class="cxd-Number-input"
|
||||
role="spinbutton"
|
||||
step="2"
|
||||
value="12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="cxd-InputRange-clear is-active"
|
||||
>
|
||||
<icon-mock
|
||||
classname="icon icon-close"
|
||||
icon="close"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Panel-footerWrap"
|
||||
>
|
||||
<div
|
||||
class="cxd-Panel-btnToolbar cxd-Panel-footer"
|
||||
>
|
||||
<button
|
||||
class="cxd-Button cxd-Button--primary cxd-Button--size-default"
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
Submit
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resize-sensor"
|
||||
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-expand"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-shrink"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-appear"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Renderer:range with marks 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
@ -1,3 +1,15 @@
|
||||
/**
|
||||
* 组件名称:InputRange 滑块
|
||||
* 单测内容:
|
||||
* 01. showInput
|
||||
* 02. multiple & clearable & delimiter
|
||||
* 03. showSteps
|
||||
* 04. marks
|
||||
* 05. tooltipVisible & tooltipPlacement
|
||||
* 06. min & max & step & joinValues
|
||||
* 07. min & max & step 变量
|
||||
*/
|
||||
|
||||
import React = require('react');
|
||||
import {render, fireEvent, screen, waitFor} from '@testing-library/react';
|
||||
import '../../../src';
|
||||
@ -300,3 +312,84 @@ test('Renderer:range with min & max & step & joinValues', async () => {
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Renderer: range with min & max & step by variables', async () => {
|
||||
const onSubmit = jest.fn();
|
||||
const submitBtnText = 'Submit';
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'page',
|
||||
data: {
|
||||
min: '6',
|
||||
max: '66',
|
||||
step: '2'
|
||||
},
|
||||
body: {
|
||||
type: 'form',
|
||||
submitText: submitBtnText,
|
||||
api: '/api/mock2/form/saveForm',
|
||||
body: [
|
||||
{
|
||||
type: 'input-range',
|
||||
label: '滑块',
|
||||
name: 'range',
|
||||
min: '${min}',
|
||||
max: '${max}',
|
||||
step: '${step}',
|
||||
showInput: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{onSubmit},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
const inputs = container.querySelectorAll('.cxd-InputRange-input input');
|
||||
const inputEl = inputs[0];
|
||||
const calculatePercentage = (value: number) => ((value - 6) / (66 - 6)) * 100;
|
||||
/** min & max 正常解析 */
|
||||
fireEvent.change(inputEl, {target: {value: 88}});
|
||||
fireEvent.blur(inputEl);
|
||||
await wait(200);
|
||||
expect(
|
||||
(
|
||||
container.querySelector('.cxd-InputRange-track-active') as Element
|
||||
).getAttribute('style')
|
||||
).toContain(`width: 100%; left: 0%;`);
|
||||
await wait(200);
|
||||
fireEvent.change(inputEl, {target: {value: 0}});
|
||||
fireEvent.blur(inputEl);
|
||||
await wait(200);
|
||||
expect(
|
||||
(
|
||||
container.querySelector('.cxd-InputRange-track-active') as Element
|
||||
).getAttribute('style')
|
||||
).toContain(`width: 0%; left: 0%;`);
|
||||
/** step正常解析 */
|
||||
fireEvent.change(inputEl, {target: {value: 12}});
|
||||
fireEvent.blur(inputEl);
|
||||
await wait(200);
|
||||
expect(
|
||||
(
|
||||
container.querySelector('.cxd-InputRange-track-active') as Element
|
||||
).getAttribute('style')
|
||||
).toContain(`width: ${calculatePercentage(12)}%; left: 0%;`);
|
||||
/** 提交参数 */
|
||||
await wait(500);
|
||||
const submitBtn = screen.getByRole('button', {name: submitBtnText});
|
||||
await waitFor(() => {
|
||||
expect(submitBtn).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(submitBtn);
|
||||
await wait(500);
|
||||
const formData = onSubmit.mock.calls[0][0];
|
||||
expect(onSubmit).toHaveBeenCalled();
|
||||
expect(formData).toEqual({
|
||||
range: 12
|
||||
});
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
@ -12,12 +12,16 @@ import {
|
||||
stripNumber,
|
||||
filter,
|
||||
ActionObject,
|
||||
isMobile
|
||||
isMobile,
|
||||
isPureVariable,
|
||||
resolveVariableAndFilter
|
||||
} from 'amis-core';
|
||||
import {Range as InputRange, NumberInput, Icon} from 'amis-ui';
|
||||
import {FormBaseControlSchema, SchemaObject} from '../../Schema';
|
||||
import {supportStatic} from './StaticHoc';
|
||||
|
||||
import type {SchemaTokenizeableString} from '../../Schema';
|
||||
|
||||
/**
|
||||
* Range
|
||||
* 文档:https://aisuda.bce.baidu.com/amis/zh-CN/components/form/range
|
||||
@ -39,17 +43,17 @@ export interface RangeControlSchema extends FormBaseControlSchema {
|
||||
/**
|
||||
* 最大值
|
||||
*/
|
||||
max?: number;
|
||||
max?: number | SchemaTokenizeableString;
|
||||
|
||||
/**
|
||||
* 最小值
|
||||
*/
|
||||
min?: number;
|
||||
min?: number | SchemaTokenizeableString;
|
||||
|
||||
/**
|
||||
* 步长
|
||||
*/
|
||||
step?: number;
|
||||
step?: number | SchemaTokenizeableString;
|
||||
|
||||
/**
|
||||
* 单位
|
||||
@ -131,17 +135,17 @@ export interface RangeProps extends FormControlProps {
|
||||
/**
|
||||
* 最小值
|
||||
*/
|
||||
min: number;
|
||||
min: number | SchemaTokenizeableString;
|
||||
|
||||
/**
|
||||
* 最大值
|
||||
*/
|
||||
max: number;
|
||||
max: number | SchemaTokenizeableString;
|
||||
|
||||
/**
|
||||
* 步长
|
||||
*/
|
||||
step: number;
|
||||
step: number | SchemaTokenizeableString;
|
||||
|
||||
/**
|
||||
* 是否展示步长
|
||||
@ -241,7 +245,11 @@ export interface DefaultProps {
|
||||
tooltipPlacement: TooltipPosType;
|
||||
}
|
||||
|
||||
export interface RangeItemProps extends RangeProps {
|
||||
export interface RangeItemProps
|
||||
extends Omit<RangeProps, 'min' | 'max' | 'step'> {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
value: FormatValue;
|
||||
onChange: (value: Value) => void;
|
||||
onAfterChange: () => void;
|
||||
@ -251,6 +259,29 @@ export interface RangeState {
|
||||
value: FormatValue;
|
||||
}
|
||||
|
||||
const resolveNumVariable = (
|
||||
value: number | string | undefined,
|
||||
data: Record<string, any> = {},
|
||||
fallback: number
|
||||
) => {
|
||||
if (typeof value === 'string') {
|
||||
value = isPureVariable(value)
|
||||
? resolveVariableAndFilter(value, data)
|
||||
: value;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const result = parseFloat(value);
|
||||
return isNaN(result) ? fallback : result;
|
||||
} else if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
} else if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value ?? fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化初始value值
|
||||
* @param value 初始value值 Value
|
||||
@ -336,7 +367,8 @@ export class Input extends React.Component<RangeItemProps, any> {
|
||||
* @returns
|
||||
*/
|
||||
getStepPrecision() {
|
||||
const {step} = this.props;
|
||||
const {step: rawStep, data} = this.props;
|
||||
const step = resolveNumVariable(rawStep, data, 1);
|
||||
const stepIsDecimal = /^\d+\.\d+$/.test(step.toString());
|
||||
return !stepIsDecimal || step < 0
|
||||
? 0
|
||||
@ -350,7 +382,8 @@ export class Input extends React.Component<RangeItemProps, any> {
|
||||
* @returns 处理之后数据
|
||||
*/
|
||||
getValue(value: string | number, type?: string) {
|
||||
const {max, min, step, value: stateValue} = this.props as RangeItemProps;
|
||||
const {min, max, step, value: stateValue} = this.props as RangeItemProps;
|
||||
|
||||
// value为null、undefined时,取对应的min/max
|
||||
value = value ?? (type === 'min' ? min : max);
|
||||
// 校正value为step的倍数
|
||||
@ -488,12 +521,13 @@ export default class RangeControl extends React.PureComponent<
|
||||
|
||||
constructor(props: RangeProps) {
|
||||
super(props);
|
||||
const {value: propsValue, multiple, delimiter, min, max} = this.props;
|
||||
const {value: propsValue, multiple, delimiter, min, max, data} = this.props;
|
||||
|
||||
const value = formatValue(propsValue, {
|
||||
multiple,
|
||||
delimiter,
|
||||
min,
|
||||
max
|
||||
min: resolveNumVariable(min, data, 0),
|
||||
max: resolveNumVariable(max, data, 0)
|
||||
});
|
||||
|
||||
this.state = {
|
||||
@ -502,26 +536,31 @@ export default class RangeControl extends React.PureComponent<
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: RangeProps) {
|
||||
const {value, min, max} = prevProps;
|
||||
const {value, min, max, data: prevData} = prevProps;
|
||||
const {
|
||||
value: nextPropsValue,
|
||||
multiple,
|
||||
delimiter,
|
||||
min: nextPropsMin,
|
||||
max: nextPropsMax,
|
||||
data,
|
||||
onChange
|
||||
} = this.props;
|
||||
const prevMin = resolveNumVariable(min, prevData, 0);
|
||||
const prevMax = resolveNumVariable(max, prevData, 100);
|
||||
const nextMin = resolveNumVariable(nextPropsMin, data, 0);
|
||||
const nextMax = resolveNumVariable(nextPropsMax, data, 100);
|
||||
|
||||
if (
|
||||
value !== nextPropsValue ||
|
||||
min !== nextPropsMin ||
|
||||
max !== nextPropsMax
|
||||
prevMin !== nextMin ||
|
||||
prevMax !== nextMax
|
||||
) {
|
||||
const value = formatValue(nextPropsValue, {
|
||||
multiple,
|
||||
delimiter,
|
||||
min: nextPropsMin,
|
||||
max: nextPropsMax
|
||||
min: nextMin,
|
||||
max: nextMax
|
||||
});
|
||||
this.setState({
|
||||
value: this.getValue(value)
|
||||
@ -531,7 +570,6 @@ export default class RangeControl extends React.PureComponent<
|
||||
|
||||
doAction(action: ActionObject, data: object, throwErrors: boolean) {
|
||||
const actionType = action?.actionType as string;
|
||||
const {multiple, min, max} = this.props;
|
||||
|
||||
if (!!~['clear', 'reset'].indexOf(actionType)) {
|
||||
this.clearValue(actionType);
|
||||
@ -540,7 +578,10 @@ export default class RangeControl extends React.PureComponent<
|
||||
|
||||
@autobind
|
||||
clearValue(type: string = 'clear') {
|
||||
const {multiple, min, max, onChange} = this.props;
|
||||
const {multiple, min: rawMin, max: rawMax, data, onChange} = this.props;
|
||||
const min = resolveNumVariable(rawMin, data, 0);
|
||||
const max = resolveNumVariable(rawMax, data, 100);
|
||||
|
||||
let resetValue = this.props.resetValue;
|
||||
|
||||
if (type === 'clear') {
|
||||
@ -623,6 +664,10 @@ export default class RangeControl extends React.PureComponent<
|
||||
const {value} = this.state;
|
||||
const props: RangeItemProps = {
|
||||
...this.props,
|
||||
/** 解析变量,下面组件透传属性时使用 props 即可 */
|
||||
min: resolveNumVariable(this.props.min, this.props.data, 0),
|
||||
max: resolveNumVariable(this.props.max, this.props.data, 0),
|
||||
step: resolveNumVariable(this.props.step, this.props.data, 1),
|
||||
value,
|
||||
onChange: this.handleChange,
|
||||
onAfterChange: this.onAfterChange
|
||||
|
Loading…
Reference in New Issue
Block a user