@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
500 lines • 19 kB
JavaScript
import _noop from "lodash/noop";
import _isString from "lodash/isString";
import _isNaN from "lodash/isNaN";
var __rest = this && this.__rest || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
}
return t;
};
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Input from '../input';
import { forwardStatics } from '@douyinfe/semi-foundation/lib/es/utils/object';
import isNullOrUndefined from '@douyinfe/semi-foundation/lib/es/utils/isNullOrUndefined';
import isBothNaN from '@douyinfe/semi-foundation/lib/es/utils/isBothNaN';
import InputNumberFoundation from '@douyinfe/semi-foundation/lib/es/inputNumber/foundation';
import BaseComponent from '../_base/baseComponent';
import { cssClasses, numbers, strings } from '@douyinfe/semi-foundation/lib/es/inputNumber/constants';
import { IconChevronUp, IconChevronDown } from '@douyinfe/semi-icons';
import '@douyinfe/semi-foundation/lib/es/inputNumber/inputNumber.css';
import LocaleConsumer from '../locale/localeConsumer';
class InputNumber extends BaseComponent {
get adapter() {
var _this = this;
return Object.assign(Object.assign({}, super.adapter), {
setValue: (value, cb) => this.setState({
value
}, cb),
setNumber: (number, cb) => this.setState({
number
}, cb),
setFocusing: (focusing, cb) => this.setState({
focusing
}, cb),
setHovering: hovering => this.setState({
hovering
}),
notifyChange: function () {
return _this.props.onChange(...arguments);
},
notifyNumberChange: function () {
return _this.props.onNumberChange(...arguments);
},
notifyBlur: e => this.props.onBlur(e),
notifyFocus: e => this.props.onFocus(e),
notifyUpClick: (value, e) => this.props.onUpClick(value, e),
notifyDownClick: (value, e) => this.props.onDownClick(value, e),
notifyKeyDown: e => this.props.onKeyDown(e),
registerGlobalEvent: (eventName, handler) => {
if (eventName && typeof handler === 'function') {
this.adapter.unregisterGlobalEvent(eventName);
this.adapter.setCache(eventName, handler);
document.addEventListener(eventName, handler);
}
},
unregisterGlobalEvent: eventName => {
if (eventName) {
const handler = this.adapter.getCache(eventName);
document.removeEventListener(eventName, handler);
this.adapter.setCache(eventName, null);
}
},
getInputCharacter: index => {
return this.inputNode.value[index];
},
recordCursorPosition: () => {
// Record position
try {
if (this.inputNode) {
this.cursorStart = this.inputNode.selectionStart;
this.cursorEnd = this.inputNode.selectionEnd;
this.currentValue = this.inputNode.value;
this.cursorBefore = this.inputNode.value.substring(0, this.cursorStart);
this.cursorAfter = this.inputNode.value.substring(this.cursorEnd);
}
} catch (e) {
console.warn(e);
// Fix error in Chrome:
// Failed to read the 'selectionStart' property from 'HTMLInputElement'
// http://stackoverflow.com/q/21177489/3040605
}
},
restoreByAfter: str => {
if (isNullOrUndefined(str)) {
return false;
}
const fullStr = this.inputNode.value;
const index = fullStr.lastIndexOf(str);
if (index === -1) {
return false;
}
if (index + str.length === fullStr.length) {
this.adapter.fixCaret(index, index);
return true;
}
return false;
},
restoreCursor: function () {
let str = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _this.cursorAfter;
if (isNullOrUndefined(str)) {
return false;
}
// For loop from full str to the str with last char to map. e.g. 123
// -> 123
// -> 23
// -> 3
return Array.prototype.some.call(str, (_, start) => {
const partStr = str.substring(start);
return _this.adapter.restoreByAfter(partStr);
});
},
fixCaret: (start, end) => {
if (start === undefined || end === undefined || !this.inputNode || !this.inputNode.value) {
return;
}
try {
const currentStart = this.inputNode.selectionStart;
const currentEnd = this.inputNode.selectionEnd;
if (start !== currentStart || end !== currentEnd) {
this.inputNode.setSelectionRange(start, end);
}
} catch (e) {
// Fix error in Chrome:
// Failed to read the 'selectionStart' property from 'HTMLInputElement'
// http://stackoverflow.com/q/21177489/3040605
}
},
setClickUpOrDown: value => {
this.clickUpOrDown = value;
},
updateStates: (states, callback) => {
this.setState(states, callback);
}
});
}
constructor(props) {
super(props);
this.setInputRef = node => {
const {
forwardedRef
} = this.props;
this.inputNode = node;
if (forwardedRef && typeof forwardedRef === 'object') {
forwardedRef.current = node;
} else if (typeof forwardedRef === 'function') {
forwardedRef(node);
}
};
this.handleInputFocus = e => this.foundation.handleInputFocus(e);
this.handleInputChange = (value, event) => this.foundation.handleInputChange(value, event);
this.handleInputBlur = e => this.foundation.handleInputBlur(e);
this.handleInputKeyDown = e => this.foundation.handleInputKeyDown(e);
this.handleInputMouseEnter = e => this.foundation.handleInputMouseEnter(e);
this.handleInputMouseLeave = e => this.foundation.handleInputMouseLeave(e);
this.handleInputMouseMove = e => this.foundation.handleInputMouseMove(e);
this.handleUpClick = e => this.foundation.handleUpClick(e);
this.handleDownClick = e => this.foundation.handleDownClick(e);
this.handleMouseUp = e => this.foundation.handleMouseUp(e);
this.handleMouseLeave = e => this.foundation.handleMouseLeave(e);
this.renderButtons = () => {
const {
prefixCls,
disabled,
innerButtons,
max,
min
} = this.props;
const {
hovering,
focusing,
number
} = this.state;
const notAllowedUp = disabled ? disabled : number === max;
const notAllowedDown = disabled ? disabled : number === min;
const suffixChildrenCls = classnames(`${prefixCls}-number-suffix-btns`, {
[`${prefixCls}-number-suffix-btns-inner`]: innerButtons,
[`${prefixCls}-number-suffix-btns-inner-hover`]: innerButtons && hovering && !focusing
});
const upClassName = classnames(`${prefixCls}-number-button`, `${prefixCls}-number-button-up`, {
[`${prefixCls}-number-button-up-disabled`]: disabled,
[`${prefixCls}-number-button-up-not-allowed`]: notAllowedUp
});
const downClassName = classnames(`${prefixCls}-number-button`, `${prefixCls}-number-button-down`, {
[`${prefixCls}-number-button-down-disabled`]: disabled,
[`${prefixCls}-number-button-down-not-allowed`]: notAllowedDown
});
return /*#__PURE__*/React.createElement("div", {
className: suffixChildrenCls
}, /*#__PURE__*/React.createElement("span", {
className: upClassName,
onMouseDown: notAllowedUp ? _noop : this.handleUpClick,
onMouseUp: this.handleMouseUp,
onMouseLeave: this.handleMouseLeave
}, /*#__PURE__*/React.createElement(IconChevronUp, {
size: "extra-small"
})), /*#__PURE__*/React.createElement("span", {
className: downClassName,
onMouseDown: notAllowedDown ? _noop : this.handleDownClick,
onMouseUp: this.handleMouseUp,
onMouseLeave: this.handleMouseLeave
}, /*#__PURE__*/React.createElement(IconChevronDown, {
size: "extra-small"
})));
};
this.renderSuffix = () => {
const {
innerButtons,
suffix
} = this.props;
const {
hovering,
focusing
} = this.state;
if (innerButtons && (hovering || focusing)) {
const buttons = this.renderButtons();
return buttons;
}
return suffix;
};
this.state = {
value: '',
number: null,
focusing: Boolean(props.autofocus) || false,
hovering: false
};
this.inputNode = null;
this.foundation = new InputNumberFoundation(this.adapter);
this.clickUpOrDown = false;
}
componentDidUpdate(prevProps) {
const {
value,
preventScroll
} = this.props;
const {
focusing
} = this.state;
let newValue;
/**
* To determine whether the front and back are equal
* NaN need to check whether both are NaN
*/
if (value !== prevProps.value && !isBothNaN(value, prevProps.value)) {
if (isNullOrUndefined(value) || value === '') {
newValue = '';
this.foundation.updateStates({
value: newValue,
number: null
});
} else {
let valueStr = value;
if (typeof value === 'number') {
valueStr = this.foundation.doFormat(value);
}
const parsedNum = this.foundation.doParse(valueStr, false, true, true);
const toNum = typeof value === 'number' ? value : this.foundation.doParse(valueStr, false, false, false);
/**
* focusing 状态为输入状态,输入状态的受控值要特殊处理
* 如:
* - 输入合法值
* 123 => input value 也应该是 123,同时需要设置 number 为 123
* - 输入非法值,只设置 input value,不设置非法的number
* abc => input value 这时是 abc,但失焦后会进行格式化
* 100(超出范围) => input value 应该是 100,但不设置 number
*
* 保持输入态有三种方式
* 1. 输入框输入
* - 输入可以解析为合法数字,input value根据输入值确定,失焦时更新input value
* - 输入不可解析为合法数字,进行格式化后显示在input框
* 2. 键盘点击上下按钮(input value根据受控值进行更改)
* 3. keepFocus+鼠标点击上下按钮(input value根据受控值进行更改)
*
* The focusing state is the input state, and the controlled value of the input state needs special treatment
* For example:
* - input legal value
* 123 = > input value should also be 123, and the number should be set to 123
* - input illegal value, only set the input value, do not set the illegal number
* abc = > input value This is abc at this time, but it will be formatted after being out of focus
* 100 (out of range) = > input value should be 100, but no number
*
* There are three ways to maintain the input state
* 1. input box input
* - input can be resolved into legal numbers, input value is determined according to the input value, and input value is updated when out of focus
* - input cannot be resolved into legal numbers, and it will be displayed in the input box after formatting
* 2. Keyboard click on the up and down button (input value is changed according to the controlled value)
* 3.keepFocus + mouse click on the up and down button (input value is changed according to the controlled value)
*/
if (focusing) {
if (this.foundation.isValidNumber(parsedNum) && parsedNum !== this.state.number) {
const obj = {
number: parsedNum
};
/**
* If you are clicking the button, it will automatically format once
* We need to set the status to false after trigger focus event
*/
if (this.clickUpOrDown) {
obj.value = this.foundation.doFormat(obj.number, true);
newValue = obj.value;
}
this.foundation.updateStates(obj, () => this.adapter.restoreCursor());
} else if (!_isNaN(toNum)) {
// Update input content when controlled input is illegal and not NaN
newValue = this.foundation.doFormat(toNum, false);
this.foundation.updateStates({
value: newValue
});
} else {
// Update input content when controlled input NaN
this.foundation.updateStates({
value: valueStr
});
}
} else if (this.foundation.isValidNumber(parsedNum)) {
newValue = this.foundation.doFormat(parsedNum, true, true);
this.foundation.updateStates({
number: parsedNum,
value: newValue
});
} else {
// Invalid digital analog blurring effect instead of controlled failure
newValue = '';
this.foundation.updateStates({
number: null,
value: newValue
});
}
}
if (newValue && _isString(newValue) && newValue !== String(this.props.value)) {
if (this.foundation._isCurrency()) {
// 仅在解析后的数值而不是格式化的字符串变化时 notifyChange
// notifyChange only when the parsed value changes, not the formatted string
const parsedNewValue = this.foundation.doParse(newValue);
const parsedPropValue = typeof this.props.value === 'string' ? this.foundation.doParse(this.props.value) : this.props.value;
if (parsedNewValue !== parsedPropValue) {
this.foundation.notifyChange(newValue, null);
}
} else {
this.foundation.notifyChange(newValue, null);
}
}
}
if (!this.clickUpOrDown) {
return;
}
if (this.props.keepFocus && this.state.focusing) {
if (document.activeElement !== this.inputNode) {
this.inputNode.focus({
preventScroll
});
}
}
}
render() {
const _a = this.props,
{
disabled,
className,
prefixCls,
min,
max,
step,
shiftStep,
precision,
formatter,
parser,
forwardedRef,
onUpClick,
onDownClick,
pressInterval,
pressTimeout,
suffix,
size,
hideButtons,
innerButtons,
style,
onNumberChange,
keepFocus,
defaultValue
} = _a,
rest = __rest(_a, ["disabled", "className", "prefixCls", "min", "max", "step", "shiftStep", "precision", "formatter", "parser", "forwardedRef", "onUpClick", "onDownClick", "pressInterval", "pressTimeout", "suffix", "size", "hideButtons", "innerButtons", "style", "onNumberChange", "keepFocus", "defaultValue"]);
const {
value,
number
} = this.state;
const inputNumberCls = classnames(className, `${prefixCls}-number`, {
[`${prefixCls}-number-size-${size}`]: size
});
const buttons = this.renderButtons();
const ariaProps = {
'aria-disabled': disabled,
step
};
if (number) {
ariaProps['aria-valuenow'] = number;
}
if (max !== Infinity) {
ariaProps['aria-valuemax'] = max;
}
if (min !== -Infinity) {
ariaProps['aria-valuemin'] = min;
}
const input = /*#__PURE__*/React.createElement("div", {
className: inputNumberCls,
style: style,
onMouseMove: e => this.handleInputMouseMove(e),
onMouseEnter: e => this.handleInputMouseEnter(e),
onMouseLeave: e => this.handleInputMouseLeave(e)
}, /*#__PURE__*/React.createElement(Input, Object.assign({
role: "spinbutton"
}, ariaProps, rest, {
size: size,
disabled: disabled,
ref: this.setInputRef,
value: value,
onFocus: this.handleInputFocus,
onChange: this.handleInputChange,
onBlur: this.handleInputBlur,
onKeyDown: this.handleInputKeyDown,
suffix: this.renderSuffix()
})), hideButtons || innerButtons ? null : buttons);
return input;
}
}
InputNumber.propTypes = {
'aria-label': PropTypes.string,
'aria-labelledby': PropTypes.string,
'aria-invalid': PropTypes.bool,
'aria-errormessage': PropTypes.string,
'aria-describedby': PropTypes.string,
'aria-required': PropTypes.bool,
autofocus: PropTypes.bool,
clearIcon: PropTypes.node,
className: PropTypes.string,
defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.bool,
formatter: PropTypes.func,
forwardedRef: PropTypes.any,
hideButtons: PropTypes.bool,
innerButtons: PropTypes.bool,
insetLabel: PropTypes.node,
insetLabelId: PropTypes.string,
keepFocus: PropTypes.bool,
max: PropTypes.number,
min: PropTypes.number,
parser: PropTypes.func,
precision: PropTypes.number,
prefixCls: PropTypes.string,
pressInterval: PropTypes.number,
pressTimeout: PropTypes.number,
preventScroll: PropTypes.bool,
shiftStep: PropTypes.number,
showCurrencySymbol: PropTypes.bool,
step: PropTypes.number,
style: PropTypes.object,
suffix: PropTypes.any,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
onBlur: PropTypes.func,
onChange: PropTypes.func,
onDownClick: PropTypes.func,
onKeyDown: PropTypes.func,
onNumberChange: PropTypes.func,
onUpClick: PropTypes.func
};
InputNumber.defaultProps = {
forwardedRef: _noop,
innerButtons: false,
keepFocus: false,
max: Infinity,
min: -Infinity,
prefixCls: cssClasses.PREFIX,
pressInterval: numbers.DEFAULT_PRESS_TIMEOUT,
pressTimeout: numbers.DEFAULT_PRESS_TIMEOUT,
shiftStep: numbers.DEFAULT_SHIFT_STEP,
showCurrencySymbol: true,
size: strings.DEFAULT_SIZE,
step: numbers.DEFAULT_STEP,
onBlur: _noop,
onChange: _noop,
onDownClick: _noop,
onFocus: _noop,
onKeyDown: _noop,
onNumberChange: _noop,
onUpClick: _noop
};
export default forwardStatics(/*#__PURE__*/React.forwardRef(function SemiInputNumber(props, ref) {
return /*#__PURE__*/React.createElement(LocaleConsumer, {
componentName: "InputNumber"
}, (locale, localeCode, dateFnsLocale, currency) => (/*#__PURE__*/React.createElement(InputNumber, Object.assign({
localeCode: localeCode,
defaultCurrency: currency
}, props, {
forwardedRef: ref
}))));
}), InputNumber);
export { InputNumber };