{"version":3,"file":"numberInput.mjs","names":[],"sources":["../../../../package/common/numberInput/numberInput.tsx"],"sourcesContent":["import type { CSSProperties, ReactElement } from 'react';\nimport React from 'react';\nimport cx from 'classnames';\nimport { NumericFormat } from 'react-number-format';\nimport type { FormValidator } from 'services/formValidation';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faChevronUp, faChevronDown, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';\n\nimport styles from './numberInput.module.pcss';\nimport Input from '../input';\nimport Button from '../button';\nimport ButtonGroup from '../buttonGroup';\nimport { ButtonSize } from '../button/button.types';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { useCoarsePointer } from '../hooks/useCoarsePointer';\nimport { useStepRepeat } from './useStepRepeat';\nimport type { NumberInputLabels, NumberInputProps } from './numberInput.types';\nimport { ThemeColor } from '../themeProvider/color.types';\n\nexport type { NumberInputProps, NumberInputArrowsLayout } from './numberInput.types';\n\ninterface StepButtonProps {\n  label: string;\n  className?: string;\n  style?: CSSProperties;\n  icon: typeof faPlus;\n  disabled: boolean;\n  direction: 1 | -1;\n  onStart: (direction: 1 | -1, multiplier: number) => void;\n  onStop: () => void;\n}\n\n/** Press-and-hold behaviour shared by both stepper layouts. */\nconst DEFAULT_LABELS: Required<NumberInputLabels> = {\n  increase: 'Increase',\n  decrease: 'Decrease',\n};\n\nfunction stepButtonProps({ label, disabled, direction, onStart, onStop }: StepButtonProps) {\n  return {\n    // The input keeps the tab stop — it carries role=\"spinbutton\", which announces the value\n    // and the arrow-key affordance, so two more tab stops per field would be noise.\n    tabIndex: -1,\n    disabled,\n    'aria-label': label,\n    // pointerdown (not click) keeps focus in the field and responds immediately on touch.\n    // Deliberately no setPointerCapture: capturing retargets the following click to the captured\n    // element, past the node Button's ripple listens on. useStepRepeat watches the window for\n    // the release instead.\n    onPointerDown: (e: React.PointerEvent<HTMLElement>) => {\n      if (e.button !== 0 && e.pointerType === 'mouse') return;\n      e.preventDefault();\n      onStart(direction, e.shiftKey ? 10 : 1);\n    },\n    onPointerUp: onStop,\n    onPointerCancel: onStop,\n    onContextMenu: (e: React.SyntheticEvent) => e.preventDefault(),\n  };\n}\n\n/** Stacked-layout chevron: borderless, sits inside the field's border. */\nfunction ChevronStepButton(props: StepButtonProps): ReactElement {\n  return (\n    <button\n      type='button'\n      {...stepButtonProps(props)}\n      className={cx(styles.arrowBtn, props.className)}\n      style={props.style}\n    >\n      <FontAwesomeIcon icon={props.icon} aria-hidden='true' />\n    </button>\n  );\n}\n\n/** Split-layout stepper: a standalone control, so a real Button. */\nfunction SplitStepButton(props: StepButtonProps): ReactElement {\n  const { disabled, ...handlers } = stepButtonProps(props);\n  return (\n    <Button\n      {...handlers}\n      className={cx(styles.splitBtn, props.className)}\n      style={props.style}\n      color={ThemeColor.Neutral}\n      size={ButtonSize.Icon}\n      disabled={disabled}\n      icon={props.icon}\n    />\n  );\n}\n\nfunction NumberInput(props: NumberInputProps): ReactElement {\n  const fvRef = React.useRef<FormValidator | null | undefined>(undefined);\n  const {\n    value,\n    onChange,\n    formValidator,\n    label,\n    error,\n    className,\n    style,\n    classNames,\n    styles: slotStyles,\n    allowNegative = false,\n    decimalSeparator,\n    thousandSeparator,\n    prefix,\n    suffix,\n    decimalScale,\n    fixedDecimalScale = true,\n    placeholder,\n    sign,\n    showArrows = false,\n    step = 1,\n    min,\n    max,\n    arrowsLayout = 'auto',\n    labels,\n    readOnly = false,\n    disabled = false,\n    isRequired = false,\n    onKeyDown: onKeyDownProp,\n    ref,\n    ...rest\n  } = useComponentDefaults('NumberInput', props);\n  const text = useLabels('NumberInput', labels, DEFAULT_LABELS);\n  const [, forceUpdate] = React.useState(0);\n  React.useEffect(() => {\n    fvRef.current = formValidator;\n    if (!formValidator) return;\n    const listener = () => forceUpdate((n) => n + 1);\n    formValidator.registerOnUpdateListener(listener);\n    return () => {\n      formValidator.removeOnUpdateListener(listener);\n    };\n  }, [formValidator]);\n  const computedIsRequired =\n    isRequired || !!formValidator?.validators.find((v) => (v as { validatorId?: string }).validatorId === 'required');\n  const rawValue = formValidator != null ? (formValidator.value as number | string) : value;\n  // When a sign is forced, display the absolute value — the prefix character carries the sign visually.\n  const displayValue = sign != null && rawValue != null ? Math.abs(Number(rawValue)) : rawValue;\n  const controlledValue = displayValue != null ? String(displayValue) : '';\n\n  function handleValueChange(floatValue: number | undefined, wasUser = true) {\n    let out = floatValue;\n    if (out !== undefined) {\n      if (sign === '-') out = -Math.abs(out);\n      if (sign === '+') out = Math.abs(out);\n    }\n    if (formValidator != null) formValidator.set(out ?? null, wasUser);\n    onChange?.(out);\n  }\n\n  function clamp(next: number): number {\n    let out = next;\n    if (!allowNegative && sign !== '-') out = Math.max(0, out);\n    if (sign === '-') out = Math.min(0, out);\n    if (min !== undefined) out = Math.max(min, out);\n    if (max !== undefined) out = Math.min(max, out);\n    return out;\n  }\n\n  const currentNumber = rawValue != null && rawValue !== '' ? Number(rawValue) : 0;\n\n  /**\n   * Called on blur and Enter, never per keystroke: rejecting an out-of-bounds edit as it is\n   * typed swallows keystrokes on their way somewhere valid (with `max: 10`, typing `1` in front\n   * of `8` to reach `18`). An empty field is skipped so a `min` is never typed in for the user.\n   */\n  function clampToBounds() {\n    if (readOnly || disabled) return;\n    if (rawValue == null || rawValue === '') return;\n    const n = Number(rawValue);\n    if (!Number.isFinite(n)) return;\n    const next = clamp(n);\n    if (next !== n) handleValueChange(next);\n  }\n\n  /** Returns whether the value moved, which is how useStepRepeat knows to end a hold at a bound. */\n  function handleStep(delta: number): boolean {\n    if (readOnly || disabled) return false;\n    const next = clamp(currentNumber + delta);\n    if (next === currentNumber) return false;\n    handleValueChange(next);\n    return true;\n  }\n\n  const { start: startRepeat, stop: stopRepeat } = useStepRepeat((delta) => handleStep(delta * step));\n\n  function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {\n    // Enter submits the surrounding form without ever blurring, so the blur clamp would not run.\n    // Before the stepper guard, since the bounds apply with or without steppers.\n    if (e.key === 'Enter') {\n      clampToBounds();\n      onKeyDownProp?.(e);\n      return;\n    }\n    if (!showStepper) {\n      onKeyDownProp?.(e);\n      return;\n    }\n    if (e.key === 'ArrowUp') {\n      e.preventDefault();\n      handleStep(e.shiftKey ? step * 10 : step);\n      return;\n    }\n    if (e.key === 'ArrowDown') {\n      e.preventDefault();\n      handleStep(e.shiftKey ? -step * 10 : -step);\n      return;\n    }\n    onKeyDownProp?.(e);\n  }\n\n  const resolvedPrefix = sign === '-' ? '-' : sign === '+' ? '+' : prefix;\n\n  const isCoarsePointer = useCoarsePointer();\n  const showStepper = showArrows && !readOnly && !disabled;\n  const isSplit = arrowsLayout === 'split' || (arrowsLayout === 'auto' && isCoarsePointer);\n\n  const canIncrease = clamp(currentNumber + step) !== currentNumber;\n  const canDecrease = clamp(currentNumber - step) !== currentNumber;\n\n  const stepperProps = { onStart: startRepeat, onStop: stopRepeat };\n\n  const stackedArrows =\n    showStepper && !isSplit ? (\n      <div className={cx(styles.arrows, classNames?.arrows)} style={slotStyles?.arrows}>\n        <ChevronStepButton\n          className={classNames?.increaseButton}\n          style={slotStyles?.increaseButton}\n          label={text.increase}\n          icon={faChevronUp}\n          disabled={!canIncrease}\n          direction={1}\n          {...stepperProps}\n        />\n        <ChevronStepButton\n          className={classNames?.decreaseButton}\n          style={slotStyles?.decreaseButton}\n          label={text.decrease}\n          icon={faChevronDown}\n          disabled={!canDecrease}\n          direction={-1}\n          {...stepperProps}\n        />\n      </div>\n    ) : null;\n\n  // aria-* is spread onto NumericFormat, which forwards unknown props to the DOM input.\n  // Input's own cloneElement doesn't set these keys, so they survive the clone.\n  const spinButtonProps = showStepper\n    ? {\n        role: 'spinbutton',\n        'aria-valuenow': Number.isFinite(currentNumber) ? currentNumber : undefined,\n        'aria-valuemin': min,\n        'aria-valuemax': max,\n        'aria-valuetext': controlledValue || undefined,\n      }\n    : {};\n\n  // Shared with the split group, whose buttons have to mirror the field's invalid state — they\n  // form one continuous border.\n  const isValid = formValidator ? !formValidator.shouldShowError : !error;\n\n  // In the split layout the ButtonGroup is the root, so the consumer's className and style\n  // belong there rather than on the Input.\n  const isSplitGroup = showStepper && isSplit;\n\n  const input = (\n    <Input\n      label={label}\n      className={cx(\n        styles.numberInput,\n        { [styles.numberInputSplit]: isSplitGroup },\n        // In split mode the ButtonGroup around all three is the root, so the\n        // consumer's root class belongs there rather than on the field.\n        isSplitGroup ? undefined : className,\n        isSplitGroup ? undefined : classNames?.root,\n        classNames?.input,\n      )}\n      style={{\n        ...(isSplitGroup ? undefined : style),\n        ...(isSplitGroup ? undefined : slotStyles?.root),\n        ...slotStyles?.input,\n      }}\n      value={controlledValue}\n      onChange={() => {}}\n      formValidator={null}\n      isRequired={computedIsRequired}\n      onBlur={() => {\n        clampToBounds();\n        if (fvRef.current) fvRef.current.touched = true;\n      }}\n      isValid={isValid}\n      errorMessage={\n        formValidator\n          ? formValidator.shouldShowError\n            ? (formValidator.getCurrentErrorMessage() ?? '')\n            : ''\n          : (error ?? '')\n      }\n      placeholder={placeholder}\n      readOnly={readOnly}\n      disabled={disabled}\n      suffix={stackedArrows}\n      // Input centres its suffix wrapper; this lets the chevrons claim the field's full height.\n      classNames={stackedArrows ? { suffix: styles.arrowSuffix } : undefined}\n      inputMode={decimalScale === 0 ? 'numeric' : 'decimal'}\n      customInput={\n        <NumericFormat\n          // NumericFormat also reports a value set from outside (fv.set(), group.reset()) once it has\n          // reformatted it; that must not mark the field dirty. A string, because its `SourceType`\n          // enum exists only in the type declarations and is undefined at runtime.\n          onValueChange={(vals, { source }) => handleValueChange(vals.floatValue, source === 'event')}\n          allowNegative={sign != null ? false : allowNegative}\n          decimalSeparator={decimalSeparator}\n          thousandSeparator={thousandSeparator}\n          decimalScale={decimalScale}\n          fixedDecimalScale={fixedDecimalScale}\n          prefix={resolvedPrefix}\n          suffix={suffix}\n          onKeyDown={handleKeyDown}\n          // react-number-format owns the <input>, so its ref is reachable only through this prop.\n          getInputRef={ref}\n          {...spinButtonProps}\n          {...rest}\n        />\n      }\n    />\n  );\n\n  if (!isSplitGroup) return input;\n\n  return (\n    // ButtonGroup collapses the doubled borders and flattens the inner corners, so the three\n    // read as one control.\n    <ButtonGroup\n      className={cx(styles.split, { [styles.splitInvalid]: !isValid }, className, classNames?.root)}\n      style={{ ...style, ...slotStyles?.root }}\n    >\n      <SplitStepButton\n        className={classNames?.decreaseButton}\n        style={slotStyles?.decreaseButton}\n        label={text.decrease}\n        icon={faMinus}\n        disabled={!canDecrease}\n        direction={-1}\n        {...stepperProps}\n      />\n      {input}\n      <SplitStepButton\n        className={classNames?.increaseButton}\n        style={slotStyles?.increaseButton}\n        label={text.increase}\n        icon={faPlus}\n        disabled={!canIncrease}\n        direction={1}\n        {...stepperProps}\n      />\n    </ButtonGroup>\n  );\n}\n\nexport default NumberInput;\n"],"mappings":"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"}