{"version":3,"file":"otpInput.mjs","names":[],"sources":["../../../../package/common/otpInput/otpInput.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport Tooltip from 'components/common/tooltip';\nimport { useComponentDefaults, useLabels } from '../configProvider';\n\nimport { OtpInputCharset, type OtpInputLabels, type OtpInputProps } from './otpInput.types';\nimport styles from './otpInput.module.pcss';\n\nconst NUMERIC_RE = /[0-9]/g;\nconst ALPHANUMERIC_RE = /[a-zA-Z0-9]/g;\n\nconst DEFAULT_LABELS: Required<OtpInputLabels> = {\n  field: 'One-time code',\n  digit: (position, length) => `Digit ${position} of ${length}`,\n};\n\nfunction filterChars(raw: string, charset: OtpInputCharset): string {\n  const re = charset === OtpInputCharset.Alphanumeric ? ALPHANUMERIC_RE : NUMERIC_RE;\n  return (raw.match(re) || []).join('');\n}\n\nfunction OtpInput(props: OtpInputProps): ReactElement {\n  const {\n    length = 6,\n    value,\n    onChange = () => {},\n    onComplete = () => {},\n    formValidator = null,\n    charset = OtpInputCharset.Numeric,\n    masked = false,\n    groupAfter = [],\n    label = '',\n    isValid = true,\n    errorMessage = '',\n    isRequired = false,\n    clearOnError = false,\n    clearErrorDelay = 900,\n    disabled = false,\n    autoFocus = false,\n    className = '',\n    style,\n    classNames,\n    styles: slotStyles,\n    labels,\n    ...rest\n  } = useComponentDefaults('OtpInput', props);\n  const text = useLabels('OtpInput', labels, DEFAULT_LABELS);\n\n  const currentValue = (formValidator ? (formValidator.value as string) : value) ?? '';\n  const [, forceUpdate] = useState(0);\n  const [isShaking, setIsShaking] = useState(false);\n  const [isFocused, setIsFocused] = useState(false);\n  const [isHovered, setIsHovered] = useState(false);\n  const [pendingClear, setPendingClear] = useState(false);\n  const [wasError, setWasError] = useState(false);\n  const inputRefs = useRef<Array<HTMLInputElement | null>>([]);\n\n  useEffect(() => {\n    if (!formValidator) return;\n    formValidator.validate();\n    const listener = () => forceUpdate((n) => n + 1);\n    formValidator.registerOnUpdateListener(listener);\n    return () => formValidator.removeOnUpdateListener(listener);\n  }, [formValidator]);\n\n  useEffect(() => {\n    if (!autoFocus) return;\n    const firstEmpty = Math.min(currentValue.length, length - 1);\n    inputRefs.current[firstEmpty]?.focus();\n    // Only run once, on mount.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const showError = formValidator ? formValidator.shouldShowError : !isValid || !!errorMessage;\n  const displayedErrorMessage = formValidator ? formValidator.getCurrentErrorMessage() : errorMessage || '';\n\n  // State set during render, not a ref read in an effect: the ref survives a remount (StrictMode,\n  // <Activity>), so the re-run saw no new error and dropped the pending clear.\n  if (showError !== wasError) {\n    setWasError(showError);\n    if (showError) setIsShaking(true);\n    setPendingClear(showError && clearOnError);\n  }\n\n  useEffect(() => {\n    if (!isShaking) return;\n    const id = window.setTimeout(() => setIsShaking(false), 400);\n    return () => window.clearTimeout(id);\n  }, [isShaking]);\n\n  useEffect(() => {\n    if (!pendingClear || !clearOnError) return;\n    const id = window.setTimeout(() => {\n      setPendingClear(false);\n      commit('');\n      inputRefs.current[0]?.focus();\n    }, clearErrorDelay);\n    return () => window.clearTimeout(id);\n    // commit is recreated every render; listing it would restart the delay on each keystroke.\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\n  }, [pendingClear, clearOnError, clearErrorDelay]);\n\n  function commit(next: string) {\n    const wasComplete = currentValue.length === length;\n    const clipped = filterChars(next, charset).slice(0, length);\n    if (formValidator) {\n      formValidator.set(clipped);\n    }\n    onChange?.(clipped);\n    // Only fire once per genuinely new completion (going from partial to full), not on every\n    // keystroke made while correcting an already-full code.\n    if (clipped.length === length && !wasComplete) {\n      onComplete?.(clipped);\n    }\n  }\n\n  function setCharAt(index: number, char: string) {\n    const chars = currentValue.split('');\n    chars[index] = char;\n    commit(chars.join(''));\n  }\n\n  function handleChange(index: number, rawInput: string) {\n    if (disabled) return;\n    if (rawInput === '') {\n      // Box was cleared (e.g. select + delete) — Backspace itself is handled in onKeyDown.\n      setCharAt(index, '');\n      return;\n    }\n\n    const filtered = filterChars(rawInput, charset);\n    if (!filtered) {\n      // Typed an invalid/disallowed character — the controlled `value` re-render discards it.\n      return;\n    }\n\n    // Clicking straight into a box past the contiguous filled prefix (skipping ahead of\n    // empty boxes) fills the next expected slot instead, rather than leaving a gap before it\n    // that setCharAt/distributeFrom can't represent in a plain string and would silently collapse.\n    const targetIndex = Math.min(index, currentValue.length);\n\n    if (rawInput.length > 1) {\n      // Mobile autofill / SMS suggestions can drop the whole code into one box, bypassing maxLength.\n      distributeFrom(targetIndex, filtered);\n      return;\n    }\n\n    setCharAt(targetIndex, filtered);\n    if (targetIndex < length - 1) {\n      inputRefs.current[targetIndex + 1]?.focus();\n    } else {\n      inputRefs.current[targetIndex]?.blur();\n    }\n  }\n\n  function distributeFrom(startIndex: number, chars: string) {\n    const clean = filterChars(chars, charset);\n    if (!clean) return;\n    const existing = currentValue.padEnd(length, ' ').split('');\n    let cursor = startIndex;\n    for (const char of clean) {\n      if (cursor >= length) break;\n      existing[cursor] = char;\n      cursor += 1;\n    }\n    commit(existing.join('').trimEnd());\n    const nextFocus = Math.min(cursor, length - 1);\n    inputRefs.current[nextFocus]?.focus();\n    if (cursor >= length) {\n      inputRefs.current[length - 1]?.blur();\n    }\n  }\n\n  function handlePaste(index: number, event: React.ClipboardEvent<HTMLInputElement>) {\n    if (disabled) return;\n    const pasted = event.clipboardData.getData('text');\n    if (!pasted) return;\n    event.preventDefault();\n    // Same clamp as handleChange — pasting into a box past the filled prefix starts at the\n    // next expected slot instead of leaving an unrepresentable gap before it.\n    distributeFrom(Math.min(index, currentValue.length), pasted);\n  }\n\n  function handleKeyDown(index: number, event: React.KeyboardEvent<HTMLInputElement>) {\n    if (disabled) return;\n\n    // React's controlled-input onChange never fires when the resulting value is identical to\n    // what's already rendered (its value tracker sees no change) — so retyping the exact digit\n    // already in a box would otherwise silently do nothing. keydown always fires, so handle that\n    // one case here and skip the native edit entirely.\n    if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {\n      const typed = filterChars(event.key, charset);\n      if (typed && currentValue[index] === typed) {\n        event.preventDefault();\n        setCharAt(index, typed);\n        if (index < length - 1) {\n          inputRefs.current[index + 1]?.focus();\n        } else {\n          inputRefs.current[index]?.blur();\n        }\n        return;\n      }\n    }\n\n    if (event.key === 'Backspace') {\n      if (currentValue[index]) {\n        setCharAt(index, '');\n        return;\n      }\n      if (index > 0) {\n        event.preventDefault();\n        setCharAt(index - 1, '');\n        inputRefs.current[index - 1]?.focus();\n      }\n      return;\n    }\n    if (event.key === 'Delete') {\n      if (currentValue[index]) setCharAt(index, '');\n      return;\n    }\n    if (event.key === 'ArrowLeft' && index > 0) {\n      event.preventDefault();\n      inputRefs.current[index - 1]?.focus();\n      return;\n    }\n    if (event.key === 'ArrowRight' && index < length - 1) {\n      event.preventDefault();\n      inputRefs.current[index + 1]?.focus();\n    }\n  }\n\n  function handleFocus(event: React.FocusEvent<HTMLInputElement>) {\n    event.target.select();\n  }\n\n  function handleGroupFocus() {\n    setIsFocused(true);\n  }\n\n  function handleGroupBlur(event: React.FocusEvent<HTMLDivElement>) {\n    // Auto-advance moves focus between boxes constantly while typing — only treat this as\n    // leaving the group once focus actually lands outside it, not on the next box.\n    if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n    setIsFocused(false);\n    if (formValidator) formValidator.touched = true;\n  }\n\n  return (\n    <div {...rest} className={cx(styles.otpInput, disabled && styles.otpInputDisabled, className)} style={style}>\n      {label && (\n        <div className={cx(styles.otpInputLabel, classNames?.label)} style={slotStyles?.label}>\n          {label}\n          {isRequired && <span className={styles.otpInputRequiredMark}> *</span>}\n        </div>\n      )}\n      <div\n        role='group'\n        aria-label={label || text.field}\n        className={cx(styles.otpInputBoxes, isShaking && styles.otpInputBoxesShake, classNames?.inputs)}\n        style={slotStyles?.inputs}\n        onFocus={handleGroupFocus}\n        onBlur={handleGroupBlur}\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={() => setIsHovered(false)}\n      >\n        {Array.from({ length }, (_, index) => {\n          const char = currentValue[index] || '';\n          return (\n            <div key={index} className={styles.otpInputBoxWrapper}>\n              <input\n                ref={(el) => {\n                  inputRefs.current[index] = el;\n                }}\n                type={masked ? 'password' : 'text'}\n                inputMode={charset === OtpInputCharset.Numeric ? 'numeric' : 'text'}\n                pattern={charset === OtpInputCharset.Numeric ? '[0-9]*' : undefined}\n                autoComplete={index === 0 ? 'one-time-code' : 'off'}\n                maxLength={1}\n                disabled={disabled}\n                value={char}\n                aria-label={text.digit(index + 1, length)}\n                aria-invalid={showError || undefined}\n                className={cx(\n                  styles.otpInputBox,\n                  showError && styles.otpInputBoxError,\n                  char && styles.otpInputBoxFilled,\n                  classNames?.input,\n                )}\n                style={slotStyles?.input}\n                onChange={(e) => handleChange(index, e.target.value)}\n                onKeyDown={(e) => handleKeyDown(index, e)}\n                onPaste={(e) => handlePaste(index, e)}\n                onFocus={handleFocus}\n              />\n              {groupAfter.includes(index + 1) && index < length - 1 && (\n                <span className={cx(styles.otpInputSeparator, classNames?.separator)} style={slotStyles?.separator} />\n              )}\n            </div>\n          );\n        })}\n        {/* Inside the boxes, which the tooltip anchors to: the root stretches in a flex column,\n            and anchored to it the tooltip pointed at the empty space beside the last box. */}\n        <Tooltip variant='error' message={displayedErrorMessage} isVisible={showError && (isFocused || isHovered)} />\n      </div>\n    </div>\n  );\n}\n\nexport default OtpInput;\n"],"mappings":"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"}