UNPKG

@zag-js/number-input

Version:

Core logic for the number-input widget implemented as a state machine

914 lines (907 loc) • 31.5 kB
'use strict'; var anatomy$1 = require('@zag-js/anatomy'); var domQuery = require('@zag-js/dom-query'); var utils = require('@zag-js/utils'); var core = require('@zag-js/core'); var number = require('@internationalized/number'); var types = require('@zag-js/types'); // src/number-input.anatomy.ts var anatomy = anatomy$1.createAnatomy("numberInput").parts( "root", "label", "input", "control", "valueText", "incrementTrigger", "decrementTrigger", "scrubber" ); var parts = anatomy.build(); var getRootId = (ctx) => ctx.ids?.root ?? `number-input:${ctx.id}`; var getInputId = (ctx) => ctx.ids?.input ?? `number-input:${ctx.id}:input`; var getIncrementTriggerId = (ctx) => ctx.ids?.incrementTrigger ?? `number-input:${ctx.id}:inc`; var getDecrementTriggerId = (ctx) => ctx.ids?.decrementTrigger ?? `number-input:${ctx.id}:dec`; var getScrubberId = (ctx) => ctx.ids?.scrubber ?? `number-input:${ctx.id}:scrubber`; var getCursorId = (ctx) => `number-input:${ctx.id}:cursor`; var getLabelId = (ctx) => ctx.ids?.label ?? `number-input:${ctx.id}:label`; var getInputEl = (ctx) => ctx.getById(getInputId(ctx)); var getIncrementTriggerEl = (ctx) => ctx.getById(getIncrementTriggerId(ctx)); var getDecrementTriggerEl = (ctx) => ctx.getById(getDecrementTriggerId(ctx)); var getCursorEl = (ctx) => ctx.getDoc().getElementById(getCursorId(ctx)); var getPressedTriggerEl = (ctx, hint) => { let btnEl = null; if (hint === "increment") { btnEl = getIncrementTriggerEl(ctx); } if (hint === "decrement") { btnEl = getDecrementTriggerEl(ctx); } return btnEl; }; var setupVirtualCursor = (ctx, point) => { if (domQuery.isSafari()) return; createVirtualCursor(ctx, point); return () => { getCursorEl(ctx)?.remove(); }; }; var preventTextSelection = (ctx) => { const doc = ctx.getDoc(); const html = doc.documentElement; const body = doc.body; body.style.pointerEvents = "none"; html.style.userSelect = "none"; html.style.cursor = "ew-resize"; return () => { body.style.pointerEvents = ""; html.style.userSelect = ""; html.style.cursor = ""; if (!html.style.length) { html.removeAttribute("style"); } if (!body.style.length) { body.removeAttribute("style"); } }; }; var getMousemoveValue = (ctx, opts) => { const { point, isRtl, event } = opts; const win = ctx.getWin(); const x = utils.roundToDpr(event.movementX, win.devicePixelRatio); const y = utils.roundToDpr(event.movementY, win.devicePixelRatio); let hint = x > 0 ? "increment" : x < 0 ? "decrement" : null; if (isRtl && hint === "increment") hint = "decrement"; if (isRtl && hint === "decrement") hint = "increment"; const newPoint = { x: point.x + x, y: point.y + y }; const width = win.innerWidth; const half = utils.roundToDpr(7.5, win.devicePixelRatio); newPoint.x = utils.wrap(newPoint.x + half, width) - half; return { hint, point: newPoint }; }; var createVirtualCursor = (ctx, point) => { const doc = ctx.getDoc(); const el = doc.createElement("div"); el.className = "scrubber--cursor"; el.id = getCursorId(ctx); Object.assign(el.style, { width: "15px", height: "15px", position: "fixed", pointerEvents: "none", left: "0px", top: "0px", zIndex: domQuery.MAX_Z_INDEX, transform: point ? `translate3d(${point.x}px, ${point.y}px, 0px)` : void 0, willChange: "transform" }); el.innerHTML = ` <svg width="46" height="15" style="left: -15.5px; position: absolute; top: 0; filter: drop-shadow(rgba(0, 0, 0, 0.4) 0px 1px 1.1px);"> <g transform="translate(2 3)"> <path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z" style="stroke-width: 2px; stroke: white;"></path> <path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z"></path> </g> </svg>`; doc.body.appendChild(el); }; // src/cursor.ts function recordCursor(inputEl, scope) { if (!inputEl || !scope.isActiveElement(inputEl)) return; try { const { selectionStart: start, selectionEnd: end, value } = inputEl; if (start == null || end == null) return void 0; return { start, end, value }; } catch { return void 0; } } function restoreCursor(inputEl, selection, scope) { if (!inputEl || !scope.isActiveElement(inputEl)) return; if (!selection) { const len = inputEl.value.length; inputEl.setSelectionRange(len, len); return; } try { const newValue = inputEl.value; const { start, end, value: oldValue } = selection; if (newValue === oldValue) { inputEl.setSelectionRange(start, end); return; } const newStart = getNewCursorPosition(oldValue, newValue, start); const newEnd = start === end ? newStart : getNewCursorPosition(oldValue, newValue, end); const clampedStart = Math.max(0, Math.min(newStart, newValue.length)); const clampedEnd = Math.max(clampedStart, Math.min(newEnd, newValue.length)); inputEl.setSelectionRange(clampedStart, clampedEnd); } catch { const len = inputEl.value.length; inputEl.setSelectionRange(len, len); } } function getNewCursorPosition(oldValue, newValue, oldPosition) { const beforeCursor = oldValue.slice(0, oldPosition); const afterCursor = oldValue.slice(oldPosition); let prefixLength = 0; const maxPrefixLength = Math.min(beforeCursor.length, newValue.length); for (let i = 0; i < maxPrefixLength; i++) { if (beforeCursor[i] === newValue[i]) { prefixLength = i + 1; } else { break; } } let suffixLength = 0; const maxSuffixLength = Math.min(afterCursor.length, newValue.length - prefixLength); for (let i = 0; i < maxSuffixLength; i++) { const oldIndex = afterCursor.length - 1 - i; const newIndex = newValue.length - 1 - i; if (afterCursor[oldIndex] === newValue[newIndex]) { suffixLength = i + 1; } else { break; } } if (prefixLength >= beforeCursor.length) { return prefixLength; } if (suffixLength >= afterCursor.length) { return newValue.length - suffixLength; } if (prefixLength > 0) { return prefixLength; } if (suffixLength > 0) { return newValue.length - suffixLength; } if (oldValue.length > 0) { const ratio = oldPosition / oldValue.length; return Math.round(ratio * newValue.length); } return newValue.length; } // src/number-input.connect.ts function connect(service, normalize) { const { state, send, prop, scope, computed } = service; const focused = state.hasTag("focus"); const disabled = computed("isDisabled"); const readOnly = !!prop("readOnly"); const required = !!prop("required"); const scrubbing = state.matches("scrubbing"); const empty = computed("isValueEmpty"); const invalid = computed("isOutOfRange") || !!prop("invalid"); const isIncrementDisabled = disabled || !computed("canIncrement") || readOnly; const isDecrementDisabled = disabled || !computed("canDecrement") || readOnly; const translations = prop("translations"); return { focused, invalid, empty, value: computed("formattedValue"), valueAsNumber: computed("valueAsNumber"), setValue(value) { send({ type: "VALUE.SET", value }); }, clearValue() { send({ type: "VALUE.CLEAR" }); }, increment() { send({ type: "VALUE.INCREMENT" }); }, decrement() { send({ type: "VALUE.DECREMENT" }); }, setToMax() { send({ type: "VALUE.SET", value: prop("max") }); }, setToMin() { send({ type: "VALUE.SET", value: prop("min") }); }, focus() { getInputEl(scope)?.focus(); }, getRootProps() { return normalize.element({ id: getRootId(scope), ...parts.root.attrs, dir: prop("dir"), "data-disabled": domQuery.dataAttr(disabled), "data-focus": domQuery.dataAttr(focused), "data-invalid": domQuery.dataAttr(invalid), "data-scrubbing": domQuery.dataAttr(scrubbing) }); }, getLabelProps() { return normalize.label({ ...parts.label.attrs, dir: prop("dir"), "data-disabled": domQuery.dataAttr(disabled), "data-focus": domQuery.dataAttr(focused), "data-invalid": domQuery.dataAttr(invalid), "data-required": domQuery.dataAttr(required), "data-scrubbing": domQuery.dataAttr(scrubbing), id: getLabelId(scope), htmlFor: getInputId(scope) }); }, getControlProps() { return normalize.element({ ...parts.control.attrs, dir: prop("dir"), role: "group", "aria-disabled": disabled, "data-focus": domQuery.dataAttr(focused), "data-disabled": domQuery.dataAttr(disabled), "data-invalid": domQuery.dataAttr(invalid), "data-scrubbing": domQuery.dataAttr(scrubbing), "aria-invalid": domQuery.ariaAttr(invalid) }); }, getValueTextProps() { return normalize.element({ ...parts.valueText.attrs, dir: prop("dir"), "data-disabled": domQuery.dataAttr(disabled), "data-invalid": domQuery.dataAttr(invalid), "data-focus": domQuery.dataAttr(focused), "data-scrubbing": domQuery.dataAttr(scrubbing) }); }, getInputProps() { return normalize.input({ ...parts.input.attrs, dir: prop("dir"), name: prop("name"), form: prop("form"), id: getInputId(scope), role: "spinbutton", defaultValue: computed("formattedValue"), pattern: prop("formatOptions") ? void 0 : prop("pattern"), inputMode: prop("inputMode"), "aria-invalid": domQuery.ariaAttr(invalid), "data-invalid": domQuery.dataAttr(invalid), disabled, "data-disabled": domQuery.dataAttr(disabled), readOnly, required: prop("required"), autoComplete: "off", autoCorrect: "off", spellCheck: "false", type: "text", "aria-roledescription": "numberfield", "aria-valuemin": prop("min"), "aria-valuemax": prop("max"), "aria-valuenow": Number.isNaN(computed("valueAsNumber")) ? void 0 : computed("valueAsNumber"), "aria-valuetext": computed("valueText"), "data-scrubbing": domQuery.dataAttr(scrubbing), onFocus() { send({ type: "INPUT.FOCUS" }); }, onBlur() { send({ type: "INPUT.BLUR" }); }, onInput(event) { const selection = recordCursor(event.currentTarget, scope); send({ type: "INPUT.CHANGE", target: event.currentTarget, hint: "set", selection }); }, onBeforeInput(event) { try { const { selectionStart, selectionEnd, value } = event.currentTarget; const nextValue = value.slice(0, selectionStart) + (event.data ?? "") + value.slice(selectionEnd); const isValid = computed("parser").isValidPartialNumber(nextValue); if (!isValid) { event.preventDefault(); } } catch { } }, onKeyDown(event) { if (event.defaultPrevented) return; if (readOnly) return; if (domQuery.isComposingEvent(event)) return; const step = domQuery.getEventStep(event) * prop("step"); const keyMap = { ArrowUp() { send({ type: "INPUT.ARROW_UP", step }); event.preventDefault(); }, ArrowDown() { send({ type: "INPUT.ARROW_DOWN", step }); event.preventDefault(); }, Home() { if (domQuery.isModifierKey(event)) return; send({ type: "INPUT.HOME" }); event.preventDefault(); }, End() { if (domQuery.isModifierKey(event)) return; send({ type: "INPUT.END" }); event.preventDefault(); }, Enter() { send({ type: "INPUT.ENTER" }); } }; const exec = keyMap[event.key]; exec?.(event); } }); }, getDecrementTriggerProps() { return normalize.button({ ...parts.decrementTrigger.attrs, dir: prop("dir"), id: getDecrementTriggerId(scope), disabled: isDecrementDisabled, "data-disabled": domQuery.dataAttr(isDecrementDisabled), "aria-label": translations.decrementLabel, type: "button", tabIndex: -1, "aria-controls": getInputId(scope), "data-scrubbing": domQuery.dataAttr(scrubbing), onPointerDown(event) { if (isDecrementDisabled) return; if (!domQuery.isLeftClick(event)) return; send({ type: "TRIGGER.PRESS_DOWN", hint: "decrement", pointerType: event.pointerType }); if (event.pointerType === "mouse") { event.preventDefault(); } if (event.pointerType === "touch") { event.currentTarget?.focus({ preventScroll: true }); } }, onPointerUp(event) { send({ type: "TRIGGER.PRESS_UP", hint: "decrement", pointerType: event.pointerType }); }, onPointerLeave() { if (isDecrementDisabled) return; send({ type: "TRIGGER.PRESS_UP", hint: "decrement" }); } }); }, getIncrementTriggerProps() { return normalize.button({ ...parts.incrementTrigger.attrs, dir: prop("dir"), id: getIncrementTriggerId(scope), disabled: isIncrementDisabled, "data-disabled": domQuery.dataAttr(isIncrementDisabled), "aria-label": translations.incrementLabel, type: "button", tabIndex: -1, "aria-controls": getInputId(scope), "data-scrubbing": domQuery.dataAttr(scrubbing), onPointerDown(event) { if (isIncrementDisabled || !domQuery.isLeftClick(event)) return; send({ type: "TRIGGER.PRESS_DOWN", hint: "increment", pointerType: event.pointerType }); if (event.pointerType === "mouse") { event.preventDefault(); } if (event.pointerType === "touch") { event.currentTarget?.focus({ preventScroll: true }); } }, onPointerUp(event) { send({ type: "TRIGGER.PRESS_UP", hint: "increment", pointerType: event.pointerType }); }, onPointerLeave(event) { send({ type: "TRIGGER.PRESS_UP", hint: "increment", pointerType: event.pointerType }); } }); }, getScrubberProps() { return normalize.element({ ...parts.scrubber.attrs, dir: prop("dir"), "data-disabled": domQuery.dataAttr(disabled), id: getScrubberId(scope), role: "presentation", "data-scrubbing": domQuery.dataAttr(scrubbing), onMouseDown(event) { if (disabled) return; if (!domQuery.isLeftClick(event)) return; const point = domQuery.getEventPoint(event); const win = domQuery.getWindow(event.currentTarget); const dpr = win.devicePixelRatio; point.x = point.x - utils.roundToDpr(7.5, dpr); point.y = point.y - utils.roundToDpr(7.5, dpr); send({ type: "SCRUBBER.PRESS_DOWN", point }); event.preventDefault(); }, style: { cursor: disabled ? void 0 : "ew-resize" } }); } }; } var createFormatter = (locale, options = {}) => { return new Intl.NumberFormat(locale, options); }; var createParser = (locale, options = {}) => { return new number.NumberParser(locale, options); }; var parseValue = (value, params) => { const { prop, computed } = params; if (!prop("formatOptions")) return parseFloat(value); if (value === "") return Number.NaN; return computed("parser").parse(value); }; var formatValue = (value, params) => { const { prop, computed } = params; if (Number.isNaN(value)) return ""; if (!prop("formatOptions")) return value.toString(); return computed("formatter").format(value); }; var getDefaultStep = (step, formatOptions) => { let defaultStep = step !== void 0 && !Number.isNaN(step) ? step : 1; if (formatOptions?.style === "percent" && (step === void 0 || Number.isNaN(step))) { defaultStep = 0.01; } return defaultStep; }; // src/number-input.machine.ts var { choose, guards, createMachine } = core.setup(); var { not, and } = guards; var machine = createMachine({ props({ props: props2 }) { const step = getDefaultStep(props2.step, props2.formatOptions); return { dir: "ltr", locale: "en-US", focusInputOnChange: true, clampValueOnBlur: !props2.allowOverflow, allowOverflow: false, inputMode: "decimal", pattern: "-?[0-9]*(.[0-9]+)?", defaultValue: "", step, min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER, spinOnPress: true, ...props2, translations: { incrementLabel: "increment value", decrementLabel: "decrease value", ...props2.translations } }; }, initialState() { return "idle"; }, context({ prop, bindable, getComputed }) { return { value: bindable(() => ({ defaultValue: prop("defaultValue"), value: prop("value"), onChange(value) { const computed = getComputed(); const valueAsNumber = parseValue(value, { computed, prop }); prop("onValueChange")?.({ value, valueAsNumber }); } })), hint: bindable(() => ({ defaultValue: null })), scrubberCursorPoint: bindable(() => ({ defaultValue: null, hash(value) { return value ? `x:${value.x}, y:${value.y}` : ""; } })), fieldsetDisabled: bindable(() => ({ defaultValue: false })) }; }, computed: { isRtl: ({ prop }) => prop("dir") === "rtl", valueAsNumber: ({ context, computed, prop }) => parseValue(context.get("value"), { computed, prop }), formattedValue: ({ computed, prop }) => formatValue(computed("valueAsNumber"), { computed, prop }), isAtMin: ({ computed, prop }) => utils.isValueAtMin(computed("valueAsNumber"), prop("min")), isAtMax: ({ computed, prop }) => utils.isValueAtMax(computed("valueAsNumber"), prop("max")), isOutOfRange: ({ computed, prop }) => !utils.isValueWithinRange(computed("valueAsNumber"), prop("min"), prop("max")), isValueEmpty: ({ context }) => context.get("value") === "", isDisabled: ({ prop, context }) => !!prop("disabled") || context.get("fieldsetDisabled"), canIncrement: ({ prop, computed }) => prop("allowOverflow") || !computed("isAtMax"), canDecrement: ({ prop, computed }) => prop("allowOverflow") || !computed("isAtMin"), valueText: ({ prop, context }) => prop("translations").valueText?.(context.get("value")), formatter: core.memo( ({ prop }) => [prop("locale"), prop("formatOptions")], ([locale, formatOptions]) => createFormatter(locale, formatOptions) ), parser: core.memo( ({ prop }) => [prop("locale"), prop("formatOptions")], ([locale, formatOptions]) => createParser(locale, formatOptions) ) }, watch({ track, action, context, computed, prop }) { track([() => context.get("value"), () => prop("locale")], () => { action(["syncInputElement"]); }); track([() => computed("isOutOfRange")], () => { action(["invokeOnInvalid"]); }); track([() => context.hash("scrubberCursorPoint")], () => { action(["setVirtualCursorPosition"]); }); }, effects: ["trackFormControl"], on: { "VALUE.SET": { actions: ["setRawValue"] }, "VALUE.CLEAR": { actions: ["clearValue"] }, "VALUE.INCREMENT": { actions: ["increment"] }, "VALUE.DECREMENT": { actions: ["decrement"] } }, states: { idle: { on: { "TRIGGER.PRESS_DOWN": [ { guard: "isTouchPointer", target: "before:spin", actions: ["setHint"] }, { target: "before:spin", actions: ["focusInput", "invokeOnFocus", "setHint"] } ], "SCRUBBER.PRESS_DOWN": { target: "scrubbing", actions: ["focusInput", "invokeOnFocus", "setHint", "setCursorPoint"] }, "INPUT.FOCUS": { target: "focused", actions: ["focusInput", "invokeOnFocus"] } } }, focused: { tags: ["focus"], effects: ["attachWheelListener"], on: { "TRIGGER.PRESS_DOWN": [ { guard: "isTouchPointer", target: "before:spin", actions: ["setHint"] }, { target: "before:spin", actions: ["focusInput", "setHint"] } ], "SCRUBBER.PRESS_DOWN": { target: "scrubbing", actions: ["focusInput", "setHint", "setCursorPoint"] }, "INPUT.ARROW_UP": { actions: ["increment"] }, "INPUT.ARROW_DOWN": { actions: ["decrement"] }, "INPUT.HOME": { actions: ["decrementToMin"] }, "INPUT.END": { actions: ["incrementToMax"] }, "INPUT.CHANGE": { actions: ["setValue", "setHint"] }, "INPUT.BLUR": [ { guard: and("clampValueOnBlur", not("isInRange")), target: "idle", actions: ["setClampedValue", "clearHint", "invokeOnBlur"] }, { guard: not("isInRange"), target: "idle", actions: ["setFormattedValue", "clearHint", "invokeOnBlur", "invokeOnInvalid"] }, { target: "idle", actions: ["setFormattedValue", "clearHint", "invokeOnBlur"] } ], "INPUT.ENTER": { actions: ["setFormattedValue", "clearHint", "invokeOnBlur"] } } }, "before:spin": { tags: ["focus"], effects: ["trackButtonDisabled", "waitForChangeDelay"], entry: choose([ { guard: "isIncrementHint", actions: ["increment"] }, { guard: "isDecrementHint", actions: ["decrement"] } ]), on: { CHANGE_DELAY: { target: "spinning", guard: and("isInRange", "spinOnPress") }, "TRIGGER.PRESS_UP": [ { guard: "isTouchPointer", target: "focused", actions: ["clearHint"] }, { target: "focused", actions: ["focusInput", "clearHint"] } ] } }, spinning: { tags: ["focus"], effects: ["trackButtonDisabled", "spinValue"], on: { SPIN: [ { guard: "isIncrementHint", actions: ["increment"] }, { guard: "isDecrementHint", actions: ["decrement"] } ], "TRIGGER.PRESS_UP": { target: "focused", actions: ["focusInput", "clearHint"] } } }, scrubbing: { tags: ["focus"], effects: ["activatePointerLock", "trackMousemove", "setupVirtualCursor", "preventTextSelection"], on: { "SCRUBBER.POINTER_UP": { target: "focused", actions: ["focusInput", "clearCursorPoint"] }, "SCRUBBER.POINTER_MOVE": [ { guard: "isIncrementHint", actions: ["increment", "setCursorPoint"] }, { guard: "isDecrementHint", actions: ["decrement", "setCursorPoint"] } ] } } }, implementations: { guards: { clampValueOnBlur: ({ prop }) => prop("clampValueOnBlur"), spinOnPress: ({ prop }) => !!prop("spinOnPress"), isInRange: ({ computed }) => !computed("isOutOfRange"), isDecrementHint: ({ context, event }) => (event.hint ?? context.get("hint")) === "decrement", isIncrementHint: ({ context, event }) => (event.hint ?? context.get("hint")) === "increment", isTouchPointer: ({ event }) => event.pointerType === "touch" }, effects: { waitForChangeDelay({ send }) { const id = setTimeout(() => { send({ type: "CHANGE_DELAY" }); }, 300); return () => clearTimeout(id); }, spinValue({ send }) { const id = setInterval(() => { send({ type: "SPIN" }); }, 50); return () => clearInterval(id); }, trackFormControl({ context, scope }) { const inputEl = getInputEl(scope); return domQuery.trackFormControl(inputEl, { onFieldsetDisabledChange(disabled) { context.set("fieldsetDisabled", disabled); }, onFormReset() { context.set("value", context.initial("value")); } }); }, setupVirtualCursor({ context, scope }) { const point = context.get("scrubberCursorPoint"); return setupVirtualCursor(scope, point); }, preventTextSelection({ scope }) { return preventTextSelection(scope); }, trackButtonDisabled({ context, scope, send }) { const hint = context.get("hint"); const btn = getPressedTriggerEl(scope, hint); return domQuery.observeAttributes(btn, { attributes: ["disabled"], callback() { send({ type: "TRIGGER.PRESS_UP", src: "attr" }); } }); }, attachWheelListener({ scope, send, prop }) { const inputEl = getInputEl(scope); if (!inputEl || !scope.isActiveElement(inputEl) || !prop("allowMouseWheel")) return; function onWheel(event) { event.preventDefault(); const dir = Math.sign(event.deltaY) * -1; if (dir === 1) { send({ type: "VALUE.INCREMENT" }); } else if (dir === -1) { send({ type: "VALUE.DECREMENT" }); } } return domQuery.addDomEvent(inputEl, "wheel", onWheel, { passive: false }); }, activatePointerLock({ scope }) { if (domQuery.isSafari()) return; return domQuery.requestPointerLock(scope.getDoc()); }, trackMousemove({ scope, send, context, computed }) { const doc = scope.getDoc(); function onMousemove(event) { const point = context.get("scrubberCursorPoint"); const isRtl = computed("isRtl"); const value = getMousemoveValue(scope, { point, isRtl, event }); if (!value.hint) return; send({ type: "SCRUBBER.POINTER_MOVE", hint: value.hint, point: value.point }); } function onMouseup() { send({ type: "SCRUBBER.POINTER_UP" }); } return utils.callAll(domQuery.addDomEvent(doc, "mousemove", onMousemove, false), domQuery.addDomEvent(doc, "mouseup", onMouseup, false)); } }, actions: { focusInput({ scope, prop }) { if (!prop("focusInputOnChange")) return; const inputEl = getInputEl(scope); if (scope.isActiveElement(inputEl)) return; domQuery.raf(() => inputEl?.focus({ preventScroll: true })); }, increment({ context, event, prop, computed }) { let nextValue = utils.incrementValue(computed("valueAsNumber"), event.step ?? prop("step")); if (!prop("allowOverflow")) nextValue = utils.clampValue(nextValue, prop("min"), prop("max")); context.set("value", formatValue(nextValue, { computed, prop })); }, decrement({ context, event, prop, computed }) { let nextValue = utils.decrementValue(computed("valueAsNumber"), event.step ?? prop("step")); if (!prop("allowOverflow")) nextValue = utils.clampValue(nextValue, prop("min"), prop("max")); context.set("value", formatValue(nextValue, { computed, prop })); }, setClampedValue({ context, prop, computed }) { const nextValue = utils.clampValue(computed("valueAsNumber"), prop("min"), prop("max")); context.set("value", formatValue(nextValue, { computed, prop })); }, setRawValue({ context, event, prop, computed }) { let nextValue = parseValue(event.value, { computed, prop }); if (!prop("allowOverflow")) nextValue = utils.clampValue(nextValue, prop("min"), prop("max")); context.set("value", formatValue(nextValue, { computed, prop })); }, setValue({ context, event }) { const value = event.target?.value ?? event.value; context.set("value", value); }, clearValue({ context }) { context.set("value", ""); }, incrementToMax({ context, prop, computed }) { const value = formatValue(prop("max"), { computed, prop }); context.set("value", value); }, decrementToMin({ context, prop, computed }) { const value = formatValue(prop("min"), { computed, prop }); context.set("value", value); }, setHint({ context, event }) { context.set("hint", event.hint); }, clearHint({ context }) { context.set("hint", null); }, invokeOnFocus({ computed, prop }) { prop("onFocusChange")?.({ focused: true, value: computed("formattedValue"), valueAsNumber: computed("valueAsNumber") }); }, invokeOnBlur({ computed, prop }) { prop("onFocusChange")?.({ focused: false, value: computed("formattedValue"), valueAsNumber: computed("valueAsNumber") }); }, invokeOnInvalid({ computed, prop, event }) { if (event.type === "INPUT.CHANGE") return; const reason = computed("valueAsNumber") > prop("max") ? "rangeOverflow" : "rangeUnderflow"; prop("onValueInvalid")?.({ reason, value: computed("formattedValue"), valueAsNumber: computed("valueAsNumber") }); }, syncInputElement({ context, event, computed, scope }) { const value = event.type.endsWith("CHANGE") ? context.get("value") : computed("formattedValue"); const inputEl = getInputEl(scope); const sel = event.selection; domQuery.raf(() => { domQuery.setElementValue(inputEl, value); restoreCursor(inputEl, sel, scope); }); }, setFormattedValue({ context, computed }) { context.set("value", computed("formattedValue")); }, setCursorPoint({ context, event }) { context.set("scrubberCursorPoint", event.point); }, clearCursorPoint({ context }) { context.set("scrubberCursorPoint", null); }, setVirtualCursorPosition({ context, scope }) { const cursorEl = getCursorEl(scope); const point = context.get("scrubberCursorPoint"); if (!cursorEl || !point) return; cursorEl.style.transform = `translate3d(${point.x}px, ${point.y}px, 0px)`; } } } }); var props = types.createProps()([ "allowMouseWheel", "allowOverflow", "clampValueOnBlur", "dir", "disabled", "focusInputOnChange", "form", "formatOptions", "getRootNode", "id", "ids", "inputMode", "invalid", "locale", "max", "min", "name", "onFocusChange", "onValueChange", "onValueInvalid", "pattern", "required", "readOnly", "spinOnPress", "step", "translations", "value", "defaultValue" ]); var splitProps = utils.createSplitProps(props); exports.anatomy = anatomy; exports.connect = connect; exports.machine = machine; exports.props = props; exports.splitProps = splitProps;