diff --git a/site/sidebar.config.ts b/site/sidebar.config.ts index 3633767..dbeeb21 100644 --- a/site/sidebar.config.ts +++ b/site/sidebar.config.ts @@ -148,6 +148,12 @@ export default [ path: '/components/input', component: () => import('tdesign-web-components/input/README.md'), }, + { + title: 'InputNumber 数字输入框', + name: 'input-number', + path: '/components/input-number', + component: () => import('tdesign-web-components/input-number/README.md'), + }, { title: 'RangeInput 范围输入框', name: 'range-input', diff --git a/src/_util/lightDom.ts b/src/_util/lightDom.ts index b3d5312..61fae53 100644 --- a/src/_util/lightDom.ts +++ b/src/_util/lightDom.ts @@ -65,7 +65,7 @@ const buildLightDomCtor = (nodeCtor: ComponentConstructor) => { const cssList = getCssList(nodeCtor.css); cssList.forEach((style) => { - const preStyleSheet = parentElement.adoptedStyleSheets.find((item) => (item as any).styleStr === style); + const preStyleSheet = parentElement.adoptedStyleSheets?.find((item) => (item as any).styleStr === style); if (preStyleSheet) { return; } diff --git a/src/input-number/README.md b/src/input-number/README.md new file mode 100644 index 0000000..f6c040f --- /dev/null +++ b/src/input-number/README.md @@ -0,0 +1,99 @@ +--- +title: InputNumber 数字输入框 +description: 数字输入框由增加、减少按钮、数值输入组成。每次点击增加按钮(或减少按钮),数字增长(或减少)的量是恒定的。 +isComponent: true +usage: { title: '', description: '' } +spline: form +--- + +### 双侧调整的数字输入框 + +已输入的值居中展示,用户可直接在输入框内修改数值,还可以使用输入框左右的箭头按钮增大或减小数值。 + +{{ center }} + +### 右侧调整数值的数字输入框 + +已输入的值居左展示,用户可直接在输入框内修改数值,还可以使用输入框右侧的箭头按钮增大或减小数值。 + +{{ left }} + +### 无按钮的数字输入框 + +仅有输入框,不能用按钮进行数值调整的数字输入框。 + +{{ normal }} + +### 带小数的数字输入框 + +可以通过 `decimalPlaces` 来设置小数保留精度,通过 `step` 来设置步进。 + +{{ step }} + +### 可格式化的数字输入框 + +通过 `format` 属性格式化数值内容。 + +{{ format }} + +### 不同尺寸的数字输入框 + +提供 大、中(默认)、小 3 种数字输入框。 + +{{ size }} + +### 不同状态的数字输入框 + +除了禁用 `disabled` 和只读 `readonly` 状态之外,提供 正常(默认)、成功 `success`、警告 `warning`、错误 `error` 4 种状态的输入框设置。 + +{{ status }} + +### 不同对齐方式的输入框 + +{{ align }} + +### 自适应宽度的输入框 + +{{ auto-width }} + +### 大数字输入框 + +{{ large-number }} + + +## API +### InputNumber Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式 | N +align | String | - | 文本内容位置,居左/居中/居右。可选项:left/center/right | N +allowInputOverLimit | Boolean | true | 是否允许输入超过 `max` `min` 范围外的数字。为保障用户体验,仅在失去焦点时进行数字范围矫正。默认允许超出,数字超出范围时,输入框变红提醒 | N +autoWidth | Boolean | false | 宽度随内容自适应 | N +decimalPlaces | Number | undefined | [小数位数](https://en.wiktionary.org/wiki/decimal_place) | N +disabled | Boolean | - | 禁用组件 | N +format | Function | - | 格式化输入框展示值。第二个事件参数 `context.fixedNumber` 表示处理过小数位数 `decimalPlaces` 的数字。TS 类型:`(value: InputNumberValue, context?: { fixedNumber?: InputNumberValue }) => InputNumberValue` | N +inputProps | Object | - | 透传 Input 输入框组件全部属性。TS 类型:`InputProps`,[Input API Documents](./input?tab=api)。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/input-number/type.ts) | N +label | TNode | - | 左侧文本。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +largeNumber | Boolean | false | 是否作为大数使用。JS 支持的最大数字位数是 16 位,超过 16 位的数字需作为字符串大数处理。此时,数据类型必须保持为字符串,否则会丢失数据 | N +max | String / Number | Infinity | 最大值。如果是大数,请传入字符串。TS 类型:`InputNumberValue` | N +min | String / Number | -Infinity | 最小值。如果是大数,请传入字符串。TS 类型:`InputNumberValue` | N +placeholder | String | undefined | 占位符 | N +readonly | Boolean | false | 只读状态 | N +size | String | medium | 组件尺寸。可选项:small/medium/large | N +status | String | default | 文本框状态。可选项:default/success/warning/error | N +step | String / Number | 1 | 数值改变步数,可以是小数。如果是大数,请保证数据类型为字符串。TS 类型:`InputNumberValue` | N +suffix | TNode | - | 后置内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +theme | String | row | 按钮布局。可选项:column/row/normal | N +tips | TNode | - | 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +value | String / Number | - | 数字输入框的值。当值为 '' 时,输入框显示为空。TS 类型:`T` `type InputNumberValue = number \| string`。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/input-number/type.ts) | N +defaultValue | String / Number | - | 数字输入框的值。当值为 '' 时,输入框显示为空。非受控属性。TS 类型:`T` `type InputNumberValue = number \| string`。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/input-number/type.ts) | N +onBlur | Function | | TS 类型:`(value: InputNumberValue, context: { e: FocusEvent }) => void`
失去焦点时触发 | N +onChange | Function | | TS 类型:`(value: T, context: ChangeContext) => void`
值变化时触发,`type` 表示触发本次变化的来源。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/input-number/type.ts)。
`interface ChangeContext { type: ChangeSource; e: InputEvent \| MouseEvent \| FocusEvent \| KeyboardEvent \| CompositionEvent }`

`type ChangeSource = 'add' \| 'reduce' \| 'input' \| 'blur' \| 'enter' \| 'clear' \| 'props'`
| N +onEnter | Function | | TS 类型:`(value: InputNumberValue, context: { e: KeyboardEvent }) => void`
回车键按下时触发 | N +onFocus | Function | | TS 类型:`(value: InputNumberValue, context: { e: FocusEvent }) => void`
获取焦点时触发 | N +onKeydown | Function | | TS 类型:`(value: InputNumberValue, context: { e: KeyboardEvent }) => void`
键盘按下时触发 | N +onKeypress | Function | | TS 类型:`(value: InputNumberValue, context: { e: KeyboardEvent }) => void`
按下字符键时触发(keydown -> keypress -> keyup) | N +onKeyup | Function | | TS 类型:`(value: InputNumberValue, context: { e: KeyboardEvent }) => void`
释放键盘时触发 | N +onValidate | Function | | TS 类型:`(context: { error?: 'exceed-maximum' \| 'below-minimum' }) => void`
最大值或最小值校验结束后触发,`exceed-maximum` 表示超出最大值,`below-minimum` 表示小于最小值 | N diff --git a/src/input-number/_example/align.tsx b/src/input-number/_example/align.tsx new file mode 100644 index 0000000..0e6cf53 --- /dev/null +++ b/src/input-number/_example/align.tsx @@ -0,0 +1,24 @@ +import 'tdesign-web-components/input-number'; +import 'tdesign-web-components/space'; + +import { Component } from 'omi'; + +export default class InputNumberAlignDemo extends Component { + render() { + return ( + + + + + + + + + + + + + + ); + } +} diff --git a/src/input-number/_example/auto-width.tsx b/src/input-number/_example/auto-width.tsx new file mode 100644 index 0000000..47b72d3 --- /dev/null +++ b/src/input-number/_example/auto-width.tsx @@ -0,0 +1,9 @@ +import 'tdesign-web-components/input-number'; + +import { Component } from 'omi'; + +export default class InputNumberAutoWidthDemo extends Component { + render() { + return ; + } +} diff --git a/src/input-number/_example/center.tsx b/src/input-number/_example/center.tsx new file mode 100644 index 0000000..dcb7b7a --- /dev/null +++ b/src/input-number/_example/center.tsx @@ -0,0 +1,85 @@ +import 'tdesign-web-components/input-number'; +import 'tdesign-web-components/space'; + +import { Component, signal } from 'omi'; + +export default class InputNumberCenterDemo extends Component { + value1 = signal(''); + + value2 = signal(100); + + decimalValue = signal(3.41); + + error = signal(undefined); + + get tips() { + if (this.error.value === 'exceed-maximum') { + return 'number can not be exceed maximum'; + } + if (this.error.value === 'below-minimum') { + return 'number can not be below minimum'; + } + return undefined; + } + + render() { + return ( + + { + this.decimalValue.value = value; + }} + max={5} + autoWidth + /> + + { + console.log('onchange', value); + this.value1.value = value; + }} + step={0.18} + max={5} + allowInputOverLimit={false} + style={{ width: 250 }} + /> + + { + this.value2.value = value; + console.info('change', value, ctx); + }} + max={15} + min={-2} + inputProps={{ tips: this.tips }} + suffix="个" + style={{ width: 300 }} + onValidate={({ error }) => { + this.error.value = error; + }} + onBlur={(v, ctx) => { + console.log('blur', v, ctx); + }} + onFocus={(v, ctx) => { + console.log('focus', v, ctx); + }} + onEnter={(v, ctx) => { + console.log('enter', v, ctx); + }} + onKeydown={(v, ctx) => { + console.info('keydown', v, ctx); + }} + onKeyup={(v, ctx) => { + console.info('keyup', v, ctx); + }} + onKeypress={(v, ctx) => { + console.info('keypress', v, ctx); + }} + /> + + ); + } +} diff --git a/src/input-number/_example/format.tsx b/src/input-number/_example/format.tsx new file mode 100644 index 0000000..ca3f873 --- /dev/null +++ b/src/input-number/_example/format.tsx @@ -0,0 +1,38 @@ +import 'tdesign-web-components/input-number'; +import 'tdesign-web-components/space'; + +import { Component, signal } from 'omi'; + +export default class InputNumberFormatDemo extends Component { + value = signal(0); + + value1 = signal(0); + + render() { + return ( + + { + this.value.value = value; + }} + max={15} + min={-12} + step={1.2} + format={(value) => `${value} %`} + style={{ width: 250 }} + /> + + `${fixedNumber} %`} + value={this.value1.value} + onChange={(value) => { + this.value1.value = value; + }} + style={{ width: 250 }} + /> + + ); + } +} diff --git a/src/input-number/_example/large-number.tsx b/src/input-number/_example/large-number.tsx new file mode 100644 index 0000000..a6e0b47 --- /dev/null +++ b/src/input-number/_example/large-number.tsx @@ -0,0 +1,22 @@ +import 'tdesign-web-components/input-number'; +import 'tdesign-web-components/space'; + +import { Component } from 'omi'; + +export default class InputNumberCenterDemo extends Component { + render() { + return ( + + + + + + ); + } +} diff --git a/src/input-number/_example/left.tsx b/src/input-number/_example/left.tsx new file mode 100644 index 0000000..0c68927 --- /dev/null +++ b/src/input-number/_example/left.tsx @@ -0,0 +1,9 @@ +import 'tdesign-web-components/input-number'; + +import { Component } from 'omi'; + +export default class InputNumberLeftDemo extends Component { + render() { + return console.log(v)} />; + } +} diff --git a/src/input-number/_example/normal.tsx b/src/input-number/_example/normal.tsx new file mode 100644 index 0000000..dd50127 --- /dev/null +++ b/src/input-number/_example/normal.tsx @@ -0,0 +1,25 @@ +import 'tdesign-web-components/input-number'; +import 'tdesign-web-components/space'; + +import { Component } from 'omi'; + +export default class InputNumberNormalDemo extends Component { + render() { + return ( + + + + + + 金额:} + suffix={} + /> + + ); + } +} diff --git a/src/input-number/_example/size.tsx b/src/input-number/_example/size.tsx new file mode 100644 index 0000000..b656271 --- /dev/null +++ b/src/input-number/_example/size.tsx @@ -0,0 +1,30 @@ +import 'tdesign-web-components/input-number'; +import 'tdesign-web-components/space'; + +import { Component } from 'omi'; + +export default class InputNumberSizeDemo extends Component { + render() { + return ( + + + + + + + + + + + + + + + + + + + + ); + } +} diff --git a/src/input-number/_example/status.tsx b/src/input-number/_example/status.tsx new file mode 100644 index 0000000..60e35cf --- /dev/null +++ b/src/input-number/_example/status.tsx @@ -0,0 +1,98 @@ +import 'tdesign-web-components/input-number'; +import 'tdesign-web-components/radio'; +import 'tdesign-web-components/space'; + +import { Component, signal } from 'omi'; + +type AlignType = 'hide' | 'align-left' | 'align-input'; + +export default class InputNumberCenterDemo extends Component { + type = signal('align-input'); + + render() { + return ( + + (this.type.value = val)} + variant="default-filled" + > + 隐藏文本提示 + 文本提示左对齐 + 文本提示对齐输入框 + + + {this.type.value === 'hide' && ( + + + 禁用 + + + + 只读 + + + + 正常 + + + + 成功 + + + + 警告 + + + + 错误 + + + + )} + + {this.type.value === 'align-left' && ( + + + 正常提示 + + + + 成功提示 + + + + 警告提示 + + + + 错误提示 + + + + )} + + {this.type.value === 'align-input' && ( + + + 正常提示 + + + + 成功提示 + + + + 警告提示 + + + + 错误提示 + + + + )} + + ); + } +} diff --git a/src/input-number/_example/step.tsx b/src/input-number/_example/step.tsx new file mode 100644 index 0000000..0e71646 --- /dev/null +++ b/src/input-number/_example/step.tsx @@ -0,0 +1,22 @@ +import 'tdesign-web-components/input-number'; + +import { Component, signal } from 'omi'; + +export default class InputNumberStepDemo extends Component { + value = signal(3.2); + + render() { + return ( + { + this.value.value = value; + }} + max={15} + min={-5} + step={1.2} + decimalPlaces={2} + /> + ); + } +} diff --git a/src/input-number/input-number.tsx b/src/input-number/input-number.tsx index 2aab8a5..4ea4b63 100644 --- a/src/input-number/input-number.tsx +++ b/src/input-number/input-number.tsx @@ -1,20 +1,456 @@ -// 临时 input-number +import 'tdesign-icons-web-components/esm/components/add'; +import 'tdesign-icons-web-components/esm/components/chevron-down'; +import 'tdesign-icons-web-components/esm/components/chevron-up'; +import 'tdesign-icons-web-components/esm/components/remove'; +import '../button'; import '../input'; -import { Component, tag } from 'omi'; +import { isEqual, pick } from 'lodash'; +import { bind, Component, createRef, OmiProps, signal, tag } from 'omi'; -import classname, { classPrefix } from '../_util/classname'; +import { + canAddNumber, + canInputNumber, + canReduceNumber, + canSetValue, + formatThousandths, + formatUnCompleteNumber, + getMaxOrMinValidateResult, + getStepValue, + InputNumberErrorType, + largeNumberToFixed, +} from '../_common/js/input-number/number'; +import classname, { getClassPrefix, getCommonClassName } from '../_util/classname'; +import { convertToLightDomNode } from '../_util/lightDom'; +import { StyledProps } from '../common'; +import { ChangeContext, InputNumberValue, TdInputNumberProps } from './type'; -export interface InputNumberProps { - align?: 'left' | 'center' | 'right'; -} +const classPrefix = getClassPrefix(); + +export interface InputNumberProps extends TdInputNumberProps, StyledProps {} @tag('t-input-number') -export default class InputNumber extends Component { - render(props) { +export default class InputNumber extends Component { + static css = ` + .${classPrefix}-input-number ${classPrefix}-button-light-dom.${classPrefix}-input-number__decrease, + .${classPrefix}-input-number ${classPrefix}-button-light-dom.${classPrefix}-input-number__increase { + border: none; + transition: none; + } + + .${classPrefix}-input-number button.${classPrefix}-input-number__decrease, + .${classPrefix}-input-number button.${classPrefix}-input-number__increase { + position: static; + } + `; + + static defaultProps = { + allowInputOverLimit: true, + autoWidth: false, + decimalPlaces: undefined, + largeNumber: false, + max: Infinity, + min: -Infinity, + placeholder: '请输入', + readonly: false, + size: 'medium', + status: 'default', + step: 1, + theme: 'row', + }; + + static propTypes = { + allowInputOverLimit: Boolean, + autoWidth: Boolean, + decimalPlaces: Number, + largeNumber: Number, + max: Number, + min: Number, + placeholder: String, + readonly: Boolean, + size: String, + status: String, + step: Number, + theme: String, + }; + + private inputRef = createRef(); + + private usedValue: InputNumberValue = ''; + + private error = signal(undefined); + + private userInput = signal(''); + + private get isControlled() { + return Reflect.has(this.props, 'value'); + } + + private get disabledReduce() { + const { disabled, min, largeNumber } = this.props; + return disabled || !canReduceNumber(this.usedValue, min, largeNumber); + } + + private get disabledAdd() { + const { disabled, max, largeNumber } = this.props; + return disabled || !canAddNumber(this.usedValue, max, largeNumber); + } + + private getUserInput(value: InputNumberValue) { + if (!value && value !== 0) { + return ''; + } + + const { decimalPlaces, largeNumber, format } = this.props; + let inputStr = value || value === 0 ? String(value) : ''; + + const num = formatUnCompleteNumber(inputStr, { + decimalPlaces, + largeNumber, + isToFixed: true, + }); + inputStr = num || num === 0 ? String(num) : ''; + if (format) { + inputStr = String(format(value, { fixedNumber: inputStr })); + } + return inputStr; + } + + @bind + private handleChange(value: InputNumberValue, context: ChangeContext) { + const preUsedValue = this.usedValue; + + if (!this.isControlled) { + this.usedValue = value; + } + this.props.onChange?.(value, context); + + const curUsedValue = this.usedValue; + + if (curUsedValue !== preUsedValue) { + this.handleUsedValueChange(); + this.handleValidate(); + } + + this.update(); + } + + @bind + private handleStepValue(op: 'add' | 'reduce') { + const { step, max, min, largeNumber } = this.props; + const newValue = getStepValue({ + op, + step, + max, + min, + lastValue: this.usedValue, + largeNumber, + }); + const overLimit = getMaxOrMinValidateResult({ + value: newValue, + largeNumber, + max, + min, + }); + return { + overLimit, + newValue, + }; + } + + @bind + private handleReduce(e: any) { + if (this.disabledReduce || this.props.readonly) { + return; + } + const r = this.handleStepValue('reduce'); + if (r.overLimit && !this.props.allowInputOverLimit) { + return; + } + this.handleChange(r.newValue, { type: 'reduce', e }); + } + + @bind + private handleAdd(e: any) { + if (this.disabledAdd || this.props.readonly) { + return; + } + const r = this.handleStepValue('add'); + if (r.overLimit && !this.props.allowInputOverLimit) { + return; + } + this.handleChange(r.newValue, { type: 'add', e }); + } + + @bind + private handleInputChange(inputValue: string) { + const { largeNumber } = this.props; + // 处理千分位 + const val = formatThousandths(inputValue); + + if (!canInputNumber(val, largeNumber)) { + return; + } + + this.userInput.value = val; + + if (largeNumber) { + this.handleChange(val, { type: 'input', e: new InputEvent('input') }); + return; + } + + if (canSetValue(String(val), Number(this.usedValue))) { + const newVal = val === '' ? undefined : Number(val); + this.handleChange(newVal, { type: 'input', e: new InputEvent('input') }); + } + } + + @bind + private handleKeydown(_: string, ctx: { e: KeyboardEvent }) { + const { key } = ctx.e; + + if (key === 'ArrowUp') { + this.handleAdd(ctx.e); + } + + if (key === 'ArrowDown') { + this.handleReduce(ctx.e); + } + + this.props.onKeydown?.(this.userInput.value, ctx); + } + + @bind + private handleKeyUp(_: string, ctx: { e: KeyboardEvent }) { + this.props.onKeyup?.(this.userInput.value, ctx); + } + + @bind + private handleKeyPress(_: string, ctx: { e: KeyboardEvent }) { + this.props.onKeypress?.(this.userInput.value, ctx); + } + + @bind + private handleEnter(value: string, ctx: { e: KeyboardEvent }) { + this.userInput.value = this.getUserInput(value); + + const { decimalPlaces, largeNumber } = this.props; + const newValue = formatUnCompleteNumber(value, { + decimalPlaces, + largeNumber, + }); + + if (newValue !== value && String(newValue) !== value) { + this.handleChange(newValue, { type: 'enter', e: ctx.e }); + } + + this.props.onEnter?.(newValue, ctx); + } + + @bind + private handleClear(ctx: { e: MouseEvent }) { + this.handleChange(undefined, { type: 'clear', e: ctx.e }); + this.userInput.value = ''; + } + + @bind + private handleFocus(_: string, ctx: { e: FocusEvent }) { + this.userInput.value = String(this.usedValue || ''); + this.props.onFocus?.(this.usedValue, ctx); + } + + @bind + private handleBlur(value: string, ctx: { e: FocusEvent }) { + const { min, max, largeNumber, allowInputOverLimit, decimalPlaces } = this.props; + + if (!allowInputOverLimit && value !== undefined) { + const r = getMaxOrMinValidateResult({ + value: this.usedValue, + largeNumber, + max, + min, + }); + if (r === 'below-minimum') { + this.handleChange(min, { type: 'blur', e: ctx.e }); + return; + } + if (r === 'exceed-maximum') { + this.handleChange(max, { type: 'blur', e: ctx.e }); + return; + } + } + const newValue = formatUnCompleteNumber(value, { + decimalPlaces, + largeNumber, + }); + + this.userInput.value = this.getUserInput(newValue); + if (newValue !== this.usedValue) { + this.handleChange(newValue, { type: 'blur', e: ctx.e }); + } + + this.props.onBlur?.(newValue, ctx); + } + + @bind + private handleValidate() { + if ([undefined, '', null].includes(this.usedValue as any)) { + return; + } + const { max, min, largeNumber } = this.props; + const error = getMaxOrMinValidateResult({ + value: this.usedValue, + max, + min, + largeNumber, + }); + this.error.value = error; + this.props.onValidate?.({ error }); + } + + @bind + private handleUsedValueChange() { + const inputValue = [undefined, null].includes(this.usedValue) ? '' : String(this.usedValue); + + const { largeNumber, decimalPlaces } = this.props; + + // userInput.value 为非合法数字,则表示用户正在输入,此时无需处理 + if (!largeNumber && !Number.isNaN(this.userInput.value)) { + if (parseFloat(this.userInput.value) !== this.usedValue) { + this.userInput.value = this.getUserInput(inputValue); + } + const fixedNumber = Number(largeNumberToFixed(inputValue, decimalPlaces, largeNumber)); + if ( + decimalPlaces !== undefined && + ![undefined, null].includes(this.usedValue) && + Number(fixedNumber) !== Number(this.usedValue) + ) { + this.handleChange(fixedNumber, { type: 'props', e: undefined }); + } + } + if (largeNumber) { + const tmpUserInput = this.getUserInput(inputValue); + this.userInput.value = tmpUserInput; + if ( + decimalPlaces !== undefined && + largeNumberToFixed(inputValue, decimalPlaces, largeNumber) !== this.usedValue + ) { + this.handleChange(tmpUserInput, { type: 'props', e: undefined }); + } + } + } + + install(): void { + this.usedValue = this.props.value || this.props.defaultValue; + this.handleUsedValueChange(); + this.handleValidate(); + } + + receiveProps( + props: InputNumberProps | OmiProps, + oldProps: InputNumberProps | OmiProps, + ) { + if ( + (this.isControlled && props.value !== oldProps.value) || + (Reflect.has(oldProps, 'value') && !this.isControlled) + ) { + this.usedValue = props.value; + this.handleUsedValueChange(); + this.handleValidate(); + } + + const value = pick(props, ['value', 'max', 'min', 'largeNumber', 'onValidate']); + const preValue = pick(oldProps, ['value', 'max', 'min', 'largeNumber', 'onValidate']); + if (!isEqual(value, preValue)) { + this.handleUsedValueChange(); + this.handleValidate(); + } + } + + render(props: OmiProps) { + const status = this.error.value ? 'error' : props.status; + + const { SIZE, STATUS } = getCommonClassName(); + + const reduceClasses = classname(`${classPrefix}-input-number__decrease`, { + [STATUS.disabled]: this.disabledReduce, + }); + + const addClasses = classname(`${classPrefix}-input-number__increase`, { [STATUS.disabled]: this.disabledAdd }); + + const addIcon = + props.theme === 'column' + ? convertToLightDomNode() + : convertToLightDomNode(); + + const reduceIcon = + props.theme === 'column' + ? convertToLightDomNode() + : convertToLightDomNode(); + return ( -
- +
+ {props.theme !== 'normal' && + convertToLightDomNode( + , + )} + + {props.theme !== 'normal' && + convertToLightDomNode( + , + )} + + {props.tips && ( +
+ {props.tips} +
+ )}
); } diff --git a/src/input-number/type.ts b/src/input-number/type.ts new file mode 100644 index 0000000..98a89a0 --- /dev/null +++ b/src/input-number/type.ts @@ -0,0 +1,140 @@ +import { TNode } from '../common'; +import { InputProps } from '../input/input'; + +export interface TdInputNumberProps { + /** + * 文本内容位置,居左/居中/居右 + */ + align?: 'left' | 'center' | 'right'; + /** + * 是否允许输入超过 `max` `min` 范围外的数字。为保障用户体验,仅在失去焦点时进行数字范围矫正。默认允许超出,数字超出范围时,输入框变红提醒 + * @default true + */ + allowInputOverLimit?: boolean; + /** + * 宽度随内容自适应 + * @default false + */ + autoWidth?: boolean; + /** + * [小数位数](https://en.wiktionary.org/wiki/decimal_place) + */ + decimalPlaces?: number; + /** + * 禁用组件 + */ + disabled?: boolean; + /** + * 格式化输入框展示值。第二个事件参数 `context.fixedNumber` 表示处理过小数位数 `decimalPlaces` 的数字 + */ + format?: (value: InputNumberValue, context?: { fixedNumber?: InputNumberValue }) => InputNumberValue; + /** + * 透传 Input 输入框组件全部属性 + */ + inputProps?: InputProps; + /** + * 左侧文本 + */ + label?: TNode; + /** + * 是否作为大数使用。JS 支持的最大数字位数是 16 位,超过 16 位的数字需作为字符串大数处理。此时,数据类型必须保持为字符串,否则会丢失数据 + * @default false + */ + largeNumber?: boolean; + /** + * 最大值。如果是大数,请传入字符串 + * @default Infinity + */ + max?: InputNumberValue; + /** + * 最小值。如果是大数,请传入字符串 + * @default -Infinity + */ + min?: InputNumberValue; + /** + * 占位符 + */ + placeholder?: string; + /** + * 只读状态 + * @default false + */ + readonly?: boolean; + /** + * 组件尺寸 + * @default medium + */ + size?: 'small' | 'medium' | 'large'; + /** + * 文本框状态 + * @default default + */ + status?: 'default' | 'success' | 'warning' | 'error'; + /** + * 数值改变步数,可以是小数。如果是大数,请保证数据类型为字符串 + * @default 1 + */ + step?: InputNumberValue; + /** + * 后置内容 + */ + suffix?: TNode; + /** + * 按钮布局 + * @default row + */ + theme?: 'column' | 'row' | 'normal'; + /** + * 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式 + */ + tips?: TNode; + /** + * 数字输入框的值。当值为 '' 时,输入框显示为空 + */ + value?: T; + /** + * 数字输入框的值。当值为 '' 时,输入框显示为空,非受控属性 + */ + defaultValue?: T; + /** + * 失去焦点时触发 + */ + onBlur?: (value: InputNumberValue, context: { e: FocusEvent }) => void; + /** + * 值变化时触发,`type` 表示触发本次变化的来源 + */ + onChange?: (value: T, context: ChangeContext) => void; + /** + * 回车键按下时触发 + */ + onEnter?: (value: InputNumberValue, context: { e: KeyboardEvent }) => void; + /** + * 获取焦点时触发 + */ + onFocus?: (value: InputNumberValue, context: { e: FocusEvent }) => void; + /** + * 键盘按下时触发 + */ + onKeydown?: (value: InputNumberValue, context: { e: KeyboardEvent }) => void; + /** + * 按下字符键时触发(keydown -> keypress -> keyup) + */ + onKeypress?: (value: InputNumberValue, context: { e: KeyboardEvent }) => void; + /** + * 释放键盘时触发 + */ + onKeyup?: (value: InputNumberValue, context: { e: KeyboardEvent }) => void; + /** + * 最大值或最小值校验结束后触发,`exceed-maximum` 表示超出最大值,`below-minimum` 表示小于最小值 + */ + onValidate?: (context: { error?: 'exceed-maximum' | 'below-minimum' }) => void; +} + +export type InputNumberValue = number | string; + +export interface ChangeContext { + type: ChangeSource; + e: InputEvent | MouseEvent | FocusEvent | KeyboardEvent | CompositionEvent; +} + +export type ChangeSource = 'add' | 'reduce' | 'input' | 'blur' | 'enter' | 'clear' | 'props';