UNPKG

@trail-ui/react

Version:
699 lines (686 loc) 25.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/number-input/index.ts var number_input_exports = {}; __export(number_input_exports, { NumberInput: () => NumberInput, useNumberInput: () => useNumberInput }); module.exports = __toCommonJS(number_input_exports); // src/number-input/number-input.tsx var import_shared_utils3 = require("@trail-ui/shared-utils"); var import_theme2 = require("@trail-ui/theme"); var import_react4 = require("react"); var import_react_aria_components2 = require("react-aria-components"); // src/input/input.tsx var import_icons = require("@trail-ui/icons"); var import_shared_utils2 = require("@trail-ui/shared-utils"); var import_theme = require("@trail-ui/theme"); var import_react3 = require("react"); var import_react_aria_components = require("react-aria-components"); // src/input/use-input.ts var import_shared_utils = require("@trail-ui/shared-utils"); var import_react2 = require("react"); var import_react_aria2 = require("react-aria"); // src/_utils/utils.tsx var import_utils = require("@react-aria/utils"); var import_react = __toESM(require("react")); var import_react_aria = require("react-aria"); var import_react_dom = __toESM(require("react-dom")); var import_jsx_runtime = require("react/jsx-runtime"); if (typeof HTMLTemplateElement !== "undefined") { const getFirstChild = Object.getOwnPropertyDescriptor(Node.prototype, "firstChild").get; Object.defineProperty(HTMLTemplateElement.prototype, "firstChild", { configurable: true, enumerable: true, get: function() { if (this.dataset.reactAriaHidden) { return this.content.firstChild; } else { return getFirstChild.call(this); } } }); } var HiddenContext = (0, import_react.createContext)(false); var hiddenFragment = typeof DocumentFragment !== "undefined" ? new DocumentFragment() : null; function useDOMRef(ref) { const domRef = (0, import_react.useRef)(null); (0, import_react.useImperativeHandle)(ref, () => domRef.current); return domRef; } // src/input/use-input.ts function useInput(props) { const { ref, onClear, ...otherProps } = props; const domRef = useDOMRef(ref); const handleClear = (0, import_react2.useCallback)(() => { if (domRef == null ? void 0 : domRef.current) { domRef.current.value = ""; domRef.current.focus(); } onClear == null ? void 0 : onClear(); }, [domRef, onClear]); const { hoverProps, isHovered } = (0, import_react_aria2.useHover)({}); const { isFocused, isFocusVisible, focusProps } = (0, import_react_aria2.useFocusRing)({ isTextInput: true, autoFocus: props.autoFocus }); const { focusProps: clearFocusProps, isFocusVisible: isClearButtonFocusVisible } = (0, import_react_aria2.useFocusRing)(); const { pressProps: clearPressProps } = (0, import_react_aria2.usePress)({ isDisabled: !!(props == null ? void 0 : props.disabled), onPress: handleClear }); const inputValue = props["data-value"]; const isFilled = !!inputValue; const isInvalid = !!props["aria-invalid"] && props["aria-invalid"] !== "false"; const getInputWrapperProps = (0, import_react2.useCallback)( (inputWrapperProps = {}) => { return { "data-filled": (0, import_shared_utils.dataAttr)(isFilled), "data-focused": (0, import_shared_utils.dataAttr)(isFocused), "data-focus-visible": (0, import_shared_utils.dataAttr)(isFocusVisible), "data-hovered": (0, import_shared_utils.dataAttr)(isHovered), "data-disabled": (0, import_shared_utils.dataAttr)(props.disabled), "data-invalid": (0, import_shared_utils.dataAttr)(isInvalid), ...inputWrapperProps }; }, [isFilled, isFocusVisible, isFocused, isHovered, isInvalid, props.disabled] ); const getInputProps = (0, import_react2.useCallback)( (inputProps = {}) => { return { "data-filled": (0, import_shared_utils.dataAttr)(isFilled), ...(0, import_react_aria2.mergeProps)(otherProps, focusProps, hoverProps, inputProps), ref: domRef }; }, [domRef, focusProps, hoverProps, isFilled, otherProps] ); const getClearButtonProps = (0, import_react2.useCallback)( (clearButtonProps = {}) => { return { role: "button", tabIndex: 0, "data-focus-visible": (0, import_shared_utils.dataAttr)(isClearButtonFocusVisible), ...(0, import_react_aria2.mergeProps)(clearFocusProps, clearPressProps, clearButtonProps) }; }, [clearFocusProps, clearPressProps, isClearButtonFocusVisible] ); return { domRef, getInputWrapperProps, getInputProps, getClearButtonProps }; } // src/input/input.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); function Input(props, ref) { [props, ref] = (0, import_react_aria_components.useContextProps)(props, ref, import_react_aria_components.InputContext); const { classNames, className, variant, fullWidth, startContent, endContent, ...otherProps } = props; const { getInputWrapperProps, getInputProps, getClearButtonProps, domRef } = useInput({ ...otherProps, ref }); const isClearable = !!props.onClear; const slots = (0, import_react3.useMemo)( () => (0, import_theme.input)({ variant, fullWidth, isClearable }), [fullWidth, variant, isClearable] ); const end = (0, import_react3.useMemo)(() => { if (isClearable) { return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "span", { ...getClearButtonProps(), className: slots.clearButton({ class: classNames == null ? void 0 : classNames.clearButton }), children: endContent || /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons.XCircleFilledIcon, {}) } ); } return endContent; }, [classNames == null ? void 0 : classNames.clearButton, endContent, getClearButtonProps, isClearable, slots]); const baseStyles = (0, import_shared_utils2.clsx)(classNames == null ? void 0 : classNames.base, className); return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: slots.base({ class: baseStyles }), ...getInputWrapperProps(), children: [ startContent, /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "input", { ...getInputProps(), className: slots.input({ class: classNames == null ? void 0 : classNames.input }), ref: domRef } ), end ] }); } var _Input = (0, import_react3.forwardRef)(Input); // src/number-input/number-input.tsx var import_jsx_runtime3 = require("react/jsx-runtime"); function NumberInput(props) { const { className, classNames, label, description, errorMessage, decrementContent = "-", incrementContent = "+", isCompact = false, placeholder, buttonProps, inputProps, ...otherProps } = props; const slots = (0, import_react4.useMemo)( () => (0, import_theme2.numberInput)({ isCompact }), [isCompact] ); const buttonStyles = (0, import_react4.useMemo)( () => (0, import_theme2.button)({ isIconOnly: true, ...buttonProps }), [buttonProps] ); const baseStyles = (0, import_shared_utils3.clsx)(classNames == null ? void 0 : classNames.base, className); return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_aria_components2.NumberField, { className: slots.base({ class: baseStyles }), ...otherProps, children: [ label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_aria_components2.Label, { className: slots.label({ class: classNames == null ? void 0 : classNames.label }), children: label }), /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_aria_components2.Group, { className: slots.wrapper({ class: classNames == null ? void 0 : classNames.wrapper }), children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( import_react_aria_components2.Button, { slot: "decrement", "data-stepper": "decrement", className: slots.button({ class: (0, import_shared_utils3.clsx)(buttonStyles, classNames == null ? void 0 : classNames.button) }), style: { zIndex: 10 }, children: decrementContent } ), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( _Input, { ...inputProps, placeholder, className: slots.input({ class: classNames == null ? void 0 : classNames.input }) } ), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( import_react_aria_components2.Button, { slot: "increment", "data-stepper": "increment", className: slots.button({ class: (0, import_shared_utils3.clsx)(buttonStyles, classNames == null ? void 0 : classNames.button) }), children: incrementContent } ) ] }), description && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_aria_components2.Text, { slot: "description", className: slots.description({ class: classNames == null ? void 0 : classNames.description }), children: description }), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_aria_components2.FieldError, { className: slots.errorMessage({ class: classNames == null ? void 0 : classNames.errorMessage }), children: errorMessage }) ] }); } // src/number-input/use-number-input.ts var import_hooks2 = require("@trail-ui/hooks"); var import_shared_utils4 = require("@trail-ui/shared-utils"); var import_react7 = require("react"); // src/number-input/use-attr-observer.ts var import_react5 = require("react"); function useAttributeObserver(ref, attributes, fn, enabled) { (0, import_react5.useEffect)(() => { var _a; if (!ref.current || !enabled) return; const win = (_a = ref.current.ownerDocument.defaultView) != null ? _a : window; const attrs = Array.isArray(attributes) ? attributes : [attributes]; const obs = new win.MutationObserver((changes) => { for (const change of changes) { if (change.type === "attributes" && change.attributeName && attrs.includes(change.attributeName)) { fn(change); } } }); obs.observe(ref.current, { attributes: true, attributeFilter: attrs }); return () => obs.disconnect(); }); } // src/number-input/use-spinner.ts var import_hooks = require("@trail-ui/hooks"); var import_react6 = require("react"); var CONTINUOUS_CHANGE_INTERVAL = 50; var CONTINUOUS_CHANGE_DELAY = 300; function useSpinner(increment, decrement) { const [isSpinning, setIsSpinning] = (0, import_react6.useState)(false); const [action, setAction] = (0, import_react6.useState)(null); const [runOnce, setRunOnce] = (0, import_react6.useState)(true); const timeoutRef = (0, import_react6.useRef)(null); const removeTimeout = () => clearTimeout(timeoutRef.current); (0, import_hooks.useInterval)( () => { if (action === "increment") { increment(); } if (action === "decrement") { decrement(); } }, isSpinning ? CONTINUOUS_CHANGE_INTERVAL : null ); const up = (0, import_react6.useCallback)(() => { if (runOnce) { increment(); } timeoutRef.current = setTimeout(() => { setRunOnce(false); setIsSpinning(true); setAction("increment"); }, CONTINUOUS_CHANGE_DELAY); }, [increment, runOnce]); const down = (0, import_react6.useCallback)(() => { if (runOnce) { decrement(); } timeoutRef.current = setTimeout(() => { setRunOnce(false); setIsSpinning(true); setAction("decrement"); }, CONTINUOUS_CHANGE_DELAY); }, [decrement, runOnce]); const stop = (0, import_react6.useCallback)(() => { setRunOnce(true); setIsSpinning(false); removeTimeout(); }, []); (0, import_react6.useEffect)(() => { return () => removeTimeout(); }, []); return { up, down, stop, isSpinning }; } // src/number-input/use-number-input.ts var FLOATING_POINT_REGEX = /^[Ee0-9+\-.]$/; function isFloatingPointNumericCharacter(character) { return FLOATING_POINT_REGEX.test(character); } function isValidNumericKeyboardEvent(event, isValid) { if (event.key == null) return true; const isModifierKey = event.ctrlKey || event.altKey || event.metaKey; const isSingleCharacterKey = event.key.length === 1; if (!isSingleCharacterKey || isModifierKey) return true; return isValid(event.key); } function useNumberInput(props = {}) { const { focusInputOnChange = true, clampValueOnBlur = true, keepWithinRange = true, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER, step: stepProp = 1, isReadOnly, isDisabled, isRequired, isInvalid, pattern = "[0-9]*(.[0-9]+)?", inputMode = "decimal", allowMouseWheel, id, // onChange: _, // precision, name, "aria-describedby": ariaDescBy, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, onFocus: onFocusProp, onBlur: onBlurProp, onInvalid: onInvalidProp, getAriaValueText: getAriaValueTextProp, isValidCharacter: isValidCharacterProp, format: formatValue, parse: parseValue, ...htmlProps } = props; const onFocus = (0, import_hooks2.useCallbackRef)(onFocusProp); const onBlur = (0, import_hooks2.useCallbackRef)(onBlurProp); const onInvalid = (0, import_hooks2.useCallbackRef)(onInvalidProp); const isValidCharacter = (0, import_hooks2.useCallbackRef)(isValidCharacterProp != null ? isValidCharacterProp : isFloatingPointNumericCharacter); const getAriaValueText = (0, import_hooks2.useCallbackRef)(getAriaValueTextProp); const counter = (0, import_hooks2.useCounter)(props); const { update: updateFn, increment: incrementFn, decrement: decrementFn } = counter; const [isFocused, setFocused] = (0, import_react7.useState)(false); const isInteractive = !(isReadOnly || isDisabled); const inputRef = (0, import_react7.useRef)(null); const inputSelectionRef = (0, import_react7.useRef)(null); const incrementButtonRef = (0, import_react7.useRef)(null); const decrementButtonRef = (0, import_react7.useRef)(null); const sanitize = (0, import_react7.useCallback)( (value) => value.split("").filter(isValidCharacter).join(""), [isValidCharacter] ); const parse = (0, import_react7.useCallback)((value) => { var _a; return (_a = parseValue == null ? void 0 : parseValue(value)) != null ? _a : value; }, [parseValue]); const format = (0, import_react7.useCallback)( (value) => { var _a; return ((_a = formatValue == null ? void 0 : formatValue(value)) != null ? _a : value).toString(); }, [formatValue] ); (0, import_hooks2.useUpdateEffect)(() => { if (counter.valueAsNumber > max) { onInvalid == null ? void 0 : onInvalid("rangeOverflow", format(counter.value), counter.valueAsNumber); } else if (counter.valueAsNumber < min) { onInvalid == null ? void 0 : onInvalid("rangeOverflow", format(counter.value), counter.valueAsNumber); } }, [counter.valueAsNumber, counter.value, format, onInvalid]); (0, import_hooks2.useSafeLayoutEffect)(() => { if (!inputRef.current) return; const notInSync = inputRef.current.value != counter.value; if (notInSync) { const parsedInput = parse(inputRef.current.value); counter.setValue(sanitize(parsedInput)); } }, [parse, sanitize]); const increment = (0, import_react7.useCallback)( (step = stepProp) => { if (isInteractive) { incrementFn(step); } }, [incrementFn, isInteractive, stepProp] ); const decrement = (0, import_react7.useCallback)( (step = stepProp) => { if (isInteractive) { decrementFn(step); } }, [decrementFn, isInteractive, stepProp] ); const spinner = useSpinner(increment, decrement); useAttributeObserver(incrementButtonRef, "disabled", spinner.stop, spinner.isSpinning); useAttributeObserver(decrementButtonRef, "disabled", spinner.stop, spinner.isSpinning); const onChange = (0, import_react7.useCallback)( (event) => { const evt = event.nativeEvent; if (evt.isComposing) return; const parsedInput = parse(event.currentTarget.value); updateFn(sanitize(parsedInput)); inputSelectionRef.current = { start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd }; }, [updateFn, sanitize, parse] ); const _onFocus = (0, import_react7.useCallback)( (event) => { var _a, _b, _c; onFocus == null ? void 0 : onFocus(event); if (!inputSelectionRef.current) return; event.target.selectionStart = (_b = inputSelectionRef.current.start) != null ? _b : (_a = event.currentTarget.value) == null ? void 0 : _a.length; event.currentTarget.selectionEnd = (_c = inputSelectionRef.current.end) != null ? _c : event.currentTarget.selectionStart; }, [onFocus] ); const onKeyDown = (0, import_react7.useCallback)( (event) => { if (event.nativeEvent.isComposing) return; if (!isValidNumericKeyboardEvent(event, isValidCharacter)) { event.preventDefault(); } const stepFactor = getStepFactor(event) * stepProp; const eventKey = event.key; const keyMap = { ArrowUp: () => increment(stepFactor), ArrowDown: () => decrement(stepFactor), Home: () => updateFn(min), End: () => updateFn(max) }; const action = keyMap[eventKey]; if (action) { event.preventDefault(); action(event); } }, [isValidCharacter, stepProp, increment, decrement, updateFn, min, max] ); const getStepFactor = (event) => { let ratio = 1; if (event.metaKey || event.ctrlKey) { ratio = 0.1; } if (event.shiftKey) { ratio = 10; } return ratio; }; const ariaValueText = (0, import_react7.useMemo)(() => { const text = getAriaValueText == null ? void 0 : getAriaValueText(counter.value); if (text != null) return text; const defaultText = counter.value.toString(); return !defaultText ? void 0 : defaultText; }, [counter.value, getAriaValueText]); const validateAndClamp = (0, import_react7.useCallback)(() => { let next = counter.value; if (counter.value === "") return; const valueStartsWithE = /^[eE]/.test(counter.value.toString()); if (valueStartsWithE) { counter.setValue(""); } else { if (counter.valueAsNumber < min) { next = min; } if (counter.valueAsNumber > max) { next = max; } counter.cast(next); } }, [counter, max, min]); const onInputBlur = (0, import_react7.useCallback)(() => { setFocused(false); if (clampValueOnBlur) { validateAndClamp(); } }, [clampValueOnBlur, setFocused, validateAndClamp]); const focusInput = (0, import_react7.useCallback)(() => { if (focusInputOnChange) { requestAnimationFrame(() => { var _a; (_a = inputRef.current) == null ? void 0 : _a.focus(); }); } }, [focusInputOnChange]); const spinUp = (0, import_react7.useCallback)( (event) => { event.preventDefault(); spinner.up(); focusInput(); }, [focusInput, spinner] ); const spinDown = (0, import_react7.useCallback)( (event) => { event.preventDefault(); spinner.down(); focusInput(); }, [focusInput, spinner] ); (0, import_hooks2.useEventListener)( () => inputRef.current, "wheel", (event) => { var _a, _b; const doc = (_b = (_a = inputRef.current) == null ? void 0 : _a.ownerDocument) != null ? _b : document; const isInputFocused = doc.activeElement === inputRef.current; if (!allowMouseWheel || !isInputFocused) return; event.preventDefault(); const stepFactor = getStepFactor(event) * stepProp; const direction = Math.sign(event.deltaY); if (direction === -1) { increment(stepFactor); } else if (direction === 1) { decrement(stepFactor); } }, { passive: false } ); const getIncrementButtonProps = (0, import_react7.useCallback)( (props2 = {}, ref = null) => { const disabled = isDisabled || keepWithinRange && counter.isAtMax; return { ...props2, ref: (0, import_hooks2.mergeRefs)(ref, incrementButtonRef), role: "button", tabIndex: -1, onPointerDown: (0, import_shared_utils4.callAllHandlers)(props2.onPointerDown, (event) => { if (event.button !== 0 || disabled) return; spinUp(event); }), onPointerLeave: (0, import_shared_utils4.callAllHandlers)(props2.onPointerLeave, spinner.stop), onPointerUp: (0, import_shared_utils4.callAllHandlers)(props2.onPointerUp, spinner.stop), disabled, "aria-disabled": (0, import_shared_utils4.dataAttr)(disabled) }; }, [counter.isAtMax, keepWithinRange, spinUp, spinner.stop, isDisabled] ); const getDecrementButtonProps = (0, import_react7.useCallback)( (props2 = {}, ref = null) => { const disabled = isDisabled || keepWithinRange && counter.isAtMin; return { ...props2, ref: (0, import_hooks2.mergeRefs)(ref, decrementButtonRef), role: "button", tabIndex: -1, onPointerDown: (0, import_shared_utils4.callAllHandlers)(props2.onPointerDown, (event) => { if (event.button !== 0 || disabled) return; spinDown(event); }), onPointerLeave: (0, import_shared_utils4.callAllHandlers)(props2.onPointerLeave, spinner.stop), onPointerUp: (0, import_shared_utils4.callAllHandlers)(props2.onPointerUp, spinner.stop), disabled, "aria-disabled": (0, import_shared_utils4.dataAttr)(disabled) }; }, [counter.isAtMin, keepWithinRange, spinDown, spinner.stop, isDisabled] ); const getInputProps = (0, import_react7.useCallback)( (props2 = {}, ref = null) => { var _a, _b, _c, _d; return { name, inputMode, type: "text", pattern, "aria-labelledby": ariaLabelledBy, "aria-label": ariaLabel, "aria-describedby": ariaDescBy, id, disabled: isDisabled, ...props2, readOnly: (_a = props2.readOnly) != null ? _a : isReadOnly, "aria-readonly": (_b = props2.readOnly) != null ? _b : isReadOnly, "aria-required": (_c = props2.required) != null ? _c : isRequired, required: (_d = props2.required) != null ? _d : isRequired, ref: (0, import_hooks2.mergeRefs)(inputRef, ref), value: format(counter.value), role: "spinbutton", "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": Number.isNaN(counter.valueAsNumber) ? void 0 : counter.valueAsNumber, "aria-invalid": (0, import_shared_utils4.dataAttr)(isInvalid != null ? isInvalid : counter.isOutOfRange), "aria-valuetext": ariaValueText, autoComplete: "off", autoCorrect: "off", onChange: (0, import_shared_utils4.callAllHandlers)(props2.onChange, onChange), onKeyDown: (0, import_shared_utils4.callAllHandlers)(props2.onKeyDown, onKeyDown), onFocus: (0, import_shared_utils4.callAllHandlers)(props2.onFocus, _onFocus, () => setFocused(true)), onBlur: (0, import_shared_utils4.callAllHandlers)(props2.onBlur, onBlur, onInputBlur) }; }, [ name, inputMode, pattern, ariaLabelledBy, ariaLabel, format, ariaDescBy, id, isDisabled, isRequired, isReadOnly, isInvalid, counter.value, counter.valueAsNumber, counter.isOutOfRange, min, max, ariaValueText, onChange, onKeyDown, _onFocus, onBlur, onInputBlur ] ); return { value: format(counter.value), valueAsNumber: counter.valueAsNumber, isFocused, isDisabled, isReadOnly, getIncrementButtonProps, getDecrementButtonProps, getInputProps, htmlProps }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { NumberInput, useNumberInput });