| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 | /* eslint-disable jsx-a11y/no-static-element-interactions *//* 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, { BaseInputNumberState, 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, isString, 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;}// eslint-disable-next-line @typescript-eslint/no-empty-interfaceexport interface InputNumberState extends BaseInputNumberState {}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,        preventScroll: PropTypes.bool,        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 = {        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;            },            updateStates: (states, callback) => {                this.setState(states, callback);            }        };    }    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, 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 = 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);                            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                        newValue = this.foundation.doFormat(valueStr, false);                        this.foundation.updateStates({ value: newValue });                    }                } else if (this.foundation.isValidNumber(parsedNum)) {                    newValue = this.foundation.doFormat(parsedNum);                    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 (isString(newValue) && newValue !== String(this.props.value)) {                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 });            }        }    }    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                    className={upClassName}                    onMouseDown={notAllowedUp ? noop : this.handleUpClick}                    onMouseUp={this.handleMouseUp}                    onMouseLeave={this.handleMouseLeave}                >                    <IconChevronUp size="extra-small" />                </span>                <span                    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 };
 |