@yamada-ui/pin-input
Version:
Yamada UI pin input component
292 lines (290 loc) • 9.17 kB
JavaScript
"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