@arun-dev/headless 4.17.0 → 4.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/otp-input/index.cjs +434 -0
- package/dist/otp-input/index.d.cts +113 -0
- package/dist/otp-input/index.d.ts +113 -0
- package/dist/otp-input/index.js +293 -0
- package/package.json +11 -1
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/otp-input/index.ts
|
|
21
|
+
var otp_input_exports = {};
|
|
22
|
+
__export(otp_input_exports, {
|
|
23
|
+
OtpInput: () => index_parts_exports
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(otp_input_exports);
|
|
26
|
+
|
|
27
|
+
// src/otp-input/index.parts.ts
|
|
28
|
+
var index_parts_exports = {};
|
|
29
|
+
__export(index_parts_exports, {
|
|
30
|
+
Input: () => OtpInputInput,
|
|
31
|
+
Root: () => OtpInputRoot,
|
|
32
|
+
Slot: () => OtpInputSlot
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// src/otp-input/OtpInputRoot.tsx
|
|
36
|
+
var import_react4 = require("react");
|
|
37
|
+
|
|
38
|
+
// src/core/useControlled.ts
|
|
39
|
+
var import_react = require("react");
|
|
40
|
+
function useControlled({
|
|
41
|
+
controlled,
|
|
42
|
+
default: defaultValue,
|
|
43
|
+
name = "Component",
|
|
44
|
+
state = "value"
|
|
45
|
+
}) {
|
|
46
|
+
const { current: isControlled } = (0, import_react.useRef)(controlled !== void 0);
|
|
47
|
+
const [uncontrolled, setUncontrolled] = (0, import_react.useState)(defaultValue);
|
|
48
|
+
const defaultRef = (0, import_react.useRef)(defaultValue);
|
|
49
|
+
const value = isControlled ? controlled : uncontrolled;
|
|
50
|
+
if (process.env.NODE_ENV !== "production") {
|
|
51
|
+
if (isControlled !== (controlled !== void 0)) {
|
|
52
|
+
console.error(
|
|
53
|
+
`${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (!isControlled && defaultRef.current !== defaultValue) {
|
|
57
|
+
console.error(
|
|
58
|
+
`${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const setValue = (0, import_react.useCallback)(
|
|
63
|
+
(next) => {
|
|
64
|
+
if (!isControlled) setUncontrolled(next);
|
|
65
|
+
},
|
|
66
|
+
[isControlled]
|
|
67
|
+
);
|
|
68
|
+
return [value, setValue];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/core/useRender.ts
|
|
72
|
+
var import_react2 = require("react");
|
|
73
|
+
|
|
74
|
+
// src/core/mergeProps.ts
|
|
75
|
+
function isEventHandler(key) {
|
|
76
|
+
return key.length > 2 && key.startsWith("on") && key[2] === key[2]?.toUpperCase();
|
|
77
|
+
}
|
|
78
|
+
function mergeRefs(...refs) {
|
|
79
|
+
return (node) => {
|
|
80
|
+
const cleanups = refs.map((ref) => {
|
|
81
|
+
if (typeof ref === "function") {
|
|
82
|
+
const result = ref(node);
|
|
83
|
+
return typeof result === "function" ? result : () => ref(null);
|
|
84
|
+
}
|
|
85
|
+
if (ref && typeof ref === "object") {
|
|
86
|
+
ref.current = node;
|
|
87
|
+
return () => {
|
|
88
|
+
ref.current = null;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return void 0;
|
|
92
|
+
});
|
|
93
|
+
return () => {
|
|
94
|
+
cleanups.forEach((cleanup) => {
|
|
95
|
+
if (typeof cleanup === "function") cleanup();
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function isSyntheticEvent(event) {
|
|
101
|
+
return typeof event === "object" && event !== null && "nativeEvent" in event;
|
|
102
|
+
}
|
|
103
|
+
function makeEventPreventable(event) {
|
|
104
|
+
if (Object.prototype.hasOwnProperty.call(event, "preventComponentHandler")) return;
|
|
105
|
+
const target = event;
|
|
106
|
+
target.preventComponentHandler = () => {
|
|
107
|
+
target.componentHandlerPrevented = true;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function isComponentHandlerPrevented(event) {
|
|
111
|
+
return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
|
|
112
|
+
}
|
|
113
|
+
function chainHandlers(earlier, value) {
|
|
114
|
+
return (...args) => {
|
|
115
|
+
const event = args[0];
|
|
116
|
+
if (isSyntheticEvent(event)) makeEventPreventable(event);
|
|
117
|
+
value(...args);
|
|
118
|
+
if (isComponentHandlerPrevented(event)) return;
|
|
119
|
+
earlier(...args);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function mergeProps(...objects) {
|
|
123
|
+
const merged = {};
|
|
124
|
+
for (const props of objects) {
|
|
125
|
+
if (!props) continue;
|
|
126
|
+
for (const key of Object.keys(props)) {
|
|
127
|
+
const value = props[key];
|
|
128
|
+
if (value === void 0) continue;
|
|
129
|
+
if (isEventHandler(key) && typeof value === "function") {
|
|
130
|
+
const earlier = merged[key];
|
|
131
|
+
merged[key] = typeof earlier === "function" ? chainHandlers(earlier, value) : value;
|
|
132
|
+
} else if (key === "className") {
|
|
133
|
+
merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
|
|
134
|
+
} else if (key === "style") {
|
|
135
|
+
merged.style = { ...merged.style, ...value };
|
|
136
|
+
} else if (key === "ref") {
|
|
137
|
+
merged.ref = merged.ref ? mergeRefs(merged.ref, value) : value;
|
|
138
|
+
} else {
|
|
139
|
+
merged[key] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return merged;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/core/useRender.ts
|
|
147
|
+
function useRender({
|
|
148
|
+
render,
|
|
149
|
+
defaultTagName,
|
|
150
|
+
props,
|
|
151
|
+
consumerProps
|
|
152
|
+
}) {
|
|
153
|
+
const merged = mergeProps(props, consumerProps);
|
|
154
|
+
if ((0, import_react2.isValidElement)(render)) {
|
|
155
|
+
return (0, import_react2.cloneElement)(render, mergeProps(merged, render.props));
|
|
156
|
+
}
|
|
157
|
+
return (0, import_react2.createElement)(defaultTagName, merged);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/otp-input/OtpInputRootContext.ts
|
|
161
|
+
var import_react3 = require("react");
|
|
162
|
+
var OtpInputRootContext = (0, import_react3.createContext)(null);
|
|
163
|
+
function useOtpInputRootContext(part) {
|
|
164
|
+
const context = (0, import_react3.useContext)(OtpInputRootContext);
|
|
165
|
+
if (context === null) {
|
|
166
|
+
throw new Error(`<OtpInput.${part}> must be rendered inside <OtpInput.Root>.`);
|
|
167
|
+
}
|
|
168
|
+
return context;
|
|
169
|
+
}
|
|
170
|
+
var GAP = " ";
|
|
171
|
+
function sanitize(text, validationType) {
|
|
172
|
+
return validationType === "numeric" ? text.replace(/\D/g, "") : text.replace(/[^A-Za-z0-9]/g, "");
|
|
173
|
+
}
|
|
174
|
+
function normalize(text, validationType, length) {
|
|
175
|
+
const allowed = validationType === "numeric" ? /[^\d ]/g : /[^A-Za-z0-9 ]/g;
|
|
176
|
+
return text.replace(allowed, "").slice(0, length).trimEnd();
|
|
177
|
+
}
|
|
178
|
+
function isComplete(value, length) {
|
|
179
|
+
return value.length === length && !value.includes(GAP);
|
|
180
|
+
}
|
|
181
|
+
function otpInputDataAttributes({ value, length, disabled }) {
|
|
182
|
+
return {
|
|
183
|
+
"data-complete": isComplete(value, length) ? "" : void 0,
|
|
184
|
+
"data-disabled": disabled ? "" : void 0
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/otp-input/OtpInputRoot.tsx
|
|
189
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
190
|
+
function OtpInputRoot({
|
|
191
|
+
value: valueProp,
|
|
192
|
+
defaultValue,
|
|
193
|
+
onValueChange,
|
|
194
|
+
onComplete,
|
|
195
|
+
length = 6,
|
|
196
|
+
validationType = "numeric",
|
|
197
|
+
disabled = false,
|
|
198
|
+
className,
|
|
199
|
+
children,
|
|
200
|
+
render,
|
|
201
|
+
...rest
|
|
202
|
+
}) {
|
|
203
|
+
const [value, setValue] = useControlled({
|
|
204
|
+
controlled: valueProp,
|
|
205
|
+
default: defaultValue ?? "",
|
|
206
|
+
name: "OtpInput.Root",
|
|
207
|
+
state: "value"
|
|
208
|
+
});
|
|
209
|
+
const commit = (0, import_react4.useCallback)(
|
|
210
|
+
(next) => {
|
|
211
|
+
const clean = normalize(next, validationType, length);
|
|
212
|
+
if (clean === value) return;
|
|
213
|
+
setValue(clean);
|
|
214
|
+
onValueChange?.(clean);
|
|
215
|
+
if (isComplete(clean, length)) onComplete?.(clean);
|
|
216
|
+
},
|
|
217
|
+
[validationType, length, value, setValue, onValueChange, onComplete]
|
|
218
|
+
);
|
|
219
|
+
const [selection, setSelection] = (0, import_react4.useState)(null);
|
|
220
|
+
const inputRef = (0, import_react4.useRef)(null);
|
|
221
|
+
const slotsRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
|
|
222
|
+
const registerSlot = (0, import_react4.useCallback)((index, element2) => {
|
|
223
|
+
if (element2) slotsRef.current.set(index, element2);
|
|
224
|
+
else slotsRef.current.delete(index);
|
|
225
|
+
}, []);
|
|
226
|
+
const slotAt = (0, import_react4.useCallback)((clientX) => {
|
|
227
|
+
let nearest = null;
|
|
228
|
+
let distance = Infinity;
|
|
229
|
+
for (const [index, element2] of slotsRef.current) {
|
|
230
|
+
const rect = element2.getBoundingClientRect();
|
|
231
|
+
const d = clientX < rect.left ? rect.left - clientX : clientX > rect.right ? clientX - rect.right : 0;
|
|
232
|
+
if (d < distance) {
|
|
233
|
+
distance = d;
|
|
234
|
+
nearest = index;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return nearest;
|
|
238
|
+
}, []);
|
|
239
|
+
const { current: initialValue } = (0, import_react4.useRef)(value);
|
|
240
|
+
(0, import_react4.useEffect)(() => {
|
|
241
|
+
const form = inputRef.current?.form;
|
|
242
|
+
if (!form) return;
|
|
243
|
+
function onReset(event) {
|
|
244
|
+
queueMicrotask(() => {
|
|
245
|
+
if (!event.defaultPrevented) commit(initialValue);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
form.addEventListener("reset", onReset);
|
|
249
|
+
return () => form.removeEventListener("reset", onReset);
|
|
250
|
+
}, [commit, initialValue]);
|
|
251
|
+
const context = (0, import_react4.useMemo)(
|
|
252
|
+
() => ({
|
|
253
|
+
value,
|
|
254
|
+
length,
|
|
255
|
+
disabled,
|
|
256
|
+
validationType,
|
|
257
|
+
selection,
|
|
258
|
+
setSelection,
|
|
259
|
+
commit,
|
|
260
|
+
inputRef,
|
|
261
|
+
registerSlot,
|
|
262
|
+
slotAt
|
|
263
|
+
}),
|
|
264
|
+
[value, length, disabled, validationType, selection, commit, registerSlot, slotAt]
|
|
265
|
+
);
|
|
266
|
+
const element = useRender({
|
|
267
|
+
render,
|
|
268
|
+
defaultTagName: "div",
|
|
269
|
+
props: {
|
|
270
|
+
...otpInputDataAttributes({ value, length, disabled }),
|
|
271
|
+
className,
|
|
272
|
+
children
|
|
273
|
+
},
|
|
274
|
+
consumerProps: rest
|
|
275
|
+
});
|
|
276
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OtpInputRootContext.Provider, { value: context, children: element });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/otp-input/OtpInputInput.tsx
|
|
280
|
+
var import_react5 = require("react");
|
|
281
|
+
function OtpInputInput({ render, ...rest }) {
|
|
282
|
+
const { value, length, disabled, validationType, setSelection, commit, inputRef, slotAt } = useOtpInputRootContext("Input");
|
|
283
|
+
function select(input, index) {
|
|
284
|
+
const filled = input.value.length;
|
|
285
|
+
const at = Math.max(0, Math.min(index, filled, length - 1));
|
|
286
|
+
if (at < filled) input.setSelectionRange(at, at + 1);
|
|
287
|
+
else input.setSelectionRange(filled, filled);
|
|
288
|
+
report(input);
|
|
289
|
+
}
|
|
290
|
+
function report(input) {
|
|
291
|
+
setSelection({ start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0 });
|
|
292
|
+
}
|
|
293
|
+
const pendingCaret = (0, import_react5.useRef)(null);
|
|
294
|
+
(0, import_react5.useLayoutEffect)(() => {
|
|
295
|
+
const input = inputRef.current;
|
|
296
|
+
const at = pendingCaret.current;
|
|
297
|
+
pendingCaret.current = null;
|
|
298
|
+
if (input && at !== null && input.ownerDocument.activeElement === input) select(input, at);
|
|
299
|
+
});
|
|
300
|
+
function edit(next, caret) {
|
|
301
|
+
pendingCaret.current = caret;
|
|
302
|
+
commit(next);
|
|
303
|
+
}
|
|
304
|
+
const cleared = (index) => value.slice(0, index) + GAP + value.slice(index + 1);
|
|
305
|
+
function onDelete(event, input) {
|
|
306
|
+
const start = input.selectionStart ?? 0;
|
|
307
|
+
const end = input.selectionEnd ?? start;
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
if (end > start + 1) {
|
|
310
|
+
edit(value.slice(0, start) + GAP.repeat(end - start) + value.slice(end), start);
|
|
311
|
+
} else if (end === start + 1 && value.charAt(start) !== GAP) {
|
|
312
|
+
edit(cleared(start), start);
|
|
313
|
+
} else if (event.inputType.includes("Backward") && start > 0) {
|
|
314
|
+
edit(cleared(start - 1), start - 1);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const beforeInput = (0, import_react5.useRef)(() => {
|
|
318
|
+
});
|
|
319
|
+
beforeInput.current = (event) => {
|
|
320
|
+
const input = event.currentTarget;
|
|
321
|
+
if (event.inputType.startsWith("delete") && event.inputType !== "deleteByCut") {
|
|
322
|
+
onDelete(event, input);
|
|
323
|
+
} else if (event.inputType === "insertText") {
|
|
324
|
+
const typed = event.data ?? "";
|
|
325
|
+
if (typed.length > 0 && sanitize(typed, validationType) !== typed) event.preventDefault();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
(0, import_react5.useEffect)(() => {
|
|
329
|
+
const input = inputRef.current;
|
|
330
|
+
if (!input) return;
|
|
331
|
+
const listener = (event) => beforeInput.current(event);
|
|
332
|
+
input.addEventListener("beforeinput", listener);
|
|
333
|
+
return () => input.removeEventListener("beforeinput", listener);
|
|
334
|
+
}, [inputRef]);
|
|
335
|
+
function syncSelection(event) {
|
|
336
|
+
const input = event.currentTarget;
|
|
337
|
+
const start = input.selectionStart ?? 0;
|
|
338
|
+
const end = input.selectionEnd ?? start;
|
|
339
|
+
if (start === end && (start < input.value.length || start >= length)) select(input, start);
|
|
340
|
+
else report(input);
|
|
341
|
+
}
|
|
342
|
+
return useRender({
|
|
343
|
+
render,
|
|
344
|
+
defaultTagName: "input",
|
|
345
|
+
props: {
|
|
346
|
+
ref: inputRef,
|
|
347
|
+
type: "text",
|
|
348
|
+
value,
|
|
349
|
+
inputMode: validationType === "numeric" ? "numeric" : "text",
|
|
350
|
+
autoComplete: "one-time-code",
|
|
351
|
+
autoCapitalize: "off",
|
|
352
|
+
autoCorrect: "off",
|
|
353
|
+
spellCheck: false,
|
|
354
|
+
maxLength: length,
|
|
355
|
+
pattern: validationType === "numeric" ? `\\d{${length}}` : `[A-Za-z0-9]{${length}}`,
|
|
356
|
+
disabled: disabled || void 0,
|
|
357
|
+
onChange(event) {
|
|
358
|
+
commit(event.currentTarget.value);
|
|
359
|
+
syncSelection(event);
|
|
360
|
+
},
|
|
361
|
+
onPaste(event) {
|
|
362
|
+
const pasted = sanitize(event.clipboardData.getData("text/plain"), validationType);
|
|
363
|
+
event.preventDefault();
|
|
364
|
+
if (!pasted) return;
|
|
365
|
+
const input = event.currentTarget;
|
|
366
|
+
if (pasted.length >= length) {
|
|
367
|
+
edit(pasted, length);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const start = input.selectionStart ?? value.length;
|
|
371
|
+
const end = input.selectionEnd ?? start;
|
|
372
|
+
const reach = start + pasted.length;
|
|
373
|
+
edit(
|
|
374
|
+
value.slice(0, start) + pasted + GAP.repeat(Math.max(0, end - reach)) + value.slice(Math.max(end, reach)),
|
|
375
|
+
reach
|
|
376
|
+
);
|
|
377
|
+
},
|
|
378
|
+
onKeyDown(event) {
|
|
379
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
380
|
+
if (event.shiftKey || event.altKey || event.metaKey || event.ctrlKey) return;
|
|
381
|
+
const input = event.currentTarget;
|
|
382
|
+
const at = input.selectionStart ?? 0;
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
select(input, event.key === "ArrowLeft" ? at - 1 : at + 1);
|
|
385
|
+
},
|
|
386
|
+
onClick(event) {
|
|
387
|
+
const index = slotAt(event.clientX);
|
|
388
|
+
if (index !== null) select(event.currentTarget, index);
|
|
389
|
+
},
|
|
390
|
+
onSelect: syncSelection,
|
|
391
|
+
onFocus(event) {
|
|
392
|
+
const gap = value.indexOf(GAP);
|
|
393
|
+
select(event.currentTarget, gap === -1 ? value.length : gap);
|
|
394
|
+
},
|
|
395
|
+
onBlur() {
|
|
396
|
+
setSelection(null);
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
consumerProps: rest
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/otp-input/OtpInputSlot.tsx
|
|
404
|
+
var import_react6 = require("react");
|
|
405
|
+
function OtpInputSlot({ index, className, children, render, ...rest }) {
|
|
406
|
+
const { value, length, disabled, selection, registerSlot } = useOtpInputRootContext("Slot");
|
|
407
|
+
const char = value.charAt(index) === GAP ? "" : value.charAt(index);
|
|
408
|
+
let active = false;
|
|
409
|
+
if (selection) {
|
|
410
|
+
active = selection.start === selection.end ? index === Math.min(selection.start, length - 1) : index >= selection.start && index < selection.end;
|
|
411
|
+
}
|
|
412
|
+
const ref = (0, import_react6.useCallback)(
|
|
413
|
+
(element) => registerSlot(index, element),
|
|
414
|
+
[registerSlot, index]
|
|
415
|
+
);
|
|
416
|
+
return useRender({
|
|
417
|
+
render,
|
|
418
|
+
defaultTagName: "div",
|
|
419
|
+
props: {
|
|
420
|
+
ref,
|
|
421
|
+
"aria-hidden": true,
|
|
422
|
+
"data-active": active ? "" : void 0,
|
|
423
|
+
"data-filled": char ? "" : void 0,
|
|
424
|
+
"data-disabled": disabled ? "" : void 0,
|
|
425
|
+
className,
|
|
426
|
+
children: children ?? char
|
|
427
|
+
},
|
|
428
|
+
consumerProps: rest
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
432
|
+
0 && (module.exports = {
|
|
433
|
+
OtpInput
|
|
434
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactElement, Ref, ComponentPropsWithRef } from 'react';
|
|
3
|
+
|
|
4
|
+
/** The characters a code may hold: digits only, or letters and digits. */
|
|
5
|
+
type OtpInputValidationType = 'numeric' | 'alphanumeric';
|
|
6
|
+
/** The state OtpInput.Root shares with its parts, and projects as `data-*` attributes. */
|
|
7
|
+
type OtpInputState = {
|
|
8
|
+
value: string;
|
|
9
|
+
length: number;
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type OtpInputRootOwnProps = {
|
|
14
|
+
/**
|
|
15
|
+
* Controlled value. Provide `onValueChange` alongside it.
|
|
16
|
+
*
|
|
17
|
+
* Never `undefined` once mounted: the mode is latched at mount. Coalesce at the call site —
|
|
18
|
+
* `value={code ?? ''}`.
|
|
19
|
+
*/
|
|
20
|
+
value?: string;
|
|
21
|
+
/** Initial value when uncontrolled. Read once, at mount. */
|
|
22
|
+
defaultValue?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Called with the code after every change, partial or complete. A box emptied before the end
|
|
25
|
+
* is a space — `"123 56"` — so check completeness with a pattern such as `/^\d{6}$/`, not
|
|
26
|
+
* the length alone.
|
|
27
|
+
*/
|
|
28
|
+
onValueChange?: (value: string) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Called with the code after a change that leaves it complete: every slot filled, no gap.
|
|
31
|
+
* Typing the last digit, pasting a whole code and filling an emptied slot all count; a change
|
|
32
|
+
* that leaves a slot empty does not. Use it to verify as soon as the code is in.
|
|
33
|
+
*/
|
|
34
|
+
onComplete?: (value: string) => void;
|
|
35
|
+
/** How many characters the code has, and so how many slots it shows. Defaults to 6. */
|
|
36
|
+
length?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Which characters are kept: `numeric` (the default) keeps digits, `alphanumeric` letters and
|
|
39
|
+
* digits. Anything else typed or pasted is dropped.
|
|
40
|
+
*/
|
|
41
|
+
validationType?: OtpInputValidationType;
|
|
42
|
+
/** Disables the input, and marks the Root and every Slot `data-disabled`. */
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
45
|
+
render?: ReactElement;
|
|
46
|
+
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
47
|
+
ref?: Ref<HTMLElement>;
|
|
48
|
+
};
|
|
49
|
+
type OtpInputRootProps = OtpInputRootOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof OtpInputRootOwnProps | 'defaultValue'>;
|
|
50
|
+
/**
|
|
51
|
+
* A one-time code: a single native `<input>` that holds the whole code, with one Slot per
|
|
52
|
+
* character drawn beside it (decision 20).
|
|
53
|
+
*
|
|
54
|
+
* One real input rather than one per character, because a text field cannot leave the native
|
|
55
|
+
* element (decision 7): SMS autofill (`autocomplete="one-time-code"`), paste, the numeric
|
|
56
|
+
* keypad, IME and password managers all target one input. The Slots are pictures of it.
|
|
57
|
+
*
|
|
58
|
+
* Holds the value through `useControlled`. The input reports changes, and the Root keeps only
|
|
59
|
+
* the characters `validationType` allows, up to `length`.
|
|
60
|
+
*/
|
|
61
|
+
declare function OtpInputRoot({ value: valueProp, defaultValue, onValueChange, onComplete, length, validationType, disabled, className, children, render, ...rest }: OtpInputRootProps): react.JSX.Element;
|
|
62
|
+
|
|
63
|
+
type OtpInputInputOwnProps = {
|
|
64
|
+
/** Element to render instead of the default `<input>`. Props and ref are merged onto it. */
|
|
65
|
+
render?: ReactElement;
|
|
66
|
+
/** Ref to the `<input>`, so focus management and a form library's focus-on-error reach it. */
|
|
67
|
+
ref?: Ref<HTMLElement>;
|
|
68
|
+
};
|
|
69
|
+
type OtpInputInputProps = OtpInputInputOwnProps & Omit<ComponentPropsWithRef<'input'>, keyof OtpInputInputOwnProps | 'value' | 'defaultValue' | 'type' | 'disabled'>;
|
|
70
|
+
/**
|
|
71
|
+
* The native `<input>` that holds the code: the element focus, typing, paste, autofill and
|
|
72
|
+
* the form all use. `name`, `required`, `autoFocus`, `aria-*` and a Field's attributes go here.
|
|
73
|
+
*
|
|
74
|
+
* Draw it over the Slots, transparent: a press then lands on the input, so mobile paste menus
|
|
75
|
+
* and autofill prompts work, and the Root maps the press to the slot under it.
|
|
76
|
+
*
|
|
77
|
+
* The caret always covers one character, so typing replaces the character in the active slot
|
|
78
|
+
* and moves on, and the last slot of a full code is replaced rather than refused. Left and
|
|
79
|
+
* Right move one slot.
|
|
80
|
+
*
|
|
81
|
+
* Delete and Backspace empty a slot in place: the characters after it keep their slots, and
|
|
82
|
+
* the caret stays, so the next character fills the same slot. Backspace on an empty slot
|
|
83
|
+
* empties the one before it; Delete on an empty slot does nothing.
|
|
84
|
+
*
|
|
85
|
+
* Carries `maxLength` and a `pattern` of exactly `length` allowed characters, so the browser's
|
|
86
|
+
* own validation reports a code that is too short. Both can be overridden.
|
|
87
|
+
*/
|
|
88
|
+
declare function OtpInputInput({ render, ...rest }: OtpInputInputProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
89
|
+
|
|
90
|
+
type OtpInputSlotOwnProps = {
|
|
91
|
+
/** Which character of the code this slot shows, from 0. */
|
|
92
|
+
index: number;
|
|
93
|
+
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
94
|
+
render?: ReactElement;
|
|
95
|
+
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
96
|
+
ref?: Ref<HTMLElement>;
|
|
97
|
+
};
|
|
98
|
+
type OtpInputSlotProps = OtpInputSlotOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof OtpInputSlotOwnProps>;
|
|
99
|
+
/**
|
|
100
|
+
* One character of the code, drawn. Shows the character at `index` unless given children,
|
|
101
|
+
* and is hidden from assistive technology: the Input carries the code.
|
|
102
|
+
*
|
|
103
|
+
* `data-active` marks the slot the caret is on while the input has focus — every slot of a
|
|
104
|
+
* selection — and `data-filled` a slot holding a character. A separator between groups of
|
|
105
|
+
* slots is the consumer's own element.
|
|
106
|
+
*/
|
|
107
|
+
declare function OtpInputSlot({ index, className, children, render, ...rest }: OtpInputSlotProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
108
|
+
|
|
109
|
+
declare namespace index_parts {
|
|
110
|
+
export { OtpInputInput as Input, OtpInputRoot as Root, OtpInputSlot as Slot };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { index_parts as OtpInput, type OtpInputInputProps, type OtpInputRootProps, type OtpInputSlotProps, type OtpInputState, type OtpInputValidationType };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactElement, Ref, ComponentPropsWithRef } from 'react';
|
|
3
|
+
|
|
4
|
+
/** The characters a code may hold: digits only, or letters and digits. */
|
|
5
|
+
type OtpInputValidationType = 'numeric' | 'alphanumeric';
|
|
6
|
+
/** The state OtpInput.Root shares with its parts, and projects as `data-*` attributes. */
|
|
7
|
+
type OtpInputState = {
|
|
8
|
+
value: string;
|
|
9
|
+
length: number;
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type OtpInputRootOwnProps = {
|
|
14
|
+
/**
|
|
15
|
+
* Controlled value. Provide `onValueChange` alongside it.
|
|
16
|
+
*
|
|
17
|
+
* Never `undefined` once mounted: the mode is latched at mount. Coalesce at the call site —
|
|
18
|
+
* `value={code ?? ''}`.
|
|
19
|
+
*/
|
|
20
|
+
value?: string;
|
|
21
|
+
/** Initial value when uncontrolled. Read once, at mount. */
|
|
22
|
+
defaultValue?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Called with the code after every change, partial or complete. A box emptied before the end
|
|
25
|
+
* is a space — `"123 56"` — so check completeness with a pattern such as `/^\d{6}$/`, not
|
|
26
|
+
* the length alone.
|
|
27
|
+
*/
|
|
28
|
+
onValueChange?: (value: string) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Called with the code after a change that leaves it complete: every slot filled, no gap.
|
|
31
|
+
* Typing the last digit, pasting a whole code and filling an emptied slot all count; a change
|
|
32
|
+
* that leaves a slot empty does not. Use it to verify as soon as the code is in.
|
|
33
|
+
*/
|
|
34
|
+
onComplete?: (value: string) => void;
|
|
35
|
+
/** How many characters the code has, and so how many slots it shows. Defaults to 6. */
|
|
36
|
+
length?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Which characters are kept: `numeric` (the default) keeps digits, `alphanumeric` letters and
|
|
39
|
+
* digits. Anything else typed or pasted is dropped.
|
|
40
|
+
*/
|
|
41
|
+
validationType?: OtpInputValidationType;
|
|
42
|
+
/** Disables the input, and marks the Root and every Slot `data-disabled`. */
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
45
|
+
render?: ReactElement;
|
|
46
|
+
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
47
|
+
ref?: Ref<HTMLElement>;
|
|
48
|
+
};
|
|
49
|
+
type OtpInputRootProps = OtpInputRootOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof OtpInputRootOwnProps | 'defaultValue'>;
|
|
50
|
+
/**
|
|
51
|
+
* A one-time code: a single native `<input>` that holds the whole code, with one Slot per
|
|
52
|
+
* character drawn beside it (decision 20).
|
|
53
|
+
*
|
|
54
|
+
* One real input rather than one per character, because a text field cannot leave the native
|
|
55
|
+
* element (decision 7): SMS autofill (`autocomplete="one-time-code"`), paste, the numeric
|
|
56
|
+
* keypad, IME and password managers all target one input. The Slots are pictures of it.
|
|
57
|
+
*
|
|
58
|
+
* Holds the value through `useControlled`. The input reports changes, and the Root keeps only
|
|
59
|
+
* the characters `validationType` allows, up to `length`.
|
|
60
|
+
*/
|
|
61
|
+
declare function OtpInputRoot({ value: valueProp, defaultValue, onValueChange, onComplete, length, validationType, disabled, className, children, render, ...rest }: OtpInputRootProps): react.JSX.Element;
|
|
62
|
+
|
|
63
|
+
type OtpInputInputOwnProps = {
|
|
64
|
+
/** Element to render instead of the default `<input>`. Props and ref are merged onto it. */
|
|
65
|
+
render?: ReactElement;
|
|
66
|
+
/** Ref to the `<input>`, so focus management and a form library's focus-on-error reach it. */
|
|
67
|
+
ref?: Ref<HTMLElement>;
|
|
68
|
+
};
|
|
69
|
+
type OtpInputInputProps = OtpInputInputOwnProps & Omit<ComponentPropsWithRef<'input'>, keyof OtpInputInputOwnProps | 'value' | 'defaultValue' | 'type' | 'disabled'>;
|
|
70
|
+
/**
|
|
71
|
+
* The native `<input>` that holds the code: the element focus, typing, paste, autofill and
|
|
72
|
+
* the form all use. `name`, `required`, `autoFocus`, `aria-*` and a Field's attributes go here.
|
|
73
|
+
*
|
|
74
|
+
* Draw it over the Slots, transparent: a press then lands on the input, so mobile paste menus
|
|
75
|
+
* and autofill prompts work, and the Root maps the press to the slot under it.
|
|
76
|
+
*
|
|
77
|
+
* The caret always covers one character, so typing replaces the character in the active slot
|
|
78
|
+
* and moves on, and the last slot of a full code is replaced rather than refused. Left and
|
|
79
|
+
* Right move one slot.
|
|
80
|
+
*
|
|
81
|
+
* Delete and Backspace empty a slot in place: the characters after it keep their slots, and
|
|
82
|
+
* the caret stays, so the next character fills the same slot. Backspace on an empty slot
|
|
83
|
+
* empties the one before it; Delete on an empty slot does nothing.
|
|
84
|
+
*
|
|
85
|
+
* Carries `maxLength` and a `pattern` of exactly `length` allowed characters, so the browser's
|
|
86
|
+
* own validation reports a code that is too short. Both can be overridden.
|
|
87
|
+
*/
|
|
88
|
+
declare function OtpInputInput({ render, ...rest }: OtpInputInputProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
89
|
+
|
|
90
|
+
type OtpInputSlotOwnProps = {
|
|
91
|
+
/** Which character of the code this slot shows, from 0. */
|
|
92
|
+
index: number;
|
|
93
|
+
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
94
|
+
render?: ReactElement;
|
|
95
|
+
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
96
|
+
ref?: Ref<HTMLElement>;
|
|
97
|
+
};
|
|
98
|
+
type OtpInputSlotProps = OtpInputSlotOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof OtpInputSlotOwnProps>;
|
|
99
|
+
/**
|
|
100
|
+
* One character of the code, drawn. Shows the character at `index` unless given children,
|
|
101
|
+
* and is hidden from assistive technology: the Input carries the code.
|
|
102
|
+
*
|
|
103
|
+
* `data-active` marks the slot the caret is on while the input has focus — every slot of a
|
|
104
|
+
* selection — and `data-filled` a slot holding a character. A separator between groups of
|
|
105
|
+
* slots is the consumer's own element.
|
|
106
|
+
*/
|
|
107
|
+
declare function OtpInputSlot({ index, className, children, render, ...rest }: OtpInputSlotProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
108
|
+
|
|
109
|
+
declare namespace index_parts {
|
|
110
|
+
export { OtpInputInput as Input, OtpInputRoot as Root, OtpInputSlot as Slot };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { index_parts as OtpInput, type OtpInputInputProps, type OtpInputRootProps, type OtpInputSlotProps, type OtpInputState, type OtpInputValidationType };
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useControlled
|
|
3
|
+
} from "../chunk-UEIB3I3T.js";
|
|
4
|
+
import {
|
|
5
|
+
__export,
|
|
6
|
+
useRender
|
|
7
|
+
} from "../chunk-OQTWWZ4B.js";
|
|
8
|
+
|
|
9
|
+
// src/otp-input/index.parts.ts
|
|
10
|
+
var index_parts_exports = {};
|
|
11
|
+
__export(index_parts_exports, {
|
|
12
|
+
Input: () => OtpInputInput,
|
|
13
|
+
Root: () => OtpInputRoot,
|
|
14
|
+
Slot: () => OtpInputSlot
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// src/otp-input/OtpInputRoot.tsx
|
|
18
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
19
|
+
|
|
20
|
+
// src/otp-input/OtpInputRootContext.ts
|
|
21
|
+
import { createContext, useContext } from "react";
|
|
22
|
+
var OtpInputRootContext = createContext(null);
|
|
23
|
+
function useOtpInputRootContext(part) {
|
|
24
|
+
const context = useContext(OtpInputRootContext);
|
|
25
|
+
if (context === null) {
|
|
26
|
+
throw new Error(`<OtpInput.${part}> must be rendered inside <OtpInput.Root>.`);
|
|
27
|
+
}
|
|
28
|
+
return context;
|
|
29
|
+
}
|
|
30
|
+
var GAP = " ";
|
|
31
|
+
function sanitize(text, validationType) {
|
|
32
|
+
return validationType === "numeric" ? text.replace(/\D/g, "") : text.replace(/[^A-Za-z0-9]/g, "");
|
|
33
|
+
}
|
|
34
|
+
function normalize(text, validationType, length) {
|
|
35
|
+
const allowed = validationType === "numeric" ? /[^\d ]/g : /[^A-Za-z0-9 ]/g;
|
|
36
|
+
return text.replace(allowed, "").slice(0, length).trimEnd();
|
|
37
|
+
}
|
|
38
|
+
function isComplete(value, length) {
|
|
39
|
+
return value.length === length && !value.includes(GAP);
|
|
40
|
+
}
|
|
41
|
+
function otpInputDataAttributes({ value, length, disabled }) {
|
|
42
|
+
return {
|
|
43
|
+
"data-complete": isComplete(value, length) ? "" : void 0,
|
|
44
|
+
"data-disabled": disabled ? "" : void 0
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/otp-input/OtpInputRoot.tsx
|
|
49
|
+
import { jsx } from "react/jsx-runtime";
|
|
50
|
+
function OtpInputRoot({
|
|
51
|
+
value: valueProp,
|
|
52
|
+
defaultValue,
|
|
53
|
+
onValueChange,
|
|
54
|
+
onComplete,
|
|
55
|
+
length = 6,
|
|
56
|
+
validationType = "numeric",
|
|
57
|
+
disabled = false,
|
|
58
|
+
className,
|
|
59
|
+
children,
|
|
60
|
+
render,
|
|
61
|
+
...rest
|
|
62
|
+
}) {
|
|
63
|
+
const [value, setValue] = useControlled({
|
|
64
|
+
controlled: valueProp,
|
|
65
|
+
default: defaultValue ?? "",
|
|
66
|
+
name: "OtpInput.Root",
|
|
67
|
+
state: "value"
|
|
68
|
+
});
|
|
69
|
+
const commit = useCallback(
|
|
70
|
+
(next) => {
|
|
71
|
+
const clean = normalize(next, validationType, length);
|
|
72
|
+
if (clean === value) return;
|
|
73
|
+
setValue(clean);
|
|
74
|
+
onValueChange?.(clean);
|
|
75
|
+
if (isComplete(clean, length)) onComplete?.(clean);
|
|
76
|
+
},
|
|
77
|
+
[validationType, length, value, setValue, onValueChange, onComplete]
|
|
78
|
+
);
|
|
79
|
+
const [selection, setSelection] = useState(null);
|
|
80
|
+
const inputRef = useRef(null);
|
|
81
|
+
const slotsRef = useRef(/* @__PURE__ */ new Map());
|
|
82
|
+
const registerSlot = useCallback((index, element2) => {
|
|
83
|
+
if (element2) slotsRef.current.set(index, element2);
|
|
84
|
+
else slotsRef.current.delete(index);
|
|
85
|
+
}, []);
|
|
86
|
+
const slotAt = useCallback((clientX) => {
|
|
87
|
+
let nearest = null;
|
|
88
|
+
let distance = Infinity;
|
|
89
|
+
for (const [index, element2] of slotsRef.current) {
|
|
90
|
+
const rect = element2.getBoundingClientRect();
|
|
91
|
+
const d = clientX < rect.left ? rect.left - clientX : clientX > rect.right ? clientX - rect.right : 0;
|
|
92
|
+
if (d < distance) {
|
|
93
|
+
distance = d;
|
|
94
|
+
nearest = index;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return nearest;
|
|
98
|
+
}, []);
|
|
99
|
+
const { current: initialValue } = useRef(value);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const form = inputRef.current?.form;
|
|
102
|
+
if (!form) return;
|
|
103
|
+
function onReset(event) {
|
|
104
|
+
queueMicrotask(() => {
|
|
105
|
+
if (!event.defaultPrevented) commit(initialValue);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
form.addEventListener("reset", onReset);
|
|
109
|
+
return () => form.removeEventListener("reset", onReset);
|
|
110
|
+
}, [commit, initialValue]);
|
|
111
|
+
const context = useMemo(
|
|
112
|
+
() => ({
|
|
113
|
+
value,
|
|
114
|
+
length,
|
|
115
|
+
disabled,
|
|
116
|
+
validationType,
|
|
117
|
+
selection,
|
|
118
|
+
setSelection,
|
|
119
|
+
commit,
|
|
120
|
+
inputRef,
|
|
121
|
+
registerSlot,
|
|
122
|
+
slotAt
|
|
123
|
+
}),
|
|
124
|
+
[value, length, disabled, validationType, selection, commit, registerSlot, slotAt]
|
|
125
|
+
);
|
|
126
|
+
const element = useRender({
|
|
127
|
+
render,
|
|
128
|
+
defaultTagName: "div",
|
|
129
|
+
props: {
|
|
130
|
+
...otpInputDataAttributes({ value, length, disabled }),
|
|
131
|
+
className,
|
|
132
|
+
children
|
|
133
|
+
},
|
|
134
|
+
consumerProps: rest
|
|
135
|
+
});
|
|
136
|
+
return /* @__PURE__ */ jsx(OtpInputRootContext.Provider, { value: context, children: element });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/otp-input/OtpInputInput.tsx
|
|
140
|
+
import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2 } from "react";
|
|
141
|
+
function OtpInputInput({ render, ...rest }) {
|
|
142
|
+
const { value, length, disabled, validationType, setSelection, commit, inputRef, slotAt } = useOtpInputRootContext("Input");
|
|
143
|
+
function select(input, index) {
|
|
144
|
+
const filled = input.value.length;
|
|
145
|
+
const at = Math.max(0, Math.min(index, filled, length - 1));
|
|
146
|
+
if (at < filled) input.setSelectionRange(at, at + 1);
|
|
147
|
+
else input.setSelectionRange(filled, filled);
|
|
148
|
+
report(input);
|
|
149
|
+
}
|
|
150
|
+
function report(input) {
|
|
151
|
+
setSelection({ start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0 });
|
|
152
|
+
}
|
|
153
|
+
const pendingCaret = useRef2(null);
|
|
154
|
+
useLayoutEffect(() => {
|
|
155
|
+
const input = inputRef.current;
|
|
156
|
+
const at = pendingCaret.current;
|
|
157
|
+
pendingCaret.current = null;
|
|
158
|
+
if (input && at !== null && input.ownerDocument.activeElement === input) select(input, at);
|
|
159
|
+
});
|
|
160
|
+
function edit(next, caret) {
|
|
161
|
+
pendingCaret.current = caret;
|
|
162
|
+
commit(next);
|
|
163
|
+
}
|
|
164
|
+
const cleared = (index) => value.slice(0, index) + GAP + value.slice(index + 1);
|
|
165
|
+
function onDelete(event, input) {
|
|
166
|
+
const start = input.selectionStart ?? 0;
|
|
167
|
+
const end = input.selectionEnd ?? start;
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
if (end > start + 1) {
|
|
170
|
+
edit(value.slice(0, start) + GAP.repeat(end - start) + value.slice(end), start);
|
|
171
|
+
} else if (end === start + 1 && value.charAt(start) !== GAP) {
|
|
172
|
+
edit(cleared(start), start);
|
|
173
|
+
} else if (event.inputType.includes("Backward") && start > 0) {
|
|
174
|
+
edit(cleared(start - 1), start - 1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const beforeInput = useRef2(() => {
|
|
178
|
+
});
|
|
179
|
+
beforeInput.current = (event) => {
|
|
180
|
+
const input = event.currentTarget;
|
|
181
|
+
if (event.inputType.startsWith("delete") && event.inputType !== "deleteByCut") {
|
|
182
|
+
onDelete(event, input);
|
|
183
|
+
} else if (event.inputType === "insertText") {
|
|
184
|
+
const typed = event.data ?? "";
|
|
185
|
+
if (typed.length > 0 && sanitize(typed, validationType) !== typed) event.preventDefault();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
useEffect2(() => {
|
|
189
|
+
const input = inputRef.current;
|
|
190
|
+
if (!input) return;
|
|
191
|
+
const listener = (event) => beforeInput.current(event);
|
|
192
|
+
input.addEventListener("beforeinput", listener);
|
|
193
|
+
return () => input.removeEventListener("beforeinput", listener);
|
|
194
|
+
}, [inputRef]);
|
|
195
|
+
function syncSelection(event) {
|
|
196
|
+
const input = event.currentTarget;
|
|
197
|
+
const start = input.selectionStart ?? 0;
|
|
198
|
+
const end = input.selectionEnd ?? start;
|
|
199
|
+
if (start === end && (start < input.value.length || start >= length)) select(input, start);
|
|
200
|
+
else report(input);
|
|
201
|
+
}
|
|
202
|
+
return useRender({
|
|
203
|
+
render,
|
|
204
|
+
defaultTagName: "input",
|
|
205
|
+
props: {
|
|
206
|
+
ref: inputRef,
|
|
207
|
+
type: "text",
|
|
208
|
+
value,
|
|
209
|
+
inputMode: validationType === "numeric" ? "numeric" : "text",
|
|
210
|
+
autoComplete: "one-time-code",
|
|
211
|
+
autoCapitalize: "off",
|
|
212
|
+
autoCorrect: "off",
|
|
213
|
+
spellCheck: false,
|
|
214
|
+
maxLength: length,
|
|
215
|
+
pattern: validationType === "numeric" ? `\\d{${length}}` : `[A-Za-z0-9]{${length}}`,
|
|
216
|
+
disabled: disabled || void 0,
|
|
217
|
+
onChange(event) {
|
|
218
|
+
commit(event.currentTarget.value);
|
|
219
|
+
syncSelection(event);
|
|
220
|
+
},
|
|
221
|
+
onPaste(event) {
|
|
222
|
+
const pasted = sanitize(event.clipboardData.getData("text/plain"), validationType);
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
if (!pasted) return;
|
|
225
|
+
const input = event.currentTarget;
|
|
226
|
+
if (pasted.length >= length) {
|
|
227
|
+
edit(pasted, length);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const start = input.selectionStart ?? value.length;
|
|
231
|
+
const end = input.selectionEnd ?? start;
|
|
232
|
+
const reach = start + pasted.length;
|
|
233
|
+
edit(
|
|
234
|
+
value.slice(0, start) + pasted + GAP.repeat(Math.max(0, end - reach)) + value.slice(Math.max(end, reach)),
|
|
235
|
+
reach
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
onKeyDown(event) {
|
|
239
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
240
|
+
if (event.shiftKey || event.altKey || event.metaKey || event.ctrlKey) return;
|
|
241
|
+
const input = event.currentTarget;
|
|
242
|
+
const at = input.selectionStart ?? 0;
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
select(input, event.key === "ArrowLeft" ? at - 1 : at + 1);
|
|
245
|
+
},
|
|
246
|
+
onClick(event) {
|
|
247
|
+
const index = slotAt(event.clientX);
|
|
248
|
+
if (index !== null) select(event.currentTarget, index);
|
|
249
|
+
},
|
|
250
|
+
onSelect: syncSelection,
|
|
251
|
+
onFocus(event) {
|
|
252
|
+
const gap = value.indexOf(GAP);
|
|
253
|
+
select(event.currentTarget, gap === -1 ? value.length : gap);
|
|
254
|
+
},
|
|
255
|
+
onBlur() {
|
|
256
|
+
setSelection(null);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
consumerProps: rest
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/otp-input/OtpInputSlot.tsx
|
|
264
|
+
import { useCallback as useCallback2 } from "react";
|
|
265
|
+
function OtpInputSlot({ index, className, children, render, ...rest }) {
|
|
266
|
+
const { value, length, disabled, selection, registerSlot } = useOtpInputRootContext("Slot");
|
|
267
|
+
const char = value.charAt(index) === GAP ? "" : value.charAt(index);
|
|
268
|
+
let active = false;
|
|
269
|
+
if (selection) {
|
|
270
|
+
active = selection.start === selection.end ? index === Math.min(selection.start, length - 1) : index >= selection.start && index < selection.end;
|
|
271
|
+
}
|
|
272
|
+
const ref = useCallback2(
|
|
273
|
+
(element) => registerSlot(index, element),
|
|
274
|
+
[registerSlot, index]
|
|
275
|
+
);
|
|
276
|
+
return useRender({
|
|
277
|
+
render,
|
|
278
|
+
defaultTagName: "div",
|
|
279
|
+
props: {
|
|
280
|
+
ref,
|
|
281
|
+
"aria-hidden": true,
|
|
282
|
+
"data-active": active ? "" : void 0,
|
|
283
|
+
"data-filled": char ? "" : void 0,
|
|
284
|
+
"data-disabled": disabled ? "" : void 0,
|
|
285
|
+
className,
|
|
286
|
+
children: children ?? char
|
|
287
|
+
},
|
|
288
|
+
consumerProps: rest
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
export {
|
|
292
|
+
index_parts_exports as OtpInput
|
|
293
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arun-dev/headless",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.19.0",
|
|
4
4
|
"description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -84,6 +84,16 @@
|
|
|
84
84
|
"default": "./dist/menu/index.cjs"
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
|
+
"./otp-input": {
|
|
88
|
+
"import": {
|
|
89
|
+
"types": "./dist/otp-input/index.d.ts",
|
|
90
|
+
"default": "./dist/otp-input/index.js"
|
|
91
|
+
},
|
|
92
|
+
"require": {
|
|
93
|
+
"types": "./dist/otp-input/index.d.cts",
|
|
94
|
+
"default": "./dist/otp-input/index.cjs"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
87
97
|
"./popover": {
|
|
88
98
|
"import": {
|
|
89
99
|
"types": "./dist/popover/index.d.ts",
|