UNPKG

@yamada-ui/pin-input

Version:

Yamada UI pin input component

292 lines (290 loc) • 9.17 kB
"use client" // src/pin-input.tsx import { forwardRef, omitThemeProps, ui, useComponentMultiStyle } from "@yamada-ui/core"; import { formControlProperties, useFormControlProps } from "@yamada-ui/form-control"; import { useControllableState } from "@yamada-ui/use-controllable-state"; import { createDescendant } from "@yamada-ui/use-descendant"; import { createContext, cx, filterUndefined, getValidChildren, handlerAll, mergeRefs, splitObject } from "@yamada-ui/utils"; import { useCallback, useEffect, useId, useState } from "react"; import { jsx } from "react/jsx-runtime"; var toArray = (value) => value == null ? void 0 : value.split(""); var validate = (value, type) => { const NUMERIC_REGEX = /^[0-9]+$/; const ALPHA_NUMERIC_REGEX = /^[a-zA-Z0-9]+$/i; const regex = type === "alphanumeric" ? ALPHA_NUMERIC_REGEX : NUMERIC_REGEX; return regex.test(value); }; var [PinInputProvider, usePinInputContext] = createContext({ name: "PinInputContext", errorMessage: `PinInputContext returned is 'undefined'. Seems you forgot to wrap the components in "<PinInput />"` }); var { DescendantsContextProvider, useDescendant, useDescendants } = createDescendant(); var PinInput = forwardRef( ({ errorBorderColor, focusBorderColor, ...props }, ref) => { const [styles, mergedProps] = useComponentMultiStyle("PinInput", { errorBorderColor, focusBorderColor, ...props }); const uuid = useId(); const { id = uuid, type = "number", className, autoFocus, children, defaultValue, items = 4, manageFocus = true, mask, otp = false, placeholder = "\u25CB", value, onChange: onChangeProp, onComplete, ...rest } = useFormControlProps(omitThemeProps(mergedProps)); const [ { "aria-readonly": _ariaReadonly, disabled, readOnly, ...formControlProps }, containerProps ] = splitObject(rest, formControlProperties); const descendants = useDescendants(); const [moveFocus, setMoveFocus] = useState(true); const [focusedIndex, setFocusedIndex] = useState(-1); const [values, setValues] = useControllableState({ defaultValue: toArray(defaultValue) || [], value: toArray(value), onChange: (values2) => onChangeProp == null ? void 0 : onChangeProp(values2.join("")) }); const css = { alignItems: "center", display: "flex", ...styles.container }; const focusNext = useCallback( (index) => { if (!moveFocus || !manageFocus) return; const next = descendants.nextValue(index, void 0, false); if (!next) return; requestAnimationFrame(() => next.node.focus()); }, [descendants, moveFocus, manageFocus] ); const focusInputField = useCallback( (direction, index) => { const input = direction === "next" ? descendants.nextValue(index, void 0, false) : descendants.prevValue(index, void 0, false); if (!input) return; const valueLength = input.node.value.length; requestAnimationFrame(() => { input.node.focus(); input.node.setSelectionRange(0, valueLength); }); }, [descendants] ); const setValue = useCallback( (value2, index, isFocus = true) => { var _a; let nextValues = [...values]; nextValues[index] = value2; setValues(nextValues); nextValues = nextValues.filter(Boolean); const isComplete = value2 !== "" && nextValues.length === descendants.count(); if (isComplete) { onComplete == null ? void 0 : onComplete(nextValues.join("")); (_a = descendants.value(index)) == null ? void 0 : _a.node.blur(); } else if (isFocus) { focusNext(index); } }, [values, setValues, descendants, onComplete, focusNext] ); const getNextValue = useCallback( (value2, eventValue) => { let nextValue = eventValue; if (!(value2 == null ? void 0 : value2.length)) return nextValue; if (value2.startsWith(eventValue.charAt(0))) { nextValue = eventValue.charAt(1); } else if (value2.startsWith(eventValue.charAt(1))) { nextValue = eventValue.charAt(0); } return nextValue; }, [] ); const onChange = useCallback( (index) => ({ target }) => { var _a; const eventValue = target.value; const currentValue = values[index]; const nextValue = getNextValue(currentValue, eventValue); if (nextValue === "") { setValue("", index); return; } if (eventValue.length > 2) { if (!validate(eventValue, type)) return; const nextValue2 = eventValue.split("").filter((_, index2) => index2 < descendants.count()); setValues(nextValue2); if (nextValue2.length === descendants.count()) { onComplete == null ? void 0 : onComplete(nextValue2.join("")); (_a = descendants.value(index)) == null ? void 0 : _a.node.blur(); } } else { if (validate(nextValue, type)) setValue(nextValue, index); setMoveFocus(true); } }, [ descendants, getNextValue, onComplete, setValue, setValues, type, values ] ); const onKeyDown = useCallback( (index) => (ev) => { if (!manageFocus) return; const actions = { ArrowLeft: () => { ev.preventDefault(); focusInputField("prev", index); }, ArrowRight: () => { ev.preventDefault(); focusInputField("next", index); }, Backspace: () => { if (ev.target.value === "") { const prevInput = descendants.prevValue(index, void 0, false); if (!prevInput) return; setValue("", index - 1, false); prevInput.node.focus(); setMoveFocus(true); } else { setMoveFocus(false); } } }; const action = actions[ev.key]; if (!action) return; action(); }, [descendants, focusInputField, manageFocus, setValue] ); const onFocus = useCallback( (index) => () => setFocusedIndex(index), [] ); const onBlur = useCallback(() => setFocusedIndex(-1), []); useEffect(() => { if (!autoFocus) return; const firstValue = descendants.firstValue(); if (!firstValue) return; requestAnimationFrame(() => firstValue.node.focus()); }, [autoFocus, descendants]); const getInputProps = useCallback( ({ index, ...props2 }) => ({ type: mask ? "password" : type === "number" ? "tel" : "text", disabled, inputMode: type === "number" ? "numeric" : "text", readOnly, ...formControlProps, ...filterUndefined(props2), id: `${id}-${index}`, autoComplete: otp ? "one-time-code" : "off", placeholder: focusedIndex === index && !readOnly && !props2.readOnly ? "" : placeholder, value: values[index] || "", onBlur: handlerAll(props2.onBlur, onBlur), onChange: handlerAll(props2.onChange, onChange(index)), onFocus: handlerAll(props2.onFocus, onFocus(index)), onKeyDown: handlerAll(props2.onKeyDown, onKeyDown(index)) }), [ type, mask, formControlProps, id, values, onChange, onKeyDown, onFocus, onBlur, otp, focusedIndex, disabled, readOnly, placeholder ] ); let cloneChildren = getValidChildren(children); if (!cloneChildren.length) for (let i = 0; i < items; i++) { cloneChildren.push(/* @__PURE__ */ jsx(PinInputField, {}, i)); } return /* @__PURE__ */ jsx(DescendantsContextProvider, { value: descendants, children: /* @__PURE__ */ jsx(PinInputProvider, { value: { styles, getInputProps }, children: /* @__PURE__ */ jsx( ui.div, { ref, className: cx("ui-pin-input", className), role: "group", __css: css, ...formControlProps, ...containerProps, children: cloneChildren } ) }) }); } ); PinInput.displayName = "PinInput"; PinInput.__ui__ = "PinInput"; var PinInputField = forwardRef( ({ className, ...rest }, ref) => { const { styles, getInputProps } = usePinInputContext(); const { index, register } = useDescendant(); const css = { ...styles.field }; rest = useFormControlProps(rest); return /* @__PURE__ */ jsx( ui.input, { className: cx("ui-pin-input__field", className), ...getInputProps({ ...rest, ref: mergeRefs(register, ref), index }), __css: css } ); } ); PinInputField.displayName = "PinInputField"; PinInputField.__ui__ = "PinInputField"; export { PinInput, PinInputField }; //# sourceMappingURL=chunk-SUK2AVTG.mjs.map