@yamada-ui/react
Version:
React UI components of the Yamada, by the Yamada, for the Yamada built with React and Emotion
181 lines (177 loc) • 6.04 kB
JavaScript
"use client";
import { runKeyAction } from "../../utils/dom.js";
import { utils_exports } from "../../utils/index.js";
import { useControllableState } from "../../hooks/use-controllable-state/index.js";
import { createDescendants } from "../../hooks/use-descendants/index.js";
import { useFieldProps } from "../field/use-field-props.js";
import { useCallback, useEffect, useId, useState } from "react";
//#region src/components/pin-input/use-pin-input.ts
const { DescendantsContext: PinInputDescendantsContext, useDescendant: usePinInputDescendant, useDescendants: usePinInputDescendants } = createDescendants();
const toArray = (value) => value?.split("");
const validate = (value, type) => {
return (type === "alphanumeric" ? /^[a-zA-Z0-9]+$/i : /^[0-9]+$/).test(value);
};
const usePinInput = (props = {}) => {
const uuid = useId();
const { props: { id = uuid, type = "number", autoFocus, defaultValue, disabled, manageFocus = true, mask, otp = false, placeholder = "◯", readOnly, value, onChange: onChangeProp, onComplete,...rest }, ariaProps, dataProps, eventProps } = useFieldProps(props);
const descendants = usePinInputDescendants();
const [moveFocus, setMoveFocus] = useState(true);
const [focusedIndex, setFocusedIndex] = useState(-1);
const [values, setValues] = useControllableState({
defaultValue: toArray(defaultValue) || [],
value: toArray(value),
onChange: (values$1) => onChangeProp?.(values$1.join(""))
});
const focusNext = useCallback((index) => {
if (!moveFocus || !manageFocus) return;
const next = descendants.nextValue(index, false);
if (!next) return;
requestAnimationFrame(() => next.node.focus());
}, [
descendants,
moveFocus,
manageFocus
]);
const focusInputField = useCallback((direction, index) => {
const input = direction === "next" ? descendants.nextValue(index, false) : descendants.prevValue(index, false);
if (!input) return;
const valueLength = input.node.value.length;
requestAnimationFrame(() => {
input.node.focus();
input.node.setSelectionRange(0, valueLength);
});
}, [descendants]);
const setValue = useCallback((value$1, index, focus = true) => {
let nextValues = [...values];
nextValues[index] = value$1;
setValues(nextValues);
nextValues = nextValues.filter(Boolean);
if (value$1 !== "" && nextValues.length === descendants.count()) {
onComplete?.(nextValues.join(""));
descendants.value(index)?.node.blur();
} else if (focus) focusNext(index);
}, [
values,
setValues,
descendants,
onComplete,
focusNext
]);
const getNextValue = useCallback((value$1, eventValue) => {
let nextValue = eventValue;
if (!value$1?.length) return nextValue;
if (value$1.startsWith(eventValue.charAt(0))) nextValue = eventValue.charAt(1);
else if (value$1.startsWith(eventValue.charAt(1))) nextValue = eventValue.charAt(0);
return nextValue;
}, []);
const onChange = useCallback((index) => ({ target }) => {
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 nextValue$1 = eventValue.split("").filter((_, index$1) => index$1 < descendants.count());
setValues(nextValue$1);
if (nextValue$1.length === descendants.count()) {
onComplete?.(nextValue$1.join(""));
descendants.value(index)?.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;
runKeyAction(ev, {
ArrowLeft: (ev$1) => {
ev$1.preventDefault();
focusInputField("prev", index);
},
ArrowRight: (ev$1) => {
ev$1.preventDefault();
focusInputField("next", index);
},
Backspace: (ev$1) => {
if (ev$1.target.value === "") {
const prevInput = descendants.prevValue(index, false);
if (!prevInput) return;
setValue("", index - 1, false);
prevInput.node.focus();
setMoveFocus(true);
} else setMoveFocus(false);
}
}, { preventDefault: false });
}, [
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 getRootProps = useCallback((props$1) => ({
role: "group",
...rest,
...props$1
}), [rest]);
return {
descendants,
getInputProps: useCallback(({ index,...props$1 }) => ({
...ariaProps,
...dataProps,
type: mask ? "password" : type === "number" ? "tel" : "text",
autoComplete: otp ? "one-time-code" : "off",
disabled,
inputMode: type === "number" ? "numeric" : "text",
placeholder: focusedIndex === index && !readOnly && !props$1.readOnly ? "" : placeholder,
readOnly,
value: values[index] || "",
...(0, utils_exports.filterUndefined)(props$1),
id: `${id}${index ? `-${index}` : ""}`,
onBlur: (0, utils_exports.handlerAll)(eventProps.onBlur, props$1.onBlur, onBlur),
onChange: (0, utils_exports.handlerAll)(props$1.onChange, onChange(index)),
onFocus: (0, utils_exports.handlerAll)(eventProps.onFocus, props$1.onFocus, onFocus(index)),
onKeyDown: (0, utils_exports.handlerAll)(props$1.onKeyDown, onKeyDown(index))
}), [
ariaProps,
dataProps,
eventProps,
mask,
type,
disabled,
readOnly,
id,
otp,
focusedIndex,
placeholder,
values,
onBlur,
onChange,
onFocus,
onKeyDown
]),
getRootProps
};
};
//#endregion
export { PinInputDescendantsContext, usePinInput, usePinInputDescendant, usePinInputDescendants };
//# sourceMappingURL=use-pin-input.js.map