@julo-ui/otp-input
Version:
React Input Component for entering sequences of digits
450 lines (425 loc) • 13.4 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
OtpInput: () => OtpInput_default,
OtpInputDescendantsProvider: () => OtpInputDescendantsProvider,
OtpInputField: () => OtpInputField_default,
OtpInputProvider: () => OtpInputProvider,
UseOtpInputProvider: () => UseOtpInputProvider,
default: () => OtpInput_default,
useOtpInput: () => useOtpInput,
useOtpInputContext: () => useOtpInputContext,
useOtpInputDescendant: () => useOtpInputDescendant,
useOtpInputDescendants: () => useOtpInputDescendants,
useOtpInputDescendantsContext: () => useOtpInputDescendantsContext,
useOtpInputField: () => useOtpInputField
});
module.exports = __toCommonJS(src_exports);
// src/OtpInput.tsx
var import_system = require("@julo-ui/system");
// src/OtpInputProvider.tsx
var import_context = require("@julo-ui/context");
// src/use-otp-input.ts
var import_react4 = require("react");
var import_descendant = require("@chakra-ui/descendant");
var import_function_utils2 = require("@julo-ui/function-utils");
var import_dom_utils = require("@julo-ui/dom-utils");
// src/usecase/use-handle-focus.ts
var import_react = require("react");
function useHandleFocus(options) {
const { autoFocus, manageFocus, descendants } = options;
const [focusedIndex, setFocusedIndex] = (0, import_react.useState)(-1);
const onFocusNext = (0, import_react.useCallback)(
(index) => {
if (!manageFocus)
return;
const next = descendants.next(index, false);
if (next)
requestAnimationFrame(() => {
next.node.focus();
});
},
[descendants, manageFocus]
);
const onFocus = (0, import_react.useCallback)((index) => {
setFocusedIndex(index);
}, []);
const onBlur = (0, import_react.useCallback)(() => {
setFocusedIndex(-1);
}, []);
(0, import_react.useEffect)(() => {
if (autoFocus) {
const first = descendants.first();
if (first) {
requestAnimationFrame(() => {
first.node.focus();
});
}
}
}, [descendants]);
return {
focusedIndex,
onFocusNext,
onFocus,
onBlur
};
}
// src/usecase/use-handle-values.ts
var import_react2 = require("react");
var import_function_utils = require("@julo-ui/function-utils");
var import_use_controllable_state = require("@julo-ui/use-controllable-state");
// src/utils.ts
var strToArray = (value) => value == null ? void 0 : value.split("");
var validateValue = (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);
};
// src/usecase/use-handle-values.ts
function useHandleValues(options) {
const {
onChange,
defaultValue,
value,
descendants,
onComplete,
onFocusNext
} = options;
const [values, setValues] = (0, import_use_controllable_state.useControllableState)({
defaultValue: strToArray(defaultValue) || [],
value: strToArray(value),
onChange: (values2) => onChange(values2.join(""))
});
const setValue = (0, import_react2.useCallback)(
(value2, index, handleFocus = true) => {
const nextValues = [...values];
nextValues[index] = value2;
setValues(nextValues);
const isComplete = value2 !== "" && nextValues.length === descendants.count() && nextValues.every(
(inputValue) => !(0, import_function_utils.isTrullyEmpty)(inputValue) && inputValue !== ""
);
if (!isComplete && handleFocus)
return onFocusNext(index);
onComplete(nextValues.join(""));
},
[descendants, onComplete, onFocusNext, setValues, values]
);
const clearValue = (0, import_react2.useCallback)(() => {
const values2 = Array(descendants.count()).fill("");
setValues(values2);
onFocusNext(0);
}, [descendants, onFocusNext, setValues]);
const getNextValue = (0, import_react2.useCallback)((value2, eventValue) => {
if (!value2 || (value2 == null ? void 0 : value2.length) === 0)
return eventValue;
if (value2[0] === eventValue.charAt(0))
return eventValue.charAt(1);
return eventValue.charAt(0);
}, []);
return { values, setValues, setValue, clearValue, getNextValue };
}
// src/usecase/use-handle-input-event.ts
var import_react3 = require("react");
function useHandleInputEvent(options) {
const {
values,
getNextValue,
setValue,
type,
descendants,
setValues,
onComplete,
manageFocus,
onFocusNext
} = options;
const onChange = (0, import_react3.useCallback)(
(event, index) => {
const eventValue = event.target.value;
const currentValue = values[index];
const nextValue = getNextValue(currentValue, eventValue);
if (nextValue === "")
return setValue("", index);
if (eventValue.length > 2) {
if (!validateValue(eventValue, type))
return;
const nextValue2 = eventValue.split("").filter((_, index2) => index2 < descendants.count());
setValues(nextValue2);
onFocusNext(eventValue.length - 2);
if (nextValue2.length === descendants.count()) {
onComplete(nextValue2.join(""));
}
return;
}
if (validateValue(nextValue, type)) {
setValue(nextValue, index);
}
},
[
descendants,
getNextValue,
onComplete,
onFocusNext,
setValue,
setValues,
type,
values
]
);
const onKeyDown = (0, import_react3.useCallback)(
(event, index) => {
if (event.key !== "Backspace" || !manageFocus)
return;
if (event.target.value !== "")
return;
const prevInput = descendants.prev(index, false);
if (!prevInput)
return;
setValue("", index - 1, false);
prevInput.node.focus();
},
[descendants, manageFocus, setValue]
);
return { onChange, onKeyDown };
}
// src/use-otp-input.ts
var [
OtpInputDescendantsProvider,
useOtpInputDescendantsContext,
useOtpInputDescendants,
useOtpInputDescendant
] = (0, import_descendant.createDescendantContext)();
function useOtpInput(props) {
const {
autoFocus = false,
value,
defaultValue,
onChange = import_function_utils2._noop,
onComplete = import_function_utils2._noop,
placeholder,
manageFocus = true,
otp = true,
id: idProp,
isDisabled,
isInvalid,
type = "number",
mask,
...resRootProps
} = props;
const uuid = (0, import_react4.useId)();
const id = idProp != null ? idProp : uuid;
const rootId = `otp-input-${id}`;
const fieldId = `otp-input-field-${id}`;
const descendants = useOtpInputDescendants();
const {
focusedIndex,
onFocusNext,
onBlur: onInputBlur,
onFocus: onInputFocus
} = useHandleFocus({ descendants, manageFocus, autoFocus });
const { values, setValue, setValues, clearValue, getNextValue } = useHandleValues({
onChange,
defaultValue,
value,
descendants,
onComplete,
onFocusNext
});
const { onChange: onInputChange, onKeyDown: onInputKeyDown } = useHandleInputEvent({
descendants,
getNextValue,
manageFocus,
onComplete,
setValue,
setValues,
type,
values,
onFocusNext
});
const getRootProps = (0, import_react4.useCallback)(
(props2 = {}, forwardedRef = null) => ({
...resRootProps,
...props2,
id: rootId,
ref: forwardedRef
}),
[resRootProps, rootId]
);
const getInputProps = (0, import_react4.useCallback)(
(props2 = { index: -1 }, forwardedRef = null) => {
const { index, onChange: onChange2, onKeyDown, onFocus, onBlur, ...resProps } = props2;
const hasFocus = focusedIndex === index;
const inputType = type === "number" ? "tel" : "text";
return {
"aria-label": "Please enter your otp code",
inputMode: type === "number" ? "numberic" : "text",
type: mask ? "password" : inputType,
ref: forwardedRef,
...resProps,
id: `${fieldId}-${index}`,
disabled: isDisabled,
"aria-invalid": (0, import_dom_utils.ariaAttr)(isInvalid),
onChange: (0, import_function_utils2.callAllFn)(
(e) => onInputChange(e, index),
onChange2
),
onKeyDown: (0, import_function_utils2.callAllFn)(
(e) => onInputKeyDown(e, index),
onKeyDown
),
onFocus: (0, import_function_utils2.callAllFn)(onInputFocus, onFocus),
onBlur: (0, import_function_utils2.callAllFn)(onInputBlur, onBlur),
value: values[index] || "",
autoComplete: otp ? "one-time-code" : "off",
placeholder: hasFocus ? "" : placeholder
};
},
[
fieldId,
focusedIndex,
isDisabled,
isInvalid,
mask,
onInputBlur,
onInputChange,
onInputFocus,
onInputKeyDown,
otp,
placeholder,
type,
values
]
);
return {
getRootProps,
getInputProps,
fieldId,
descendants,
values,
setValue,
setValues,
clearValue
};
}
// src/OtpInputProvider.tsx
var import_jsx_runtime = require("react/jsx-runtime");
var [UseOtpInputProvider, useOtpInputContext] = (0, import_context.createContext)({
name: "UseOtpInputContext",
providerName: "<UseOtpInputProvider />"
});
function OtpInputProvider(props) {
const {
children,
value: { descendants, ...context }
} = props;
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OtpInputDescendantsProvider, { value: descendants, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UseOtpInputProvider, { value: context, children }) });
}
// src/styles.ts
var import_react5 = require("@emotion/react");
var otpInputCx = import_react5.css`
display: flex;
.julo-otp-input__field {
&:not(:last-of-type) {
margin-right: 0.5rem;
}
}
`;
// src/OtpInput.tsx
var import_jsx_runtime2 = require("react/jsx-runtime");
var OtpInput = (0, import_system.forwardRef)((props, ref) => {
const { className, children, ...resProps } = props;
const { getRootProps, ...context } = useOtpInput(resProps);
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OtpInputProvider, { value: context, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
import_system.julo.div,
{
className: (0, import_system.cx)("julo-otp-input", className),
...getRootProps({}, ref),
__css: otpInputCx,
children
}
) });
});
OtpInput.displayName = "OtpInput";
var OtpInput_default = OtpInput;
// src/components/otp-input-field/OtpInputField.tsx
var import_system2 = require("@julo-ui/system");
// src/components/otp-input-field/use-otp-input-field.ts
var import_dom_utils2 = require("@julo-ui/dom-utils");
function useOtpInputField(props, ref = null) {
const { getInputProps } = useOtpInputContext();
const { index, register } = useOtpInputDescendant();
return getInputProps({ ...props, ref: (0, import_dom_utils2.mergeRefs)(register, ref), index });
}
// src/components/otp-input-field/styles.ts
var import_react6 = require("@emotion/react");
var otpInputFieldCx = import_react6.css`
width: 3rem;
height: 2.875rem;
padding: 0.75rem 0.8125rem;
border: 1px solid var(--colors-neutrals-30);
border-radius: var(--corner-md);
text-align: center;
&:focus,
&:focus-visible {
border-color: var(--colors-primary-20);
}
&:focus-visible {
outline: none;
}
&:disabled {
cursor: not-allowed;
border-color: var(--colors-neutrals-50);
background-color: var(--colors-neutrals-30);
}
&[aria-invalid='true'] {
border-color: var(--colors-red-30);
}
`;
// src/components/otp-input-field/OtpInputField.tsx
var import_jsx_runtime3 = require("react/jsx-runtime");
var OtpInputField = (0, import_system2.forwardRef)((props, ref) => {
const { className, ...resProps } = props;
const inputProps = useOtpInputField(resProps, ref);
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
import_system2.julo.input,
{
className: (0, import_system2.cx)("julo-otp-input__field", className),
...inputProps,
__css: otpInputFieldCx
}
);
});
OtpInputField.displayName = "OtpInputField";
var OtpInputField_default = OtpInputField;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
OtpInput,
OtpInputDescendantsProvider,
OtpInputField,
OtpInputProvider,
UseOtpInputProvider,
useOtpInput,
useOtpInputContext,
useOtpInputDescendant,
useOtpInputDescendants,
useOtpInputDescendantsContext,
useOtpInputField
});