UNPKG

@fluentui/react

Version:

Reusable React components for building web experiences.

305 lines (304 loc) 19.3 kB
define(["require", "exports", "tslib", "react", "../../Button", "../../Label", "../../Icon", "../../Utilities", "./SpinButton.styles", "./SpinButton.types", "../../Positioning", "@fluentui/react-hooks"], function (require, exports, tslib_1, React, Button_1, Label_1, Icon_1, Utilities_1, SpinButton_styles_1, SpinButton_types_1, Positioning_1, react_hooks_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SpinButtonBase = void 0; var getClassNames = (0, Utilities_1.classNamesFunction)(); var COMPONENT_NAME = 'SpinButton'; var DEFAULT_PROPS = { disabled: false, label: '', step: 1, labelPosition: Positioning_1.Position.start, incrementButtonIcon: { iconName: 'ChevronUpSmall' }, decrementButtonIcon: { iconName: 'ChevronDownSmall' }, }; var INITIAL_STEP_DELAY = 400; var STEP_DELAY = 75; var useComponentRef = function (props, input, value) { React.useImperativeHandle(props.componentRef, function () { return ({ get value() { return value; }, focus: function () { if (input.current) { input.current.focus(); } }, }); }, [input, value]); }; var noOp = 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) */ }; /** Clamp the value to the provided min and/or max */ var clampValue = function (value, _a) { var min = _a.min, max = _a.max; if (typeof max === 'number') { value = Math.min(value, max); } if (typeof min === 'number') { value = Math.max(value, min); } return value; }; exports.SpinButtonBase = React.forwardRef(function (propsWithoutDefaults, ref) { var props = (0, Utilities_1.getPropsWithDefaults)(DEFAULT_PROPS, propsWithoutDefaults); var disabled = props.disabled, label = props.label, min = props.min, max = props.max, step = props.step, defaultValue = props.defaultValue, valueFromProps = props.value, precisionFromProps = props.precision, labelPosition = props.labelPosition, iconProps = props.iconProps, incrementButtonIcon = props.incrementButtonIcon, incrementButtonAriaLabel = props.incrementButtonAriaLabel, decrementButtonIcon = props.decrementButtonIcon, decrementButtonAriaLabel = props.decrementButtonAriaLabel, ariaLabel = props.ariaLabel, ariaDescribedBy = props.ariaDescribedBy, customUpArrowButtonStyles = props.upArrowButtonStyles, customDownArrowButtonStyles = props.downArrowButtonStyles, theme = props.theme, ariaPositionInSet = props.ariaPositionInSet, ariaSetSize = props.ariaSetSize, ariaValueNow = props.ariaValueNow, ariaValueText = props.ariaValueText, className = props.className, inputProps = props.inputProps, onDecrement = props.onDecrement, onIncrement = props.onIncrement, iconButtonProps = props.iconButtonProps, onValidate = props.onValidate, onChange = props.onChange, styles = props.styles; var input = React.useRef(null); var inputId = (0, react_hooks_1.useId)('input'); var labelId = (0, react_hooks_1.useId)('Label'); var _a = React.useState(false), isFocused = _a[0], setIsFocused = _a[1]; var _b = React.useState(SpinButton_types_1.KeyboardSpinDirection.notSpinning), keyboardSpinDirection = _b[0], setKeyboardSpinDirection = _b[1]; var async = (0, react_hooks_1.useAsync)(); var precision = React.useMemo(function () { return precisionFromProps !== null && precisionFromProps !== void 0 ? precisionFromProps : Math.max((0, Utilities_1.calculatePrecision)(step), 0); }, [precisionFromProps, step]); /** * Actual current value. If `props.value` is provided (controlled), it will always be used. * If not (uncontrolled), this tracks the current value based on user modifications. * Note that while the user is editing text in the field, this will not be updated until "commit" * (blur or press enter). */ var _c = (0, react_hooks_1.useControllableValue)(valueFromProps, defaultValue !== null && defaultValue !== void 0 ? defaultValue : String(min || 0), onChange), value = _c[0], setValue = _c[1]; /** * "Uncommitted" internal value while the user is editing text in the field. This lets us wait to * call `onChange` (and possibly update the real value) until the user "commits" the value by * pressing enter or blurring the field. */ var _d = React.useState(), intermediateValue = _d[0], setIntermediateValue = _d[1]; var internalState = React.useRef({ stepTimeoutHandle: -1, latestValue: undefined, latestIntermediateValue: undefined, }).current; // On each render, update this saved value used by callbacks. (This should be safe even if render // is called multiple times, because an event handler or timeout callback will only run once.) internalState.latestValue = value; internalState.latestIntermediateValue = intermediateValue; var previousValueFromProps = (0, react_hooks_1.usePrevious)(valueFromProps); React.useEffect(function () { // If props.value changes while editing, clear the intermediate value if (valueFromProps !== previousValueFromProps && intermediateValue !== undefined) { setIntermediateValue(undefined); } }, [valueFromProps, previousValueFromProps, intermediateValue]); var classNames = getClassNames(styles, { theme: theme, disabled: disabled, isFocused: isFocused, keyboardSpinDirection: keyboardSpinDirection, labelPosition: labelPosition, className: className, }); var nativeProps = (0, Utilities_1.getNativeProps)(props, Utilities_1.divProperties, [ 'onBlur', 'onFocus', 'className', 'onChange', ]); /** Validate (commit) function called on blur or enter keypress. */ var validate = React.useCallback(function (ev) { // Only run validation if the value changed var enteredValue = internalState.latestIntermediateValue; if (enteredValue !== undefined && enteredValue !== internalState.latestValue) { var newValue = void 0; if (onValidate) { newValue = onValidate(enteredValue, ev); } else if (enteredValue && enteredValue.trim().length && !isNaN(Number(enteredValue))) { // default validation handling newValue = String(clampValue(Number(enteredValue), { min: min, max: max })); } if (newValue !== undefined && newValue !== internalState.latestValue) { // Commit the value if it changed setValue(newValue, ev); } } // Done validating, so clear the intermediate typed value (if any) setIntermediateValue(undefined); }, [internalState, max, min, onValidate, setValue]); /** * Stop spinning (clear any currently pending update and set spinning to false) */ var stop = React.useCallback(function () { if (internalState.stepTimeoutHandle >= 0) { async.clearTimeout(internalState.stepTimeoutHandle); internalState.stepTimeoutHandle = -1; } if (internalState.spinningByMouse || keyboardSpinDirection !== SpinButton_types_1.KeyboardSpinDirection.notSpinning) { internalState.spinningByMouse = false; setKeyboardSpinDirection(SpinButton_types_1.KeyboardSpinDirection.notSpinning); } }, [internalState, keyboardSpinDirection, async]); /** * Update the value with the given stepFunction. * Also starts spinning for mousedown events by scheduling another update with setTimeout. * @param stepFunction - function to use to step by * @param event - The event that triggered the updateValue */ var updateValue = React.useCallback(function (stepFunction, ev) { ev.persist(); if (internalState.latestIntermediateValue !== undefined) { // Edge case: if intermediateValue is set, this means that the user was editing the input // text and then started spinning (either with mouse or keyboard). We need to validate and // call onChange before starting to spin. if (ev.type === 'keydown' || ev.type === 'mousedown') { // For the arrow keys, we have to manually trigger validation. // (For the buttons, validation will happen automatically since the input's onBlur will // be triggered after mousedown on the button completes.) validate(ev); } async.requestAnimationFrame(function () { // After handling any value updates, do the spinning update updateValue(stepFunction, ev); }); return; } // Call the step function and update the value. // (Note: we access the latest value via internalState (not directly) to ensure we don't use // a stale captured value. This is mainly important for spinning by mouse, where we trigger // additional calls to the original updateValue function via setTimeout. It also lets us // avoid useCallback deps on frequently changing values.) var newValue = stepFunction(internalState.latestValue || '', ev); if (newValue !== undefined && newValue !== internalState.latestValue) { setValue(newValue, ev); } // Schedule the next spin if applicable // (will be canceled if there's a mouseup before the timeout runs) var wasSpinning = internalState.spinningByMouse; internalState.spinningByMouse = ev.type === 'mousedown'; if (internalState.spinningByMouse) { internalState.stepTimeoutHandle = async.setTimeout(function () { updateValue(stepFunction, ev); }, wasSpinning ? STEP_DELAY : INITIAL_STEP_DELAY); } }, [internalState, async, validate, setValue]); /** Composed increment handler (uses `props.onIncrement` or default) */ var handleIncrement = React.useCallback(function (newValue) { if (onIncrement) { return onIncrement(newValue); } else { var numericValue = clampValue(Number(newValue) + Number(step), { max: max }); numericValue = (0, Utilities_1.precisionRound)(numericValue, precision); return String(numericValue); } }, [precision, max, onIncrement, step]); /** Composed decrement handler (uses `props.onDecrement` or default) */ var handleDecrement = React.useCallback(function (newValue) { if (onDecrement) { return onDecrement(newValue); } else { var numericValue = clampValue(Number(newValue) - Number(step), { min: min }); numericValue = (0, Utilities_1.precisionRound)(numericValue, precision); return String(numericValue); } }, [precision, min, onDecrement, step]); /** Handles when the user types in the input */ var handleInputChange = function (ev) { setIntermediateValue(ev.target.value); }; /** Composed focus handler (does internal stuff and calls `props.onFocus`) */ var handleFocus = function (ev) { var _a; // We can't set focus on a non-existing element if (!input.current) { return; } if (internalState.spinningByMouse || keyboardSpinDirection !== SpinButton_types_1.KeyboardSpinDirection.notSpinning) { stop(); } input.current.select(); setIsFocused(true); (_a = props.onFocus) === null || _a === void 0 ? void 0 : _a.call(props, ev); }; /** Composed blur handler (does internal stuff and calls `props.onBlur`) */ var handleBlur = function (ev) { var _a; validate(ev); setIsFocused(false); (_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, ev); }; /** Update value when arrow keys are pressed, commit on enter, or revert on escape */ var handleKeyDown = function (ev) { // eat the up and down arrow keys to keep focus in the spinButton // (especially when a spinButton is inside of a FocusZone) // eslint-disable-next-line deprecation/deprecation if (ev.which === Utilities_1.KeyCodes.up || ev.which === Utilities_1.KeyCodes.down || ev.which === Utilities_1.KeyCodes.enter) { ev.preventDefault(); ev.stopPropagation(); } if (disabled) { stop(); return; } var spinDirection = SpinButton_types_1.KeyboardSpinDirection.notSpinning; // eslint-disable-next-line deprecation/deprecation switch (ev.which) { case Utilities_1.KeyCodes.up: spinDirection = SpinButton_types_1.KeyboardSpinDirection.up; updateValue(handleIncrement, ev); break; case Utilities_1.KeyCodes.down: spinDirection = SpinButton_types_1.KeyboardSpinDirection.down; updateValue(handleDecrement, ev); break; case Utilities_1.KeyCodes.enter: // Commit the edited value validate(ev); break; case Utilities_1.KeyCodes.escape: // Revert to previous value setIntermediateValue(undefined); break; } // style the increment/decrement button to look active // when the corresponding up/down arrow keys trigger a step if (keyboardSpinDirection !== spinDirection) { setKeyboardSpinDirection(spinDirection); } }; /** Stop spinning on keyUp if the up or down arrow key fired this event */ var handleKeyUp = React.useCallback(function (ev) { // eslint-disable-next-line deprecation/deprecation if (disabled || ev.which === Utilities_1.KeyCodes.up || ev.which === Utilities_1.KeyCodes.down) { stop(); return; } }, [disabled, stop]); var handleIncrementMouseDown = React.useCallback(function (ev) { updateValue(handleIncrement, ev); }, [handleIncrement, updateValue]); var handleDecrementMouseDown = React.useCallback(function (ev) { updateValue(handleDecrement, ev); }, [handleDecrement, updateValue]); useComponentRef(props, input, value); useDebugWarnings(props); var valueIsNumber = !!value && !isNaN(Number(value)); // Number('') is 0 which may not be desirable var labelContent = (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: labelId, htmlFor: inputId, className: classNames.label, disabled: disabled }, label)))); return (React.createElement("div", { className: classNames.root, ref: ref }, labelPosition !== Positioning_1.Position.bottom && labelContent, React.createElement("div", tslib_1.__assign({}, nativeProps, { className: classNames.spinButtonWrapper, "aria-label": ariaLabel && ariaLabel, "aria-posinset": ariaPositionInSet, "aria-setsize": ariaSetSize, "data-ktp-target": true }), React.createElement("input", tslib_1.__assign({ // Display intermediateValue while editing the text (before commit) value: intermediateValue !== null && intermediateValue !== void 0 ? intermediateValue : value, id: inputId, onChange: noOp, onInput: handleInputChange, className: classNames.input, type: "text", autoComplete: "off", role: "spinbutton", "aria-labelledby": label && labelId, "aria-valuenow": ariaValueNow !== null && ariaValueNow !== void 0 ? ariaValueNow : (valueIsNumber ? Number(value) : undefined), "aria-valuetext": ariaValueText !== null && ariaValueText !== void 0 ? ariaValueText : (valueIsNumber ? undefined : value), "aria-valuemin": min, "aria-valuemax": max, "aria-describedby": ariaDescribedBy, onBlur: handleBlur, ref: input, onFocus: handleFocus, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, disabled: disabled, "aria-disabled": disabled, "data-lpignore": true, "data-ktp-execute-target": true }, inputProps)), React.createElement("span", { className: classNames.arrowButtonsContainer }, React.createElement(Button_1.IconButton, tslib_1.__assign({ styles: (0, SpinButton_styles_1.getArrowButtonStyles)(theme, true, customUpArrowButtonStyles), className: 'ms-UpButton', checked: keyboardSpinDirection === SpinButton_types_1.KeyboardSpinDirection.up, disabled: disabled, iconProps: incrementButtonIcon, onMouseDown: handleIncrementMouseDown, onMouseLeave: stop, onMouseUp: stop, tabIndex: -1, ariaLabel: incrementButtonAriaLabel, "data-is-focusable": false }, iconButtonProps)), React.createElement(Button_1.IconButton, tslib_1.__assign({ styles: (0, SpinButton_styles_1.getArrowButtonStyles)(theme, false, customDownArrowButtonStyles), className: 'ms-DownButton', checked: keyboardSpinDirection === SpinButton_types_1.KeyboardSpinDirection.down, disabled: disabled, iconProps: decrementButtonIcon, onMouseDown: handleDecrementMouseDown, onMouseLeave: stop, onMouseUp: stop, tabIndex: -1, ariaLabel: decrementButtonAriaLabel, "data-is-focusable": false }, iconButtonProps)))), labelPosition === Positioning_1.Position.bottom && labelContent)); }); exports.SpinButtonBase.displayName = COMPONENT_NAME; var useDebugWarnings = function (props) { }; }); //# sourceMappingURL=SpinButton.base.js.map