123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- /* eslint-disable @typescript-eslint/no-unused-vars */
- /* eslint-disable no-unused-vars */
- /* eslint-disable max-depth */
- /* eslint-disable react/no-did-update-set-state */
- /* eslint-disable max-len */
- import React from 'react';
- import PropTypes from 'prop-types';
- import classnames from 'classnames';
- import Input, { InputProps } from '../input';
- import { forwardStatics } from '@douyinfe/semi-foundation/utils/object';
- import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
- import isBothNaN from '@douyinfe/semi-foundation/utils/isBothNaN';
- import InputNumberFoundation, { InputNumberAdapter } from '@douyinfe/semi-foundation/inputNumber/foundation';
- import BaseComponent from '../_base/baseComponent';
- import { cssClasses, numbers, strings } from '@douyinfe/semi-foundation/inputNumber/constants';
- import { IconChevronUp, IconChevronDown } from '@douyinfe/semi-icons';
- import '@douyinfe/semi-foundation/inputNumber/inputNumber.scss';
- import { isNaN, noop } from 'lodash';
- import { ArrayElement } from '../_base/base';
- export interface InputNumberProps extends InputProps {
- autofocus?: boolean;
- className?: string;
- defaultValue?: number | string;
- disabled?: boolean;
- formatter?: (value: number | string) => string;
- forwardedRef?: React.MutableRefObject<HTMLInputElement> | ((instance: HTMLInputElement) => void);
- hideButtons?: boolean;
- innerButtons?: boolean;
- insetLabel?: React.ReactNode;
- insetLabelId?: string;
- keepFocus?: boolean;
- max?: number;
- min?: number;
- parser?: (value: string) => string;
- precision?: number;
- prefixCls?: string;
- pressInterval?: number;
- pressTimeout?: number;
- shiftStep?: number;
- showClear?: boolean;
- size?: ArrayElement<typeof strings.SIZE>;
- step?: number;
- style?: React.CSSProperties;
- suffix?: React.ReactNode;
- value?: number | string;
- onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
- onChange?: (value: number | string, e?: React.ChangeEvent) => void;
- onDownClick?: (value: string, e: React.MouseEvent<HTMLButtonElement>) => void;
- onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
- onKeyDown?: React.KeyboardEventHandler;
- onNumberChange?: (value: number, e?: React.ChangeEvent) => void;
- onUpClick?: (value: string, e: React.MouseEvent<HTMLButtonElement>) => void;
- }
- export interface InputNumberState {
- value?: number | string;
- number?: number | null; // Current parsed numbers
- focusing?: boolean;
- hovering?: boolean;
- }
- class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
- static 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,
- 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,
- shiftStep: PropTypes.number,
- 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,
- };
- static defaultProps: InputNumberProps = {
- disabled: false,
- 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,
- size: strings.DEFAULT_SIZE,
- step: numbers.DEFAULT_STEP,
- onBlur: noop,
- onChange: noop,
- onDownClick: noop,
- onFocus: noop,
- onKeyDown: noop,
- onNumberChange: noop,
- onUpClick: noop,
- };
- get adapter(): InputNumberAdapter {
- return {
- ...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: (...args) => this.props.onChange(...args),
- notifyNumberChange: (...args) => this.props.onNumberChange(...args),
- 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);
- }
- },
- 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: (str = 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, (_: any, start: number) => {
- 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;
- }
- };
- }
- inputNode: HTMLInputElement;
- clickUpOrDown: boolean;
- cursorStart!: number;
- cursorEnd!: number;
- currentValue!: number | string;
- cursorBefore!: string;
- cursorAfter!: string;
- foundation: InputNumberFoundation;
- constructor(props: InputNumberProps) {
- super(props);
- this.state = {
- value: '',
- number: null, // Current parsed numbers
- focusing: Boolean(props.autofocus) || false,
- hovering: false,
- };
- this.inputNode = null;
- this.foundation = new InputNumberFoundation(this.adapter);
- this.clickUpOrDown = false;
- }
- componentDidUpdate(prevProps: InputNumberProps) {
- const { value } = this.props;
- const { focusing } = this.state;
- /**
- * 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 === '') {
- this.setState({ value: '', number: null });
- } else {
- let valueStr = value;
- if (typeof value === 'number') {
- valueStr = value.toString();
- }
- 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?: number; value?: string } = { 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(valueStr, true);
- }
- this.setState(obj, () => this.adapter.restoreCursor());
- } else if (!isNaN(toNum)) {
- // Update input content when controlled input is illegal and not NaN
- this.setState({ value: this.foundation.doFormat(toNum, false) });
- } else {
- // Update input content when controlled input NaN
- this.setState({ value: this.foundation.doFormat(valueStr, false) });
- }
- } else if (this.foundation.isValidNumber(parsedNum)) {
- this.setState({ number: parsedNum, value: this.foundation.doFormat(parsedNum) });
- } else {
- // Invalid digital analog blurring effect instead of controlled failure
- this.setState({ number: null, value: '' });
- }
- }
- }
- if (!this.clickUpOrDown) {
- return;
- }
- if (this.props.keepFocus && this.state.focusing) {
- if (document.activeElement !== this.inputNode) {
- this.inputNode.focus();
- }
- }
- }
- setInputRef = (node: HTMLInputElement) => {
- const { forwardedRef } = this.props;
- this.inputNode = node;
- if (forwardedRef && typeof forwardedRef === 'object') {
- forwardedRef.current = node;
- } else if (typeof forwardedRef === 'function') {
- forwardedRef(node);
- }
- };
- handleInputFocus = (e: React.FocusEvent<HTMLInputElement>) => this.foundation.handleInputFocus(e);
- handleInputChange = (value: string, event: React.ChangeEvent<HTMLInputElement>) => this.foundation.handleInputChange(value, event);
- handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => this.foundation.handleInputBlur(e);
- handleInputKeyDown = (e: React.KeyboardEvent) => this.foundation.handleInputKeyDown(e);
- handleInputMouseEnter = (e: React.MouseEvent) => this.foundation.handleInputMouseEnter(e);
- handleInputMouseLeave = (e: React.MouseEvent) => this.foundation.handleInputMouseLeave(e);
- handleInputMouseMove = (e: React.MouseEvent) => this.foundation.handleInputMouseMove(e);
- handleUpClick = (e: React.KeyboardEvent) => this.foundation.handleUpClick(e);
- handleDownClick = (e: React.KeyboardEvent) => this.foundation.handleDownClick(e);
- handleMouseUp = (e: React.MouseEvent) => this.foundation.handleMouseUp(e);
- handleMouseLeave = (e: React.MouseEvent) => this.foundation.handleMouseLeave(e);
- 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 (
- <div className={suffixChildrenCls}>
- <span
- role="button"
- tabIndex={-1}
- className={upClassName}
- onMouseDown={notAllowedUp ? noop : this.handleUpClick}
- onMouseUp={this.handleMouseUp}
- onMouseLeave={this.handleMouseLeave}
- >
- <IconChevronUp size="extra-small" />
- </span>
- <span
- role="button"
- tabIndex={-1}
- className={downClassName}
- onMouseDown={notAllowedDown ? noop : this.handleDownClick}
- onMouseUp={this.handleMouseUp}
- onMouseLeave={this.handleMouseLeave}
- >
- <IconChevronDown size="extra-small" />
- </span>
- </div>
- );
- };
- renderSuffix = () => {
- const { innerButtons, suffix } = this.props;
- const { hovering, focusing } = this.state;
- if (innerButtons && (hovering || focusing)) {
- const buttons = this.renderButtons();
- return buttons;
- }
- return suffix;
- };
- render() {
- const {
- disabled,
- className,
- prefixCls,
- min,
- max,
- step,
- shiftStep,
- precision,
- formatter,
- parser,
- forwardedRef,
- onUpClick,
- onDownClick,
- pressInterval,
- pressTimeout,
- suffix,
- size,
- hideButtons,
- innerButtons,
- style,
- onNumberChange,
- keepFocus,
- defaultValue,
- ...rest
- } = this.props;
- 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 = (
- <div
- className={inputNumberCls}
- style={style}
- onMouseMove={e => this.handleInputMouseMove(e)}
- onMouseEnter={e => this.handleInputMouseEnter(e)}
- onMouseLeave={e => this.handleInputMouseLeave(e)}
- >
- <Input
- 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
- )}
- </div>
- );
- return input;
- }
- }
- export default forwardStatics(
- React.forwardRef<HTMLInputElement, InputNumberProps>(function SemiInputNumber(props, ref) {
- return <InputNumber {...props} forwardedRef={ref} />;
- }),
- InputNumber
- );
- export { InputNumber };
|