"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var React = require("react");
var Button_1 = require("../../Button");
var Label_1 = require("../../Label");
var Icon_1 = require("../../Icon");
var Utilities_1 = require("../../Utilities");
var positioning_1 = require("../../utilities/positioning");
var SpinButton_styles_1 = require("./SpinButton.styles");
var SpinButton_classNames_1 = require("./SpinButton.classNames");
var KeytipData_1 = require("../../KeytipData");
var KeyboardSpinDirection;
(function (KeyboardSpinDirection) {
    KeyboardSpinDirection[KeyboardSpinDirection["down"] = -1] = "down";
    KeyboardSpinDirection[KeyboardSpinDirection["notSpinning"] = 0] = "notSpinning";
    KeyboardSpinDirection[KeyboardSpinDirection["up"] = 1] = "up";
})(KeyboardSpinDirection = exports.KeyboardSpinDirection || (exports.KeyboardSpinDirection = {}));
var SpinButton = /** @class */ (function (_super) {
    tslib_1.__extends(SpinButton, _super);
    function SpinButton(props) {
        var _this = _super.call(this, props) || this;
        _this._input = React.createRef();
        _this._initialStepDelay = 400;
        _this._stepDelay = 75;
        _this._onFocus = function (ev) {
            // We can't set focus on a non-existing element
            if (!_this._input.current) {
                return;
            }
            if (_this._spinningByMouse || _this.state.keyboardSpinDirection !== KeyboardSpinDirection.notSpinning) {
                _this._stop();
            }
            _this._input.current.select();
            _this.setState({ isFocused: true });
            if (_this.props.onFocus) {
                _this.props.onFocus(ev);
            }
        };
        _this._onBlur = function (ev) {
            _this._validate(ev);
            _this.setState({ isFocused: false });
            if (_this.props.onBlur) {
                _this.props.onBlur(ev);
            }
        };
        _this._onValidate = function (value, event) {
            if (_this.props.onValidate) {
                return _this.props.onValidate(value, event);
            }
            else {
                return _this._defaultOnValidate(value);
            }
        };
        _this._calculatePrecision = function (props) {
            var _a = props.precision, precision = _a === void 0 ? Math.max(Utilities_1.calculatePrecision(props.step), 0) : _a;
            return precision;
        };
        /**
         * Validate function to use if one is not passed in
         */
        _this._defaultOnValidate = function (value) {
            if (value === null || value.trim().length === 0 || isNaN(Number(value))) {
                return _this._lastValidValue;
            }
            var newValue = Math.min(_this.props.max, Math.max(_this.props.min, Number(value)));
            return String(newValue);
        };
        _this._onIncrement = function (value) {
            if (_this.props.onIncrement) {
                return _this.props.onIncrement(value);
            }
            else {
                return _this._defaultOnIncrement(value);
            }
        };
        /**
         * Increment function to use if one is not passed in
         */
        _this._defaultOnIncrement = function (value) {
            var _a = _this.props, max = _a.max, step = _a.step;
            var newValue = Math.min(Number(value) + Number(step), max);
            newValue = Utilities_1.precisionRound(newValue, _this._precision);
            return String(newValue);
        };
        _this._onDecrement = function (value) {
            if (_this.props.onDecrement) {
                return _this.props.onDecrement(value);
            }
            else {
                return _this._defaultOnDecrement(value);
            }
        };
        /**
         * Increment function to use if one is not passed in
         */
        _this._defaultOnDecrement = function (value) {
            var _a = _this.props, min = _a.min, step = _a.step;
            var newValue = Math.max(Number(value) - Number(step), min);
            newValue = Utilities_1.precisionRound(newValue, _this._precision);
            return String(newValue);
        };
        /**
         * This is used when validating text entry
         * in the input (not when changed via the buttons)
         * @param event - the event that fired
         */
        _this._validate = function (event) {
            if (_this.state.value !== undefined && _this._valueToValidate !== undefined && _this._valueToValidate !== _this._lastValidValue) {
                var newValue = _this._onValidate(_this._valueToValidate, event);
                if (newValue) {
                    _this._lastValidValue = newValue;
                    _this._valueToValidate = undefined;
                    _this.setState({ value: newValue });
                }
            }
        };
        /**
         * The method is needed to ensure we are updating the actual input value.
         * without this our value will never change (and validation will not have the correct number)
         * @param event - the event that was fired
         */
        _this._onInputChange = function (event) {
            var element = event.target;
            var value = element.value;
            _this._valueToValidate = value;
            _this.setState({
                value: value
            });
        };
        /**
         * Update the value with the given stepFunction
         * @param shouldSpin - should we fire off another updateValue when we are done here? This should be true
         * when spinning in response to a mouseDown
         * @param stepFunction - function to use to step by
         */
        _this._updateValue = function (shouldSpin, stepDelay, stepFunction) {
            var newValue = stepFunction(_this.state.value);
            if (newValue) {
                _this._lastValidValue = newValue;
                _this.setState({ value: newValue });
            }
            if (_this._spinningByMouse !== shouldSpin) {
                _this._spinningByMouse = shouldSpin;
            }
            if (shouldSpin) {
                _this._currentStepFunctionHandle = _this._async.setTimeout(function () {
                    _this._updateValue(shouldSpin, _this._stepDelay, stepFunction);
                }, stepDelay);
            }
        };
        /**
         * Stop spinning (clear any currently pending update and set spinning to false)
         */
        _this._stop = function () {
            if (_this._currentStepFunctionHandle >= 0) {
                _this._async.clearTimeout(_this._currentStepFunctionHandle);
                _this._currentStepFunctionHandle = -1;
            }
            if (_this._spinningByMouse || _this.state.keyboardSpinDirection !== KeyboardSpinDirection.notSpinning) {
                _this._spinningByMouse = false;
                _this.setState({ keyboardSpinDirection: KeyboardSpinDirection.notSpinning });
            }
        };
        /**
         * Handle keydown on the text field. We need to update
         * the value when up or down arrow are depressed
         * @param event - the keyboardEvent that was fired
         */
        _this._handleKeyDown = function (event) {
            // eat the up and down arrow keys to keep focus in the spinButton
            // (especially when a spinButton is inside of a FocusZone)
            if (event.which === Utilities_1.KeyCodes.up || event.which === Utilities_1.KeyCodes.down || event.which === Utilities_1.KeyCodes.enter) {
                event.preventDefault();
                event.stopPropagation();
            }
            if (_this.props.disabled) {
                _this._stop();
                return;
            }
            var spinDirection = KeyboardSpinDirection.notSpinning;
            switch (event.which) {
                case Utilities_1.KeyCodes.up:
                    spinDirection = KeyboardSpinDirection.up;
                    _this._updateValue(false /* shouldSpin */, _this._initialStepDelay, _this._onIncrement);
                    break;
                case Utilities_1.KeyCodes.down:
                    spinDirection = KeyboardSpinDirection.down;
                    _this._updateValue(false /* shouldSpin */, _this._initialStepDelay, _this._onDecrement);
                    break;
                case Utilities_1.KeyCodes.enter:
                case Utilities_1.KeyCodes.tab:
                    _this._validate(event);
                    break;
                case Utilities_1.KeyCodes.escape:
                    if (_this.state.value !== _this._lastValidValue) {
                        _this.setState({ value: _this._lastValidValue });
                    }
                    break;
                default:
                    break;
            }
            // style the increment/decrement button to look active
            // when the corresponding up/down arrow keys trigger a step
            if (_this.state.keyboardSpinDirection !== spinDirection) {
                _this.setState({ keyboardSpinDirection: spinDirection });
            }
        };
        /**
         * Make sure that we have stopped spinning on keyUp
         * if the up or down arrow fired this event
         * @param event - keyboard event
         */
        _this._handleKeyUp = function (event) {
            if (_this.props.disabled || event.which === Utilities_1.KeyCodes.up || event.which === Utilities_1.KeyCodes.down) {
                _this._stop();
                return;
            }
        };
        _this._onIncrementMouseDown = function () {
            _this._updateValue(true /* shouldSpin */, _this._initialStepDelay, _this._onIncrement);
        };
        _this._onDecrementMouseDown = function () {
            _this._updateValue(true /* shouldSpin */, _this._initialStepDelay, _this._onDecrement);
        };
        Utilities_1.initializeComponentRef(_this);
        Utilities_1.initializeFocusRects();
        Utilities_1.warnMutuallyExclusive('SpinButton', props, {
            value: 'defaultValue'
        });
        var value = props.value || props.defaultValue || String(props.min) || '0';
        _this._lastValidValue = value;
        // Ensure that the autocalculated precision is not negative.
        _this._precision = _this._calculatePrecision(_this.props);
        _this.state = {
            isFocused: false,
            value: value,
            keyboardSpinDirection: KeyboardSpinDirection.notSpinning
        };
        _this._async = new Utilities_1.Async(_this);
        _this._currentStepFunctionHandle = -1;
        _this._labelId = Utilities_1.getId('Label');
        _this._inputId = Utilities_1.getId('input');
        _this._spinningByMouse = false;
        _this._valueToValidate = undefined;
        return _this;
    }
    SpinButton.prototype.componentWillUnmount = function () {
        this._async.dispose();
    };
    /**
     * Invoked when a component is receiving new props. This method is not called for the initial render.
     */
    // tslint:disable-next-line function-name
    SpinButton.prototype.UNSAFE_componentWillReceiveProps = function (newProps) {
        this._lastValidValue = this.state.value;
        var value = newProps.value ? newProps.value : String(newProps.min);
        if (newProps.defaultValue) {
            value = String(Math.max(newProps.min, Math.min(newProps.max, Number(newProps.defaultValue))));
        }
        if (newProps.value !== undefined) {
            this.setState({
                value: value
            });
        }
        this._precision = this._calculatePrecision(newProps);
    };
    SpinButton.prototype.render = function () {
        var _this = this;
        var _a = this.props, disabled = _a.disabled, label = _a.label, min = _a.min, max = _a.max, labelPosition = _a.labelPosition, iconProps = _a.iconProps, incrementButtonIcon = _a.incrementButtonIcon, incrementButtonAriaLabel = _a.incrementButtonAriaLabel, decrementButtonIcon = _a.decrementButtonIcon, decrementButtonAriaLabel = _a.decrementButtonAriaLabel, ariaLabel = _a.ariaLabel, ariaDescribedBy = _a.ariaDescribedBy, customStyles = _a.styles, customUpArrowButtonStyles = _a.upArrowButtonStyles, customDownArrowButtonStyles = _a.downArrowButtonStyles, theme = _a.theme, ariaPositionInSet = _a.ariaPositionInSet, ariaSetSize = _a.ariaSetSize, ariaValueNow = _a.ariaValueNow, ariaValueText = _a.ariaValueText, keytipProps = _a.keytipProps, className = _a.className, inputProps = _a.inputProps, iconButtonProps = _a.iconButtonProps;
        var _b = this.state, isFocused = _b.isFocused, value = _b.value, keyboardSpinDirection = _b.keyboardSpinDirection;
        var classNames = this.props.getClassNames
            ? this.props.getClassNames(theme, disabled, isFocused, keyboardSpinDirection, labelPosition, className)
            : SpinButton_classNames_1.getClassNames(SpinButton_styles_1.getStyles(theme, customStyles), disabled, isFocused, keyboardSpinDirection, labelPosition, className);
        var nativeProps = Utilities_1.getNativeProps(this.props, Utilities_1.divProperties, ['onBlur', 'onFocus', 'className']);
        return (React.createElement("div", { className: classNames.root },
            labelPosition !== positioning_1.Position.bottom && (iconProps || label) && (React.createElement("div", { className: classNames.labelWrapper },
                iconProps && React.createElement(Icon_1.Icon, tslib_1.__assign({}, iconProps, { className: classNames.icon, "aria-hidden": "true" })),
                label && (React.createElement(Label_1.Label, { id: this._labelId, htmlFor: this._inputId, className: classNames.label, disabled: disabled }, label)))),
            React.createElement(KeytipData_1.KeytipData, { keytipProps: keytipProps, disabled: disabled }, function (keytipAttributes) { return (React.createElement("div", tslib_1.__assign({}, nativeProps, { className: classNames.spinButtonWrapper, "aria-label": ariaLabel && ariaLabel, "aria-posinset": ariaPositionInSet, "aria-setsize": ariaSetSize, "data-ktp-target": keytipAttributes['data-ktp-target'] }),
                React.createElement("input", tslib_1.__assign({ value: value, id: _this._inputId, onChange: _this._onChange, onInput: _this._onInputChange, className: classNames.input, type: "text", autoComplete: "off", role: "spinbutton", "aria-labelledby": label && _this._labelId, "aria-valuenow": !isNaN(Number(ariaValueNow)) ? ariaValueNow : !isNaN(Number(value)) ? Number(value) : undefined, "aria-valuetext": ariaValueText ? ariaValueText : isNaN(Number(value)) ? value : undefined, "aria-valuemin": min, "aria-valuemax": max, "aria-describedby": Utilities_1.mergeAriaAttributeValues(ariaDescribedBy, keytipAttributes['aria-describedby']), onBlur: _this._onBlur, ref: _this._input, onFocus: _this._onFocus, onKeyDown: _this._handleKeyDown, onKeyUp: _this._handleKeyUp, readOnly: disabled, "aria-disabled": disabled, "data-lpignore": true, "data-ktp-execute-target": keytipAttributes['data-ktp-execute-target'] }, inputProps)),
                React.createElement("span", { className: classNames.arrowBox },
                    React.createElement(Button_1.IconButton, tslib_1.__assign({ styles: SpinButton_styles_1.getArrowButtonStyles(theme, true, customUpArrowButtonStyles), className: 'ms-UpButton', checked: keyboardSpinDirection === KeyboardSpinDirection.up, disabled: disabled, iconProps: incrementButtonIcon, onMouseDown: _this._onIncrementMouseDown, onMouseLeave: _this._stop, onMouseUp: _this._stop, tabIndex: -1, ariaLabel: incrementButtonAriaLabel, "data-is-focusable": false }, iconButtonProps)),
                    React.createElement(Button_1.IconButton, tslib_1.__assign({ styles: SpinButton_styles_1.getArrowButtonStyles(theme, false, customDownArrowButtonStyles), className: 'ms-DownButton', checked: keyboardSpinDirection === KeyboardSpinDirection.down, disabled: disabled, iconProps: decrementButtonIcon, onMouseDown: _this._onDecrementMouseDown, onMouseLeave: _this._stop, onMouseUp: _this._stop, tabIndex: -1, ariaLabel: decrementButtonAriaLabel, "data-is-focusable": false }, iconButtonProps))))); }),
            labelPosition === positioning_1.Position.bottom && (iconProps || label) && (React.createElement("div", { className: classNames.labelWrapper },
                iconProps && React.createElement(Icon_1.Icon, { iconName: iconProps.iconName, className: classNames.icon, "aria-hidden": "true" }),
                label && (React.createElement(Label_1.Label, { id: this._labelId, htmlFor: this._inputId, className: classNames.label, disabled: disabled }, label))))));
    };
    SpinButton.prototype.focus = function () {
        if (this._input.current) {
            this._input.current.focus();
        }
    };
    Object.defineProperty(SpinButton.prototype, "value", {
        /**
         * Gets the value of the spin button.
         */
        get: function () {
            return this.props.value === undefined ? this.state.value : this.props.value;
        },
        enumerable: true,
        configurable: true
    });
    SpinButton.prototype._onChange = function () {
        /**
         * A noop input change handler. Using onInput instead of onChange was meant to address an issue
         * which apparently has been resolved in React 16 (https://github.com/facebook/react/issues/7027).
         * The no-op onChange handler was still needed because React gives console errors if an input
         * doesn't have onChange.
         *
         * TODO (Fabric 8?) - switch to just calling onChange (this is a breaking change for any tests,
         * ours or 3rd-party, which simulate entering text in a SpinButton)
         */
    };
    SpinButton.defaultProps = {
        step: 1,
        min: 0,
        max: 100,
        disabled: false,
        labelPosition: positioning_1.Position.start,
        label: '',
        incrementButtonIcon: { iconName: 'ChevronUpSmall' },
        decrementButtonIcon: { iconName: 'ChevronDownSmall' }
    };
    SpinButton = tslib_1.__decorate([
        Utilities_1.customizable('SpinButton', ['theme', 'styles'], true)
    ], SpinButton);
    return SpinButton;
}(React.Component));
exports.SpinButton = SpinButton;
//# sourceMappingURL=SpinButton.js.map