@arun-dev/headless 4.18.0 → 4.19.1

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.
@@ -32,7 +32,7 @@ __export(index_parts_exports, {
32
32
  });
33
33
 
34
34
  // src/checkbox/CheckboxRoot.tsx
35
- var import_react5 = require("react");
35
+ var import_react6 = require("react");
36
36
 
37
37
  // src/core/useControlled.ts
38
38
  var import_react = require("react");
@@ -68,7 +68,24 @@ function useControlled({
68
68
  }
69
69
 
70
70
  // src/core/useFormReset.ts
71
+ var import_react3 = require("react");
72
+
73
+ // src/core/useLatest.ts
71
74
  var import_react2 = require("react");
75
+ function useLatest(value) {
76
+ const ref = (0, import_react2.useRef)(value);
77
+ (0, import_react2.useLayoutEffect)(() => {
78
+ ref.current = value;
79
+ });
80
+ return ref;
81
+ }
82
+ function afterReset(event, work) {
83
+ setTimeout(() => {
84
+ if (!event.defaultPrevented) work();
85
+ }, 0);
86
+ }
87
+
88
+ // src/core/useFormReset.ts
72
89
  function useFormReset({
73
90
  elementRef,
74
91
  inputRef,
@@ -76,25 +93,26 @@ function useFormReset({
76
93
  inputChecked,
77
94
  commit
78
95
  }) {
79
- const { current: initialValue } = (0, import_react2.useRef)(value);
80
- (0, import_react2.useEffect)(() => {
96
+ const { current: initialValue } = (0, import_react3.useRef)(value);
97
+ const latest = useLatest({ value, inputChecked, commit });
98
+ (0, import_react3.useEffect)(() => {
81
99
  const form = elementRef.current?.form;
82
100
  if (!form) return;
83
101
  function onReset(event) {
84
- queueMicrotask(() => {
85
- if (event.defaultPrevented) return;
86
- if (inputRef.current) inputRef.current.checked = inputChecked;
87
- if (value === initialValue) return;
88
- commit(initialValue);
102
+ afterReset(event, () => {
103
+ const { value: current, inputChecked: checked, commit: set } = latest.current;
104
+ if (inputRef.current) inputRef.current.checked = checked;
105
+ if (current === initialValue) return;
106
+ set(initialValue);
89
107
  });
90
108
  }
91
109
  form.addEventListener("reset", onReset);
92
110
  return () => form.removeEventListener("reset", onReset);
93
- }, [elementRef, inputRef, value, inputChecked, commit, initialValue]);
111
+ }, [elementRef, inputRef, latest, initialValue]);
94
112
  }
95
113
 
96
114
  // src/core/useRender.ts
97
- var import_react3 = require("react");
115
+ var import_react4 = require("react");
98
116
 
99
117
  // src/core/mergeProps.ts
100
118
  function isEventHandler(key) {
@@ -176,17 +194,17 @@ function useRender({
176
194
  consumerProps
177
195
  }) {
178
196
  const merged = mergeProps(props, consumerProps);
179
- if ((0, import_react3.isValidElement)(render)) {
180
- return (0, import_react3.cloneElement)(render, mergeProps(merged, render.props));
197
+ if ((0, import_react4.isValidElement)(render)) {
198
+ return (0, import_react4.cloneElement)(render, mergeProps(merged, render.props));
181
199
  }
182
- return (0, import_react3.createElement)(defaultTagName, merged);
200
+ return (0, import_react4.createElement)(defaultTagName, merged);
183
201
  }
184
202
 
185
203
  // src/checkbox/CheckboxRootContext.ts
186
- var import_react4 = require("react");
187
- var CheckboxRootContext = (0, import_react4.createContext)(null);
204
+ var import_react5 = require("react");
205
+ var CheckboxRootContext = (0, import_react5.createContext)(null);
188
206
  function useCheckboxRootContext() {
189
- const context = (0, import_react4.useContext)(CheckboxRootContext);
207
+ const context = (0, import_react5.useContext)(CheckboxRootContext);
190
208
  if (context === null) {
191
209
  throw new Error("<Checkbox.Indicator> must be rendered inside <Checkbox.Root>.");
192
210
  }
@@ -223,16 +241,16 @@ function CheckboxRoot({
223
241
  name: "Checkbox.Root",
224
242
  state: "checked"
225
243
  });
226
- const commitChecked = (0, import_react5.useCallback)(
244
+ const commitChecked = (0, import_react6.useCallback)(
227
245
  (next) => {
228
246
  setChecked(next);
229
247
  onCheckedChange?.(next);
230
248
  },
231
249
  [setChecked, onCheckedChange]
232
250
  );
233
- const state = (0, import_react5.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
234
- const elementRef = (0, import_react5.useRef)(null);
235
- const inputRef = (0, import_react5.useRef)(null);
251
+ const state = (0, import_react6.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
252
+ const elementRef = (0, import_react6.useRef)(null);
253
+ const inputRef = (0, import_react6.useRef)(null);
236
254
  const submitted = checked === true;
237
255
  useFormReset({
238
256
  elementRef,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  useFormReset
3
- } from "../chunk-IWUVUY76.js";
3
+ } from "../chunk-FDGT4JPK.js";
4
+ import "../chunk-JJ5FACZU.js";
4
5
  import {
5
6
  useControlled
6
7
  } from "../chunk-UEIB3I3T.js";
@@ -1,3 +1,8 @@
1
+ import {
2
+ afterReset,
3
+ useLatest
4
+ } from "./chunk-JJ5FACZU.js";
5
+
1
6
  // src/core/useFormReset.ts
2
7
  import { useEffect, useRef } from "react";
3
8
  function useFormReset({
@@ -8,20 +13,21 @@ function useFormReset({
8
13
  commit
9
14
  }) {
10
15
  const { current: initialValue } = useRef(value);
16
+ const latest = useLatest({ value, inputChecked, commit });
11
17
  useEffect(() => {
12
18
  const form = elementRef.current?.form;
13
19
  if (!form) return;
14
20
  function onReset(event) {
15
- queueMicrotask(() => {
16
- if (event.defaultPrevented) return;
17
- if (inputRef.current) inputRef.current.checked = inputChecked;
18
- if (value === initialValue) return;
19
- commit(initialValue);
21
+ afterReset(event, () => {
22
+ const { value: current, inputChecked: checked, commit: set } = latest.current;
23
+ if (inputRef.current) inputRef.current.checked = checked;
24
+ if (current === initialValue) return;
25
+ set(initialValue);
20
26
  });
21
27
  }
22
28
  form.addEventListener("reset", onReset);
23
29
  return () => form.removeEventListener("reset", onReset);
24
- }, [elementRef, inputRef, value, inputChecked, commit, initialValue]);
30
+ }, [elementRef, inputRef, latest, initialValue]);
25
31
  }
26
32
 
27
33
  export {
@@ -0,0 +1,19 @@
1
+ // src/core/useLatest.ts
2
+ import { useLayoutEffect, useRef } from "react";
3
+ function useLatest(value) {
4
+ const ref = useRef(value);
5
+ useLayoutEffect(() => {
6
+ ref.current = value;
7
+ });
8
+ return ref;
9
+ }
10
+ function afterReset(event, work) {
11
+ setTimeout(() => {
12
+ if (!event.defaultPrevented) work();
13
+ }, 0);
14
+ }
15
+
16
+ export {
17
+ useLatest,
18
+ afterReset
19
+ };
@@ -33,7 +33,7 @@ __export(index_parts_exports, {
33
33
  });
34
34
 
35
35
  // src/otp-input/OtpInputRoot.tsx
36
- var import_react4 = require("react");
36
+ var import_react5 = require("react");
37
37
 
38
38
  // src/core/useControlled.ts
39
39
  var import_react = require("react");
@@ -68,8 +68,23 @@ function useControlled({
68
68
  return [value, setValue];
69
69
  }
70
70
 
71
- // src/core/useRender.ts
71
+ // src/core/useLatest.ts
72
72
  var import_react2 = require("react");
73
+ function useLatest(value) {
74
+ const ref = (0, import_react2.useRef)(value);
75
+ (0, import_react2.useLayoutEffect)(() => {
76
+ ref.current = value;
77
+ });
78
+ return ref;
79
+ }
80
+ function afterReset(event, work) {
81
+ setTimeout(() => {
82
+ if (!event.defaultPrevented) work();
83
+ }, 0);
84
+ }
85
+
86
+ // src/core/useRender.ts
87
+ var import_react3 = require("react");
73
88
 
74
89
  // src/core/mergeProps.ts
75
90
  function isEventHandler(key) {
@@ -151,28 +166,36 @@ function useRender({
151
166
  consumerProps
152
167
  }) {
153
168
  const merged = mergeProps(props, consumerProps);
154
- if ((0, import_react2.isValidElement)(render)) {
155
- return (0, import_react2.cloneElement)(render, mergeProps(merged, render.props));
169
+ if ((0, import_react3.isValidElement)(render)) {
170
+ return (0, import_react3.cloneElement)(render, mergeProps(merged, render.props));
156
171
  }
157
- return (0, import_react2.createElement)(defaultTagName, merged);
172
+ return (0, import_react3.createElement)(defaultTagName, merged);
158
173
  }
159
174
 
160
175
  // src/otp-input/OtpInputRootContext.ts
161
- var import_react3 = require("react");
162
- var OtpInputRootContext = (0, import_react3.createContext)(null);
176
+ var import_react4 = require("react");
177
+ var OtpInputRootContext = (0, import_react4.createContext)(null);
163
178
  function useOtpInputRootContext(part) {
164
- const context = (0, import_react3.useContext)(OtpInputRootContext);
179
+ const context = (0, import_react4.useContext)(OtpInputRootContext);
165
180
  if (context === null) {
166
181
  throw new Error(`<OtpInput.${part}> must be rendered inside <OtpInput.Root>.`);
167
182
  }
168
183
  return context;
169
184
  }
185
+ var GAP = " ";
170
186
  function sanitize(text, validationType) {
171
187
  return validationType === "numeric" ? text.replace(/\D/g, "") : text.replace(/[^A-Za-z0-9]/g, "");
172
188
  }
189
+ function normalize(text, validationType, length) {
190
+ const allowed = validationType === "numeric" ? /[^\d ]/g : /[^A-Za-z0-9 ]/g;
191
+ return text.replace(allowed, "").slice(0, length).trimEnd();
192
+ }
193
+ function isComplete(value, length) {
194
+ return value.length === length && !value.includes(GAP);
195
+ }
173
196
  function otpInputDataAttributes({ value, length, disabled }) {
174
197
  return {
175
- "data-complete": value.length === length ? "" : void 0,
198
+ "data-complete": isComplete(value, length) ? "" : void 0,
176
199
  "data-disabled": disabled ? "" : void 0
177
200
  };
178
201
  }
@@ -183,6 +206,7 @@ function OtpInputRoot({
183
206
  value: valueProp,
184
207
  defaultValue,
185
208
  onValueChange,
209
+ onComplete,
186
210
  length = 6,
187
211
  validationType = "numeric",
188
212
  disabled = false,
@@ -197,23 +221,24 @@ function OtpInputRoot({
197
221
  name: "OtpInput.Root",
198
222
  state: "value"
199
223
  });
200
- const commit = (0, import_react4.useCallback)(
224
+ const commit = (0, import_react5.useCallback)(
201
225
  (next) => {
202
- const clean = sanitize(next, validationType).slice(0, length);
226
+ const clean = normalize(next, validationType, length);
203
227
  if (clean === value) return;
204
228
  setValue(clean);
205
229
  onValueChange?.(clean);
230
+ if (isComplete(clean, length)) onComplete?.(clean);
206
231
  },
207
- [validationType, length, value, setValue, onValueChange]
232
+ [validationType, length, value, setValue, onValueChange, onComplete]
208
233
  );
209
- const [selection, setSelection] = (0, import_react4.useState)(null);
210
- const inputRef = (0, import_react4.useRef)(null);
211
- const slotsRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
212
- const registerSlot = (0, import_react4.useCallback)((index, element2) => {
234
+ const [selection, setSelection] = (0, import_react5.useState)(null);
235
+ const inputRef = (0, import_react5.useRef)(null);
236
+ const slotsRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
237
+ const registerSlot = (0, import_react5.useCallback)((index, element2) => {
213
238
  if (element2) slotsRef.current.set(index, element2);
214
239
  else slotsRef.current.delete(index);
215
240
  }, []);
216
- const slotAt = (0, import_react4.useCallback)((clientX) => {
241
+ const slotAt = (0, import_react5.useCallback)((clientX) => {
217
242
  let nearest = null;
218
243
  let distance = Infinity;
219
244
  for (const [index, element2] of slotsRef.current) {
@@ -226,19 +251,18 @@ function OtpInputRoot({
226
251
  }
227
252
  return nearest;
228
253
  }, []);
229
- const { current: initialValue } = (0, import_react4.useRef)(value);
230
- (0, import_react4.useEffect)(() => {
254
+ const { current: initialValue } = (0, import_react5.useRef)(value);
255
+ const latest = useLatest(commit);
256
+ (0, import_react5.useEffect)(() => {
231
257
  const form = inputRef.current?.form;
232
258
  if (!form) return;
233
259
  function onReset(event) {
234
- queueMicrotask(() => {
235
- if (!event.defaultPrevented) commit(initialValue);
236
- });
260
+ afterReset(event, () => latest.current(initialValue));
237
261
  }
238
262
  form.addEventListener("reset", onReset);
239
263
  return () => form.removeEventListener("reset", onReset);
240
- }, [commit, initialValue]);
241
- const context = (0, import_react4.useMemo)(
264
+ }, [latest, initialValue]);
265
+ const context = (0, import_react5.useMemo)(
242
266
  () => ({
243
267
  value,
244
268
  length,
@@ -267,6 +291,7 @@ function OtpInputRoot({
267
291
  }
268
292
 
269
293
  // src/otp-input/OtpInputInput.tsx
294
+ var import_react6 = require("react");
270
295
  function OtpInputInput({ render, ...rest }) {
271
296
  const { value, length, disabled, validationType, setSelection, commit, inputRef, slotAt } = useOtpInputRootContext("Input");
272
297
  function select(input, index) {
@@ -279,6 +304,48 @@ function OtpInputInput({ render, ...rest }) {
279
304
  function report(input) {
280
305
  setSelection({ start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0 });
281
306
  }
307
+ const pendingCaret = (0, import_react6.useRef)(null);
308
+ (0, import_react6.useLayoutEffect)(() => {
309
+ const input = inputRef.current;
310
+ const at = pendingCaret.current;
311
+ pendingCaret.current = null;
312
+ if (input && at !== null && input.ownerDocument.activeElement === input) select(input, at);
313
+ });
314
+ function edit(next, caret) {
315
+ pendingCaret.current = caret;
316
+ commit(next);
317
+ }
318
+ const cleared = (index) => value.slice(0, index) + GAP + value.slice(index + 1);
319
+ function onDelete(event, input) {
320
+ const start = input.selectionStart ?? 0;
321
+ const end = input.selectionEnd ?? start;
322
+ event.preventDefault();
323
+ if (end > start + 1) {
324
+ edit(value.slice(0, start) + GAP.repeat(end - start) + value.slice(end), start);
325
+ } else if (end === start + 1 && value.charAt(start) !== GAP) {
326
+ edit(cleared(start), start);
327
+ } else if (event.inputType.includes("Backward") && start > 0) {
328
+ edit(cleared(start - 1), start - 1);
329
+ }
330
+ }
331
+ const beforeInput = (0, import_react6.useRef)(() => {
332
+ });
333
+ beforeInput.current = (event) => {
334
+ const input = event.currentTarget;
335
+ if (event.inputType.startsWith("delete") && event.inputType !== "deleteByCut") {
336
+ onDelete(event, input);
337
+ } else if (event.inputType === "insertText") {
338
+ const typed = event.data ?? "";
339
+ if (typed.length > 0 && sanitize(typed, validationType) !== typed) event.preventDefault();
340
+ }
341
+ };
342
+ (0, import_react6.useEffect)(() => {
343
+ const input = inputRef.current;
344
+ if (!input) return;
345
+ const listener = (event) => beforeInput.current(event);
346
+ input.addEventListener("beforeinput", listener);
347
+ return () => input.removeEventListener("beforeinput", listener);
348
+ }, [inputRef]);
282
349
  function syncSelection(event) {
283
350
  const input = event.currentTarget;
284
351
  const start = input.selectionStart ?? 0;
@@ -301,12 +368,6 @@ function OtpInputInput({ render, ...rest }) {
301
368
  maxLength: length,
302
369
  pattern: validationType === "numeric" ? `\\d{${length}}` : `[A-Za-z0-9]{${length}}`,
303
370
  disabled: disabled || void 0,
304
- onBeforeInput(event) {
305
- const typed = event.data;
306
- if (typed && typed.length === 1 && !sanitize(typed, validationType)) {
307
- event.preventDefault();
308
- }
309
- },
310
371
  onChange(event) {
311
372
  commit(event.currentTarget.value);
312
373
  syncSelection(event);
@@ -317,12 +378,16 @@ function OtpInputInput({ render, ...rest }) {
317
378
  if (!pasted) return;
318
379
  const input = event.currentTarget;
319
380
  if (pasted.length >= length) {
320
- commit(pasted);
381
+ edit(pasted, length);
321
382
  return;
322
383
  }
323
384
  const start = input.selectionStart ?? value.length;
324
385
  const end = input.selectionEnd ?? start;
325
- commit(value.slice(0, start) + pasted + value.slice(end));
386
+ const reach = start + pasted.length;
387
+ edit(
388
+ value.slice(0, start) + pasted + GAP.repeat(Math.max(0, end - reach)) + value.slice(Math.max(end, reach)),
389
+ reach
390
+ );
326
391
  },
327
392
  onKeyDown(event) {
328
393
  if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
@@ -333,13 +398,13 @@ function OtpInputInput({ render, ...rest }) {
333
398
  select(input, event.key === "ArrowLeft" ? at - 1 : at + 1);
334
399
  },
335
400
  onClick(event) {
336
- if (event.detail > 1) return;
337
401
  const index = slotAt(event.clientX);
338
402
  if (index !== null) select(event.currentTarget, index);
339
403
  },
340
404
  onSelect: syncSelection,
341
405
  onFocus(event) {
342
- select(event.currentTarget, event.currentTarget.value.length);
406
+ const gap = value.indexOf(GAP);
407
+ select(event.currentTarget, gap === -1 ? value.length : gap);
343
408
  },
344
409
  onBlur() {
345
410
  setSelection(null);
@@ -350,15 +415,15 @@ function OtpInputInput({ render, ...rest }) {
350
415
  }
351
416
 
352
417
  // src/otp-input/OtpInputSlot.tsx
353
- var import_react5 = require("react");
418
+ var import_react7 = require("react");
354
419
  function OtpInputSlot({ index, className, children, render, ...rest }) {
355
420
  const { value, length, disabled, selection, registerSlot } = useOtpInputRootContext("Slot");
356
- const char = value.charAt(index);
421
+ const char = value.charAt(index) === GAP ? "" : value.charAt(index);
357
422
  let active = false;
358
423
  if (selection) {
359
424
  active = selection.start === selection.end ? index === Math.min(selection.start, length - 1) : index >= selection.start && index < selection.end;
360
425
  }
361
- const ref = (0, import_react5.useCallback)(
426
+ const ref = (0, import_react7.useCallback)(
362
427
  (element) => registerSlot(index, element),
363
428
  [registerSlot, index]
364
429
  );
@@ -20,8 +20,18 @@ type OtpInputRootOwnProps = {
20
20
  value?: string;
21
21
  /** Initial value when uncontrolled. Read once, at mount. */
22
22
  defaultValue?: string;
23
- /** Called with the code after every change, partial or complete. */
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
+ */
24
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;
25
35
  /** How many characters the code has, and so how many slots it shows. Defaults to 6. */
26
36
  length?: number;
27
37
  /**
@@ -48,7 +58,7 @@ type OtpInputRootProps = OtpInputRootOwnProps & Omit<ComponentPropsWithRef<'div'
48
58
  * Holds the value through `useControlled`. The input reports changes, and the Root keeps only
49
59
  * the characters `validationType` allows, up to `length`.
50
60
  */
51
- declare function OtpInputRoot({ value: valueProp, defaultValue, onValueChange, length, validationType, disabled, className, children, render, ...rest }: OtpInputRootProps): react.JSX.Element;
61
+ declare function OtpInputRoot({ value: valueProp, defaultValue, onValueChange, onComplete, length, validationType, disabled, className, children, render, ...rest }: OtpInputRootProps): react.JSX.Element;
52
62
 
53
63
  type OtpInputInputOwnProps = {
54
64
  /** Element to render instead of the default `<input>`. Props and ref are merged onto it. */
@@ -66,7 +76,11 @@ type OtpInputInputProps = OtpInputInputOwnProps & Omit<ComponentPropsWithRef<'in
66
76
  *
67
77
  * The caret always covers one character, so typing replaces the character in the active slot
68
78
  * and moves on, and the last slot of a full code is replaced rather than refused. Left and
69
- * Right move one slot; Backspace and Delete are the platform's.
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.
70
84
  *
71
85
  * Carries `maxLength` and a `pattern` of exactly `length` allowed characters, so the browser's
72
86
  * own validation reports a code that is too short. Both can be overridden.
@@ -20,8 +20,18 @@ type OtpInputRootOwnProps = {
20
20
  value?: string;
21
21
  /** Initial value when uncontrolled. Read once, at mount. */
22
22
  defaultValue?: string;
23
- /** Called with the code after every change, partial or complete. */
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
+ */
24
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;
25
35
  /** How many characters the code has, and so how many slots it shows. Defaults to 6. */
26
36
  length?: number;
27
37
  /**
@@ -48,7 +58,7 @@ type OtpInputRootProps = OtpInputRootOwnProps & Omit<ComponentPropsWithRef<'div'
48
58
  * Holds the value through `useControlled`. The input reports changes, and the Root keeps only
49
59
  * the characters `validationType` allows, up to `length`.
50
60
  */
51
- declare function OtpInputRoot({ value: valueProp, defaultValue, onValueChange, length, validationType, disabled, className, children, render, ...rest }: OtpInputRootProps): react.JSX.Element;
61
+ declare function OtpInputRoot({ value: valueProp, defaultValue, onValueChange, onComplete, length, validationType, disabled, className, children, render, ...rest }: OtpInputRootProps): react.JSX.Element;
52
62
 
53
63
  type OtpInputInputOwnProps = {
54
64
  /** Element to render instead of the default `<input>`. Props and ref are merged onto it. */
@@ -66,7 +76,11 @@ type OtpInputInputProps = OtpInputInputOwnProps & Omit<ComponentPropsWithRef<'in
66
76
  *
67
77
  * The caret always covers one character, so typing replaces the character in the active slot
68
78
  * and moves on, and the last slot of a full code is replaced rather than refused. Left and
69
- * Right move one slot; Backspace and Delete are the platform's.
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.
70
84
  *
71
85
  * Carries `maxLength` and a `pattern` of exactly `length` allowed characters, so the browser's
72
86
  * own validation reports a code that is too short. Both can be overridden.
@@ -1,3 +1,7 @@
1
+ import {
2
+ afterReset,
3
+ useLatest
4
+ } from "../chunk-JJ5FACZU.js";
1
5
  import {
2
6
  useControlled
3
7
  } from "../chunk-UEIB3I3T.js";
@@ -27,12 +31,20 @@ function useOtpInputRootContext(part) {
27
31
  }
28
32
  return context;
29
33
  }
34
+ var GAP = " ";
30
35
  function sanitize(text, validationType) {
31
36
  return validationType === "numeric" ? text.replace(/\D/g, "") : text.replace(/[^A-Za-z0-9]/g, "");
32
37
  }
38
+ function normalize(text, validationType, length) {
39
+ const allowed = validationType === "numeric" ? /[^\d ]/g : /[^A-Za-z0-9 ]/g;
40
+ return text.replace(allowed, "").slice(0, length).trimEnd();
41
+ }
42
+ function isComplete(value, length) {
43
+ return value.length === length && !value.includes(GAP);
44
+ }
33
45
  function otpInputDataAttributes({ value, length, disabled }) {
34
46
  return {
35
- "data-complete": value.length === length ? "" : void 0,
47
+ "data-complete": isComplete(value, length) ? "" : void 0,
36
48
  "data-disabled": disabled ? "" : void 0
37
49
  };
38
50
  }
@@ -43,6 +55,7 @@ function OtpInputRoot({
43
55
  value: valueProp,
44
56
  defaultValue,
45
57
  onValueChange,
58
+ onComplete,
46
59
  length = 6,
47
60
  validationType = "numeric",
48
61
  disabled = false,
@@ -59,12 +72,13 @@ function OtpInputRoot({
59
72
  });
60
73
  const commit = useCallback(
61
74
  (next) => {
62
- const clean = sanitize(next, validationType).slice(0, length);
75
+ const clean = normalize(next, validationType, length);
63
76
  if (clean === value) return;
64
77
  setValue(clean);
65
78
  onValueChange?.(clean);
79
+ if (isComplete(clean, length)) onComplete?.(clean);
66
80
  },
67
- [validationType, length, value, setValue, onValueChange]
81
+ [validationType, length, value, setValue, onValueChange, onComplete]
68
82
  );
69
83
  const [selection, setSelection] = useState(null);
70
84
  const inputRef = useRef(null);
@@ -87,17 +101,16 @@ function OtpInputRoot({
87
101
  return nearest;
88
102
  }, []);
89
103
  const { current: initialValue } = useRef(value);
104
+ const latest = useLatest(commit);
90
105
  useEffect(() => {
91
106
  const form = inputRef.current?.form;
92
107
  if (!form) return;
93
108
  function onReset(event) {
94
- queueMicrotask(() => {
95
- if (!event.defaultPrevented) commit(initialValue);
96
- });
109
+ afterReset(event, () => latest.current(initialValue));
97
110
  }
98
111
  form.addEventListener("reset", onReset);
99
112
  return () => form.removeEventListener("reset", onReset);
100
- }, [commit, initialValue]);
113
+ }, [latest, initialValue]);
101
114
  const context = useMemo(
102
115
  () => ({
103
116
  value,
@@ -127,6 +140,7 @@ function OtpInputRoot({
127
140
  }
128
141
 
129
142
  // src/otp-input/OtpInputInput.tsx
143
+ import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2 } from "react";
130
144
  function OtpInputInput({ render, ...rest }) {
131
145
  const { value, length, disabled, validationType, setSelection, commit, inputRef, slotAt } = useOtpInputRootContext("Input");
132
146
  function select(input, index) {
@@ -139,6 +153,48 @@ function OtpInputInput({ render, ...rest }) {
139
153
  function report(input) {
140
154
  setSelection({ start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0 });
141
155
  }
156
+ const pendingCaret = useRef2(null);
157
+ useLayoutEffect(() => {
158
+ const input = inputRef.current;
159
+ const at = pendingCaret.current;
160
+ pendingCaret.current = null;
161
+ if (input && at !== null && input.ownerDocument.activeElement === input) select(input, at);
162
+ });
163
+ function edit(next, caret) {
164
+ pendingCaret.current = caret;
165
+ commit(next);
166
+ }
167
+ const cleared = (index) => value.slice(0, index) + GAP + value.slice(index + 1);
168
+ function onDelete(event, input) {
169
+ const start = input.selectionStart ?? 0;
170
+ const end = input.selectionEnd ?? start;
171
+ event.preventDefault();
172
+ if (end > start + 1) {
173
+ edit(value.slice(0, start) + GAP.repeat(end - start) + value.slice(end), start);
174
+ } else if (end === start + 1 && value.charAt(start) !== GAP) {
175
+ edit(cleared(start), start);
176
+ } else if (event.inputType.includes("Backward") && start > 0) {
177
+ edit(cleared(start - 1), start - 1);
178
+ }
179
+ }
180
+ const beforeInput = useRef2(() => {
181
+ });
182
+ beforeInput.current = (event) => {
183
+ const input = event.currentTarget;
184
+ if (event.inputType.startsWith("delete") && event.inputType !== "deleteByCut") {
185
+ onDelete(event, input);
186
+ } else if (event.inputType === "insertText") {
187
+ const typed = event.data ?? "";
188
+ if (typed.length > 0 && sanitize(typed, validationType) !== typed) event.preventDefault();
189
+ }
190
+ };
191
+ useEffect2(() => {
192
+ const input = inputRef.current;
193
+ if (!input) return;
194
+ const listener = (event) => beforeInput.current(event);
195
+ input.addEventListener("beforeinput", listener);
196
+ return () => input.removeEventListener("beforeinput", listener);
197
+ }, [inputRef]);
142
198
  function syncSelection(event) {
143
199
  const input = event.currentTarget;
144
200
  const start = input.selectionStart ?? 0;
@@ -161,12 +217,6 @@ function OtpInputInput({ render, ...rest }) {
161
217
  maxLength: length,
162
218
  pattern: validationType === "numeric" ? `\\d{${length}}` : `[A-Za-z0-9]{${length}}`,
163
219
  disabled: disabled || void 0,
164
- onBeforeInput(event) {
165
- const typed = event.data;
166
- if (typed && typed.length === 1 && !sanitize(typed, validationType)) {
167
- event.preventDefault();
168
- }
169
- },
170
220
  onChange(event) {
171
221
  commit(event.currentTarget.value);
172
222
  syncSelection(event);
@@ -177,12 +227,16 @@ function OtpInputInput({ render, ...rest }) {
177
227
  if (!pasted) return;
178
228
  const input = event.currentTarget;
179
229
  if (pasted.length >= length) {
180
- commit(pasted);
230
+ edit(pasted, length);
181
231
  return;
182
232
  }
183
233
  const start = input.selectionStart ?? value.length;
184
234
  const end = input.selectionEnd ?? start;
185
- commit(value.slice(0, start) + pasted + value.slice(end));
235
+ const reach = start + pasted.length;
236
+ edit(
237
+ value.slice(0, start) + pasted + GAP.repeat(Math.max(0, end - reach)) + value.slice(Math.max(end, reach)),
238
+ reach
239
+ );
186
240
  },
187
241
  onKeyDown(event) {
188
242
  if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
@@ -193,13 +247,13 @@ function OtpInputInput({ render, ...rest }) {
193
247
  select(input, event.key === "ArrowLeft" ? at - 1 : at + 1);
194
248
  },
195
249
  onClick(event) {
196
- if (event.detail > 1) return;
197
250
  const index = slotAt(event.clientX);
198
251
  if (index !== null) select(event.currentTarget, index);
199
252
  },
200
253
  onSelect: syncSelection,
201
254
  onFocus(event) {
202
- select(event.currentTarget, event.currentTarget.value.length);
255
+ const gap = value.indexOf(GAP);
256
+ select(event.currentTarget, gap === -1 ? value.length : gap);
203
257
  },
204
258
  onBlur() {
205
259
  setSelection(null);
@@ -213,7 +267,7 @@ function OtpInputInput({ render, ...rest }) {
213
267
  import { useCallback as useCallback2 } from "react";
214
268
  function OtpInputSlot({ index, className, children, render, ...rest }) {
215
269
  const { value, length, disabled, selection, registerSlot } = useOtpInputRootContext("Slot");
216
- const char = value.charAt(index);
270
+ const char = value.charAt(index) === GAP ? "" : value.charAt(index);
217
271
  let active = false;
218
272
  if (selection) {
219
273
  active = selection.start === selection.end ? index === Math.min(selection.start, length - 1) : index >= selection.start && index < selection.end;
@@ -32,19 +32,34 @@ __export(index_parts_exports, {
32
32
  });
33
33
 
34
34
  // src/radio-group/RadioGroupRoot.tsx
35
- var import_react4 = require("react");
35
+ var import_react5 = require("react");
36
36
 
37
- // src/core/useControlled.ts
37
+ // src/core/useLatest.ts
38
38
  var import_react = require("react");
39
+ function useLatest(value) {
40
+ const ref = (0, import_react.useRef)(value);
41
+ (0, import_react.useLayoutEffect)(() => {
42
+ ref.current = value;
43
+ });
44
+ return ref;
45
+ }
46
+ function afterReset(event, work) {
47
+ setTimeout(() => {
48
+ if (!event.defaultPrevented) work();
49
+ }, 0);
50
+ }
51
+
52
+ // src/core/useControlled.ts
53
+ var import_react2 = require("react");
39
54
  function useControlled({
40
55
  controlled,
41
56
  default: defaultValue,
42
57
  name = "Component",
43
58
  state = "value"
44
59
  }) {
45
- const { current: isControlled } = (0, import_react.useRef)(controlled !== void 0);
46
- const [uncontrolled, setUncontrolled] = (0, import_react.useState)(defaultValue);
47
- const defaultRef = (0, import_react.useRef)(defaultValue);
60
+ const { current: isControlled } = (0, import_react2.useRef)(controlled !== void 0);
61
+ const [uncontrolled, setUncontrolled] = (0, import_react2.useState)(defaultValue);
62
+ const defaultRef = (0, import_react2.useRef)(defaultValue);
48
63
  const value = isControlled ? controlled : uncontrolled;
49
64
  if (process.env.NODE_ENV !== "production") {
50
65
  if (isControlled !== (controlled !== void 0)) {
@@ -58,7 +73,7 @@ function useControlled({
58
73
  );
59
74
  }
60
75
  }
61
- const setValue = (0, import_react.useCallback)(
76
+ const setValue = (0, import_react2.useCallback)(
62
77
  (next) => {
63
78
  if (!isControlled) setUncontrolled(next);
64
79
  },
@@ -68,7 +83,7 @@ function useControlled({
68
83
  }
69
84
 
70
85
  // src/core/useRender.ts
71
- var import_react2 = require("react");
86
+ var import_react3 = require("react");
72
87
 
73
88
  // src/core/mergeProps.ts
74
89
  function isEventHandler(key) {
@@ -150,17 +165,17 @@ function useRender({
150
165
  consumerProps
151
166
  }) {
152
167
  const merged = mergeProps(props, consumerProps);
153
- if ((0, import_react2.isValidElement)(render)) {
154
- return (0, import_react2.cloneElement)(render, mergeProps(merged, render.props));
168
+ if ((0, import_react3.isValidElement)(render)) {
169
+ return (0, import_react3.cloneElement)(render, mergeProps(merged, render.props));
155
170
  }
156
- return (0, import_react2.createElement)(defaultTagName, merged);
171
+ return (0, import_react3.createElement)(defaultTagName, merged);
157
172
  }
158
173
 
159
174
  // src/radio-group/RadioGroupRootContext.ts
160
- var import_react3 = require("react");
161
- var RadioGroupRootContext = (0, import_react3.createContext)(null);
175
+ var import_react4 = require("react");
176
+ var RadioGroupRootContext = (0, import_react4.createContext)(null);
162
177
  function useRadioGroupRootContext() {
163
- const context = (0, import_react3.useContext)(RadioGroupRootContext);
178
+ const context = (0, import_react4.useContext)(RadioGroupRootContext);
164
179
  if (context === null) {
165
180
  throw new Error("<RadioGroup.Item> must be rendered inside <RadioGroup.Root>.");
166
181
  }
@@ -208,34 +223,35 @@ function RadioGroupRoot({
208
223
  name: "RadioGroup.Root",
209
224
  state: "value"
210
225
  });
211
- const generatedName = (0, import_react4.useId)();
226
+ const generatedName = (0, import_react5.useId)();
212
227
  const name = nameProp ?? generatedName;
213
- const commitValue = (0, import_react4.useCallback)(
228
+ const commitValue = (0, import_react5.useCallback)(
214
229
  (next) => {
215
230
  setValue(next);
216
231
  onValueChange?.(next);
217
232
  },
218
233
  [setValue, onValueChange]
219
234
  );
220
- const elementRef = (0, import_react4.useRef)(null);
221
- const { current: initialValue } = (0, import_react4.useRef)(value);
222
- (0, import_react4.useEffect)(() => {
235
+ const elementRef = (0, import_react5.useRef)(null);
236
+ const { current: initialValue } = (0, import_react5.useRef)(value);
237
+ const latest = useLatest({ value, commit: commitValue });
238
+ (0, import_react5.useEffect)(() => {
223
239
  const owner = radiosIn(elementRef.current, name)[0]?.form;
224
240
  if (!owner) return;
225
241
  function onReset(event) {
226
- queueMicrotask(() => {
227
- if (event.defaultPrevented) return;
242
+ afterReset(event, () => {
243
+ const { value: current, commit } = latest.current;
228
244
  for (const input of radiosIn(elementRef.current, name)) {
229
- input.checked = input.value === value;
245
+ input.checked = input.value === current;
230
246
  }
231
- if (value === initialValue) return;
232
- commitValue(initialValue);
247
+ if (current === initialValue) return;
248
+ commit(initialValue);
233
249
  });
234
250
  }
235
251
  owner.addEventListener("reset", onReset);
236
252
  return () => owner.removeEventListener("reset", onReset);
237
- }, [name, value, commitValue, initialValue]);
238
- const state = (0, import_react4.useMemo)(
253
+ }, [name, latest, initialValue]);
254
+ const state = (0, import_react5.useMemo)(
239
255
  () => ({ value, disabled, required, name, form, select: commitValue }),
240
256
  [value, disabled, required, name, form, commitValue]
241
257
  );
@@ -257,7 +273,7 @@ function RadioGroupRoot({
257
273
  }
258
274
 
259
275
  // src/radio-group/RadioGroupItem.tsx
260
- var import_react5 = require("react");
276
+ var import_react6 = require("react");
261
277
  function RadioGroupItem({
262
278
  value,
263
279
  disabled: disabledProp = false,
@@ -268,7 +284,7 @@ function RadioGroupItem({
268
284
  const group = useRadioGroupRootContext();
269
285
  const disabled = group.disabled || disabledProp;
270
286
  const checked = group.value === value;
271
- const state = (0, import_react5.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
287
+ const state = (0, import_react6.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
272
288
  return useRender({
273
289
  render,
274
290
  defaultTagName: "input",
@@ -1,3 +1,7 @@
1
+ import {
2
+ afterReset,
3
+ useLatest
4
+ } from "../chunk-JJ5FACZU.js";
1
5
  import {
2
6
  useControlled
3
7
  } from "../chunk-UEIB3I3T.js";
@@ -79,22 +83,23 @@ function RadioGroupRoot({
79
83
  );
80
84
  const elementRef = useRef(null);
81
85
  const { current: initialValue } = useRef(value);
86
+ const latest = useLatest({ value, commit: commitValue });
82
87
  useEffect(() => {
83
88
  const owner = radiosIn(elementRef.current, name)[0]?.form;
84
89
  if (!owner) return;
85
90
  function onReset(event) {
86
- queueMicrotask(() => {
87
- if (event.defaultPrevented) return;
91
+ afterReset(event, () => {
92
+ const { value: current, commit } = latest.current;
88
93
  for (const input of radiosIn(elementRef.current, name)) {
89
- input.checked = input.value === value;
94
+ input.checked = input.value === current;
90
95
  }
91
- if (value === initialValue) return;
92
- commitValue(initialValue);
96
+ if (current === initialValue) return;
97
+ commit(initialValue);
93
98
  });
94
99
  }
95
100
  owner.addEventListener("reset", onReset);
96
101
  return () => owner.removeEventListener("reset", onReset);
97
- }, [name, value, commitValue, initialValue]);
102
+ }, [name, latest, initialValue]);
98
103
  const state = useMemo(
99
104
  () => ({ value, disabled, required, name, form, select: commitValue }),
100
105
  [value, disabled, required, name, form, commitValue]
@@ -32,7 +32,7 @@ __export(index_parts_exports, {
32
32
  });
33
33
 
34
34
  // src/switch/SwitchRoot.tsx
35
- var import_react5 = require("react");
35
+ var import_react6 = require("react");
36
36
 
37
37
  // src/core/useControlled.ts
38
38
  var import_react = require("react");
@@ -68,7 +68,24 @@ function useControlled({
68
68
  }
69
69
 
70
70
  // src/core/useFormReset.ts
71
+ var import_react3 = require("react");
72
+
73
+ // src/core/useLatest.ts
71
74
  var import_react2 = require("react");
75
+ function useLatest(value) {
76
+ const ref = (0, import_react2.useRef)(value);
77
+ (0, import_react2.useLayoutEffect)(() => {
78
+ ref.current = value;
79
+ });
80
+ return ref;
81
+ }
82
+ function afterReset(event, work) {
83
+ setTimeout(() => {
84
+ if (!event.defaultPrevented) work();
85
+ }, 0);
86
+ }
87
+
88
+ // src/core/useFormReset.ts
72
89
  function useFormReset({
73
90
  elementRef,
74
91
  inputRef,
@@ -76,25 +93,26 @@ function useFormReset({
76
93
  inputChecked,
77
94
  commit
78
95
  }) {
79
- const { current: initialValue } = (0, import_react2.useRef)(value);
80
- (0, import_react2.useEffect)(() => {
96
+ const { current: initialValue } = (0, import_react3.useRef)(value);
97
+ const latest = useLatest({ value, inputChecked, commit });
98
+ (0, import_react3.useEffect)(() => {
81
99
  const form = elementRef.current?.form;
82
100
  if (!form) return;
83
101
  function onReset(event) {
84
- queueMicrotask(() => {
85
- if (event.defaultPrevented) return;
86
- if (inputRef.current) inputRef.current.checked = inputChecked;
87
- if (value === initialValue) return;
88
- commit(initialValue);
102
+ afterReset(event, () => {
103
+ const { value: current, inputChecked: checked, commit: set } = latest.current;
104
+ if (inputRef.current) inputRef.current.checked = checked;
105
+ if (current === initialValue) return;
106
+ set(initialValue);
89
107
  });
90
108
  }
91
109
  form.addEventListener("reset", onReset);
92
110
  return () => form.removeEventListener("reset", onReset);
93
- }, [elementRef, inputRef, value, inputChecked, commit, initialValue]);
111
+ }, [elementRef, inputRef, latest, initialValue]);
94
112
  }
95
113
 
96
114
  // src/core/useRender.ts
97
- var import_react3 = require("react");
115
+ var import_react4 = require("react");
98
116
 
99
117
  // src/core/mergeProps.ts
100
118
  function isEventHandler(key) {
@@ -176,17 +194,17 @@ function useRender({
176
194
  consumerProps
177
195
  }) {
178
196
  const merged = mergeProps(props, consumerProps);
179
- if ((0, import_react3.isValidElement)(render)) {
180
- return (0, import_react3.cloneElement)(render, mergeProps(merged, render.props));
197
+ if ((0, import_react4.isValidElement)(render)) {
198
+ return (0, import_react4.cloneElement)(render, mergeProps(merged, render.props));
181
199
  }
182
- return (0, import_react3.createElement)(defaultTagName, merged);
200
+ return (0, import_react4.createElement)(defaultTagName, merged);
183
201
  }
184
202
 
185
203
  // src/switch/SwitchRootContext.ts
186
- var import_react4 = require("react");
187
- var SwitchRootContext = (0, import_react4.createContext)(null);
204
+ var import_react5 = require("react");
205
+ var SwitchRootContext = (0, import_react5.createContext)(null);
188
206
  function useSwitchRootContext() {
189
- const context = (0, import_react4.useContext)(SwitchRootContext);
207
+ const context = (0, import_react5.useContext)(SwitchRootContext);
190
208
  if (context === null) {
191
209
  throw new Error("<Switch.Thumb> must be rendered inside <Switch.Root>.");
192
210
  }
@@ -222,16 +240,16 @@ function SwitchRoot({
222
240
  name: "Switch.Root",
223
241
  state: "checked"
224
242
  });
225
- const commitChecked = (0, import_react5.useCallback)(
243
+ const commitChecked = (0, import_react6.useCallback)(
226
244
  (next) => {
227
245
  setChecked(next);
228
246
  onCheckedChange?.(next);
229
247
  },
230
248
  [setChecked, onCheckedChange]
231
249
  );
232
- const state = (0, import_react5.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
233
- const elementRef = (0, import_react5.useRef)(null);
234
- const inputRef = (0, import_react5.useRef)(null);
250
+ const state = (0, import_react6.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
251
+ const elementRef = (0, import_react6.useRef)(null);
252
+ const inputRef = (0, import_react6.useRef)(null);
235
253
  useFormReset({
236
254
  elementRef,
237
255
  inputRef,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  useFormReset
3
- } from "../chunk-IWUVUY76.js";
3
+ } from "../chunk-FDGT4JPK.js";
4
+ import "../chunk-JJ5FACZU.js";
4
5
  import {
5
6
  useControlled
6
7
  } from "../chunk-UEIB3I3T.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.18.0",
3
+ "version": "4.19.1",
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",