{"version":3,"sources":["../src/use-pin-input.ts"],"sourcesContent":["import { createDescendantContext } from \"@chakra-ui/descendant\"\nimport { useControllableState } from \"@chakra-ui/react-use-controllable-state\"\nimport { ariaAttr, callAllHandlers } from \"@chakra-ui/shared-utils\"\nimport { createContext } from \"@chakra-ui/react-context\"\nimport { mergeRefs } from \"@chakra-ui/react-use-merge-refs\"\nimport { useCallback, useEffect, useState, useId } from \"react\"\n\n/* -------------------------------------------------------------------------------------------------\n * Create context to track descendants and their indices\n * -----------------------------------------------------------------------------------------------*/\n\nexport const [\n  PinInputDescendantsProvider,\n  usePinInputDescendantsContext,\n  usePinInputDescendants,\n  usePinInputDescendant,\n] = createDescendantContext<HTMLInputElement>()\n\n/* -------------------------------------------------------------------------------------------------\n * Create context that stores pin-input logic\n * -----------------------------------------------------------------------------------------------*/\n\nexport type PinInputContext = Omit<UsePinInputReturn, \"descendants\"> & {\n  /**\n   * Sets the pin input component to the disabled state\n   */\n  isDisabled?: boolean\n  /**\n   * Sets the pin input component to the invalid state\n   */\n  isInvalid?: boolean\n}\n\nexport const [PinInputProvider, usePinInputContext] =\n  createContext<PinInputContext>({\n    name: \"PinInputContext\",\n    errorMessage:\n      \"usePinInputContext: `context` is undefined. Seems you forgot to all pin input fields within `<PinInput />`\",\n  })\n\n/* -------------------------------------------------------------------------------------------------\n * usePinInput hook\n * -----------------------------------------------------------------------------------------------*/\n\nexport interface UsePinInputProps {\n  /**\n   * If `true`, the pin input receives focus on mount\n   */\n  autoFocus?: boolean\n  /**\n   * The value of the pin input. This is the value\n   * that will be returned when the pin input is filled\n   */\n  value?: string\n  /**\n   * The default value of the pin input\n   */\n  defaultValue?: string\n  /**\n   * Function called on input change\n   */\n  onChange?: (value: string) => void\n  /**\n   * Function called when all inputs have valid values\n   */\n  onComplete?: (value: string) => void\n  /**\n   * The placeholder for the pin input\n   */\n  placeholder?: string\n  /**\n   * If `true`, focus will move automatically to the next input once filled\n   * @default true\n   */\n  manageFocus?: boolean\n  /**\n   * If `true`, the pin input component signals to its fields that they should\n   * use `autocomplete=\"one-time-code\"`.\n   */\n  otp?: boolean\n  /**\n   * The top-level id string that will be applied to the input fields.\n   * The index of the input will be appended to this top-level id.\n   *\n   * @example\n   * if id=\"foo\", the first input will have `foo-0`\n   */\n  id?: string\n  /**\n   * If `true`, the pin input component is put in the disabled state\n   */\n  isDisabled?: boolean\n  /**\n   * If `true`, the pin input component is put in the invalid state\n   */\n  isInvalid?: boolean\n  /**\n   * The type of values the pin-input should allow\n   */\n  type?: \"alphanumeric\" | \"number\"\n  /**\n   * If `true`, the input's value will be masked just like `type=password`\n   */\n  mask?: boolean\n}\n\nconst toArray = (value?: string) => value?.split(\"\")\n\nfunction validate(value: string, type: UsePinInputProps[\"type\"]) {\n  const NUMERIC_REGEX = /^[0-9]+$/\n  const ALPHA_NUMERIC_REGEX = /^[a-zA-Z0-9]+$/i\n  const regex = type === \"alphanumeric\" ? ALPHA_NUMERIC_REGEX : NUMERIC_REGEX\n  return regex.test(value)\n}\n\n/* -------------------------------------------------------------------------------------------------\n * usePinInput - handles the general pin input logic\n * -----------------------------------------------------------------------------------------------*/\n\n/**\n * @internal\n */\nexport function usePinInput(props: UsePinInputProps = {}) {\n  const {\n    autoFocus,\n    value,\n    defaultValue,\n    onChange,\n    onComplete,\n    placeholder = \"○\",\n    manageFocus = true,\n    otp = false,\n    id: idProp,\n    isDisabled,\n    isInvalid,\n    type = \"number\",\n    mask,\n  } = props\n\n  const uuid = useId()\n  const id = idProp ?? `pin-input-${uuid}`\n\n  const descendants = usePinInputDescendants()\n\n  const [moveFocus, setMoveFocus] = useState(true)\n  const [focusedIndex, setFocusedIndex] = useState(-1)\n\n  const [values, setValues] = useControllableState<string[]>({\n    defaultValue: toArray(defaultValue) || [],\n    value: toArray(value),\n    onChange: (values) => onChange?.(values.join(\"\")),\n  })\n\n  useEffect(() => {\n    if (autoFocus) {\n      const first = descendants.first()\n      if (first) {\n        requestAnimationFrame(() => {\n          first.node.focus()\n        })\n      }\n    }\n    // We don't want to listen for updates to `autoFocus` since it only runs initially\n    // eslint-disable-next-line\n  }, [descendants])\n\n  const focusNext = useCallback(\n    (index: number) => {\n      if (!moveFocus || !manageFocus) return\n      const next = descendants.next(index, false)\n      if (next) {\n        requestAnimationFrame(() => {\n          next.node.focus()\n        })\n      }\n    },\n    [descendants, moveFocus, manageFocus],\n  )\n\n  const setValue = useCallback(\n    (value: string, index: number, handleFocus: boolean = true) => {\n      const nextValues = [...values]\n      nextValues[index] = value\n      setValues(nextValues)\n\n      const isComplete =\n        value !== \"\" &&\n        nextValues.length === descendants.count() &&\n        nextValues.every(\n          (inputValue) => inputValue != null && inputValue !== \"\",\n        )\n\n      if (isComplete) {\n        onComplete?.(nextValues.join(\"\"))\n      } else {\n        if (handleFocus) focusNext(index)\n      }\n    },\n    [values, setValues, focusNext, onComplete, descendants],\n  )\n\n  const clear = useCallback(() => {\n    const values: string[] = Array(descendants.count()).fill(\"\")\n    setValues(values)\n    const first = descendants.first()\n    first?.node?.focus()\n  }, [descendants, setValues])\n\n  const getNextValue = useCallback((value: string, eventValue: string) => {\n    let nextValue = eventValue\n    if (value?.length > 0) {\n      if (value[0] === eventValue.charAt(0)) {\n        nextValue = eventValue.charAt(1)\n      } else if (value[0] === eventValue.charAt(1)) {\n        nextValue = eventValue.charAt(0)\n      }\n    }\n    return nextValue\n  }, [])\n\n  const getInputProps = useCallback(\n    (props: InputProps & { index: number }): InputProps => {\n      const { index, ...rest } = props\n\n      /**\n       * Improved from: https://github.com/uber/baseweb/blob/master/src/pin-code/pin-code.js\n       */\n      const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n        const eventValue = event.target.value\n        const currentValue = values[index]\n        const nextValue = getNextValue(currentValue, eventValue)\n\n        // if the value was removed using backspace\n        if (nextValue === \"\") {\n          setValue(\"\", index)\n          return\n        }\n\n        // in the case of an autocomplete or copy and paste\n        if (eventValue.length > 2) {\n          // see if we can use the string to fill out our values\n          if (validate(eventValue, type)) {\n            // Ensure the value matches the number of inputs\n            const nextValue = eventValue\n              .split(\"\")\n              .filter((_, index) => index < descendants.count())\n\n            setValues(nextValue)\n\n            // if pasting fills the entire input fields, trigger `onComplete`\n            if (nextValue.length === descendants.count()) {\n              onComplete?.(nextValue.join(\"\"))\n            }\n          }\n        } else {\n          // only set if the new value is a number\n          if (validate(nextValue, type)) {\n            setValue(nextValue, index)\n          }\n\n          setMoveFocus(true)\n        }\n      }\n\n      const onKeyDown = (event: React.KeyboardEvent) => {\n        if (event.key === \"Backspace\" && manageFocus) {\n          if ((event.target as HTMLInputElement).value === \"\") {\n            const prevInput = descendants.prev(index, false)\n            if (prevInput) {\n              setValue(\"\", index - 1, false)\n              prevInput.node?.focus()\n              setMoveFocus(true)\n            }\n          } else {\n            setMoveFocus(false)\n          }\n        }\n      }\n\n      const onFocus = () => {\n        setFocusedIndex(index)\n      }\n\n      const onBlur = () => {\n        setFocusedIndex(-1)\n      }\n\n      const hasFocus = focusedIndex === index\n      const inputType = type === \"number\" ? \"tel\" : \"text\"\n\n      return {\n        \"aria-label\": \"Please enter your pin code\",\n        inputMode: type === \"number\" ? \"numeric\" : \"text\",\n        type: mask ? \"password\" : inputType,\n        ...rest,\n        id: `${id}-${index}`,\n        disabled: isDisabled,\n        \"aria-invalid\": ariaAttr(isInvalid),\n        onChange: callAllHandlers(rest.onChange, onChange),\n        onKeyDown: callAllHandlers(rest.onKeyDown, onKeyDown),\n        onFocus: callAllHandlers(rest.onFocus, onFocus),\n        onBlur: callAllHandlers(rest.onBlur, onBlur),\n        value: values[index] || \"\",\n        autoComplete: otp ? \"one-time-code\" : \"off\",\n        placeholder: hasFocus ? \"\" : placeholder,\n      }\n    },\n    [\n      descendants,\n      focusedIndex,\n      getNextValue,\n      id,\n      isDisabled,\n      mask,\n      isInvalid,\n      manageFocus,\n      onComplete,\n      otp,\n      placeholder,\n      setValue,\n      setValues,\n      type,\n      values,\n    ],\n  )\n\n  return {\n    // prop getter\n    getInputProps,\n    // state\n    id,\n    descendants,\n    values,\n    // actions\n    setValue,\n    setValues,\n    clear,\n  }\n}\n\nexport type UsePinInputReturn = ReturnType<typeof usePinInput>\n\nexport interface UsePinInputFieldProps extends InputProps {\n  ref?: React.Ref<HTMLInputElement>\n}\n\n/**\n * @internal\n */\nexport function usePinInputField(\n  props: UsePinInputFieldProps = {},\n  ref: React.Ref<any> = null,\n) {\n  const { getInputProps } = usePinInputContext()\n  const { index, register } = usePinInputDescendant()\n\n  return getInputProps({\n    ...props,\n    ref: mergeRefs(register, ref),\n    index,\n  })\n}\n\ninterface InputProps\n  extends Omit<\n    React.ComponentPropsWithRef<\"input\">,\n    \"color\" | \"height\" | \"width\"\n  > {}\n"],"mappings":";;;AAAA,SAAS,+BAA+B;AACxC,SAAS,4BAA4B;AACrC,SAAS,UAAU,uBAAuB;AAC1C,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAC1B,SAAS,aAAa,WAAW,UAAU,aAAa;AAMjD,IAAM;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,IAAI,wBAA0C;AAiBvC,IAAM,CAAC,kBAAkB,kBAAkB,IAChD,cAA+B;AAAA,EAC7B,MAAM;AAAA,EACN,cACE;AACJ,CAAC;AAoEH,IAAM,UAAU,CAAC,UAAmB,+BAAO,MAAM;AAEjD,SAAS,SAAS,OAAe,MAAgC;AAC/D,QAAM,gBAAgB;AACtB,QAAM,sBAAsB;AAC5B,QAAM,QAAQ,SAAS,iBAAiB,sBAAsB;AAC9D,SAAO,MAAM,KAAK,KAAK;AACzB;AASO,SAAS,YAAY,QAA0B,CAAC,GAAG;AACxD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,cAAc;AAAA,IACd,MAAM;AAAA,IACN,IAAI;AAAA,IACJ;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP;AAAA,EACF,IAAI;AAEJ,QAAM,OAAO,MAAM;AACnB,QAAM,KAAK,0BAAU,aAAa;AAElC,QAAM,cAAc,uBAAuB;AAE3C,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAC/C,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,EAAE;AAEnD,QAAM,CAAC,QAAQ,SAAS,IAAI,qBAA+B;AAAA,IACzD,cAAc,QAAQ,YAAY,KAAK,CAAC;AAAA,IACxC,OAAO,QAAQ,KAAK;AAAA,IACpB,UAAU,CAACA,YAAW,qCAAWA,QAAO,KAAK,EAAE;AAAA,EACjD,CAAC;AAED,YAAU,MAAM;AACd,QAAI,WAAW;AACb,YAAM,QAAQ,YAAY,MAAM;AAChC,UAAI,OAAO;AACT,8BAAsB,MAAM;AAC1B,gBAAM,KAAK,MAAM;AAAA,QACnB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EAGF,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,YAAY;AAAA,IAChB,CAAC,UAAkB;AACjB,UAAI,CAAC,aAAa,CAAC;AAAa;AAChC,YAAM,OAAO,YAAY,KAAK,OAAO,KAAK;AAC1C,UAAI,MAAM;AACR,8BAAsB,MAAM;AAC1B,eAAK,KAAK,MAAM;AAAA,QAClB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,IACA,CAAC,aAAa,WAAW,WAAW;AAAA,EACtC;AAEA,QAAM,WAAW;AAAA,IACf,CAACC,QAAe,OAAe,cAAuB,SAAS;AAC7D,YAAM,aAAa,CAAC,GAAG,MAAM;AAC7B,iBAAW,KAAK,IAAIA;AACpB,gBAAU,UAAU;AAEpB,YAAM,aACJA,WAAU,MACV,WAAW,WAAW,YAAY,MAAM,KACxC,WAAW;AAAA,QACT,CAAC,eAAe,cAAc,QAAQ,eAAe;AAAA,MACvD;AAEF,UAAI,YAAY;AACd,iDAAa,WAAW,KAAK,EAAE;AAAA,MACjC,OAAO;AACL,YAAI;AAAa,oBAAU,KAAK;AAAA,MAClC;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,WAAW,WAAW,YAAY,WAAW;AAAA,EACxD;AAEA,QAAM,QAAQ,YAAY,MAAM;AAzMlC;AA0MI,UAAMD,UAAmB,MAAM,YAAY,MAAM,CAAC,EAAE,KAAK,EAAE;AAC3D,cAAUA,OAAM;AAChB,UAAM,QAAQ,YAAY,MAAM;AAChC,yCAAO,SAAP,mBAAa;AAAA,EACf,GAAG,CAAC,aAAa,SAAS,CAAC;AAE3B,QAAM,eAAe,YAAY,CAACC,QAAe,eAAuB;AACtE,QAAI,YAAY;AAChB,SAAIA,UAAA,gBAAAA,OAAO,UAAS,GAAG;AACrB,UAAIA,OAAM,CAAC,MAAM,WAAW,OAAO,CAAC,GAAG;AACrC,oBAAY,WAAW,OAAO,CAAC;AAAA,MACjC,WAAWA,OAAM,CAAC,MAAM,WAAW,OAAO,CAAC,GAAG;AAC5C,oBAAY,WAAW,OAAO,CAAC;AAAA,MACjC;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEL,QAAM,gBAAgB;AAAA,IACpB,CAACC,WAAsD;AACrD,YAAM,EAAE,OAAO,GAAG,KAAK,IAAIA;AAK3B,YAAMC,YAAW,CAAC,UAA+C;AAC/D,cAAM,aAAa,MAAM,OAAO;AAChC,cAAM,eAAe,OAAO,KAAK;AACjC,cAAM,YAAY,aAAa,cAAc,UAAU;AAGvD,YAAI,cAAc,IAAI;AACpB,mBAAS,IAAI,KAAK;AAClB;AAAA,QACF;AAGA,YAAI,WAAW,SAAS,GAAG;AAEzB,cAAI,SAAS,YAAY,IAAI,GAAG;AAE9B,kBAAMC,aAAY,WACf,MAAM,EAAE,EACR,OAAO,CAAC,GAAGC,WAAUA,SAAQ,YAAY,MAAM,CAAC;AAEnD,sBAAUD,UAAS;AAGnB,gBAAIA,WAAU,WAAW,YAAY,MAAM,GAAG;AAC5C,uDAAaA,WAAU,KAAK,EAAE;AAAA,YAChC;AAAA,UACF;AAAA,QACF,OAAO;AAEL,cAAI,SAAS,WAAW,IAAI,GAAG;AAC7B,qBAAS,WAAW,KAAK;AAAA,UAC3B;AAEA,uBAAa,IAAI;AAAA,QACnB;AAAA,MACF;AAEA,YAAM,YAAY,CAAC,UAA+B;AAxQxD;AAyQQ,YAAI,MAAM,QAAQ,eAAe,aAAa;AAC5C,cAAK,MAAM,OAA4B,UAAU,IAAI;AACnD,kBAAM,YAAY,YAAY,KAAK,OAAO,KAAK;AAC/C,gBAAI,WAAW;AACb,uBAAS,IAAI,QAAQ,GAAG,KAAK;AAC7B,8BAAU,SAAV,mBAAgB;AAChB,2BAAa,IAAI;AAAA,YACnB;AAAA,UACF,OAAO;AACL,yBAAa,KAAK;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AAEA,YAAM,UAAU,MAAM;AACpB,wBAAgB,KAAK;AAAA,MACvB;AAEA,YAAM,SAAS,MAAM;AACnB,wBAAgB,EAAE;AAAA,MACpB;AAEA,YAAM,WAAW,iBAAiB;AAClC,YAAM,YAAY,SAAS,WAAW,QAAQ;AAE9C,aAAO;AAAA,QACL,cAAc;AAAA,QACd,WAAW,SAAS,WAAW,YAAY;AAAA,QAC3C,MAAM,OAAO,aAAa;AAAA,QAC1B,GAAG;AAAA,QACH,IAAI,GAAG,MAAM;AAAA,QACb,UAAU;AAAA,QACV,gBAAgB,SAAS,SAAS;AAAA,QAClC,UAAU,gBAAgB,KAAK,UAAUD,SAAQ;AAAA,QACjD,WAAW,gBAAgB,KAAK,WAAW,SAAS;AAAA,QACpD,SAAS,gBAAgB,KAAK,SAAS,OAAO;AAAA,QAC9C,QAAQ,gBAAgB,KAAK,QAAQ,MAAM;AAAA,QAC3C,OAAO,OAAO,KAAK,KAAK;AAAA,QACxB,cAAc,MAAM,kBAAkB;AAAA,QACtC,aAAa,WAAW,KAAK;AAAA,MAC/B;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA;AAAA,IAEL;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAWO,SAAS,iBACd,QAA+B,CAAC,GAChC,MAAsB,MACtB;AACA,QAAM,EAAE,cAAc,IAAI,mBAAmB;AAC7C,QAAM,EAAE,OAAO,SAAS,IAAI,sBAAsB;AAElD,SAAO,cAAc;AAAA,IACnB,GAAG;AAAA,IACH,KAAK,UAAU,UAAU,GAAG;AAAA,IAC5B;AAAA,EACF,CAAC;AACH;","names":["values","value","props","onChange","nextValue","index"]}