formstand 0.4.0 → 0.4.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.
package/dist/index.js CHANGED
@@ -272,6 +272,7 @@ var dirtyPathsOf = (values, initial, prefix) => {
272
272
  return [prefix];
273
273
  };
274
274
  var FORM_SEQ_KEY = "__form__";
275
+ var SUBMIT_SEQ_KEY = "__submit__";
275
276
  var stringArraysEqual = (a, b) => a === b || a.length === b.length && a.every((v, i) => v === b[i]);
276
277
  var reuseErrorRefs = (next, prev) => {
277
278
  if (next === prev) return prev;
@@ -464,19 +465,18 @@ var createForm = (schema, options) => {
464
465
  };
465
466
  };
466
467
  const sequences = /* @__PURE__ */ new Map();
467
- const seqRef = { current: 0 };
468
- const nextSeq = (key) => {
469
- seqRef.current += 1;
470
- sequences.set(key, seqRef.current);
471
- return seqRef.current;
468
+ const claimPass = (key) => {
469
+ const pass = Symbol(key);
470
+ sequences.set(key, pass);
471
+ return pass;
472
472
  };
473
473
  const validateAsyncOnForm = async () => {
474
- const seq = nextSeq(FORM_SEQ_KEY);
474
+ const pass = claimPass(FORM_SEQ_KEY);
475
475
  const valuesAtStart = store.getState().values;
476
476
  store.setState((state) => ({ ...state, isValidatingForm: true }));
477
477
  const result = await validateAsync(schema, valuesAtStart);
478
478
  const live = store.getState();
479
- const stillOwns = sequences.get(FORM_SEQ_KEY) === seq;
479
+ const stillOwns = sequences.get(FORM_SEQ_KEY) === pass;
480
480
  if (!stillOwns || live.values !== valuesAtStart) {
481
481
  if (stillOwns) {
482
482
  store.setState((state) => ({ ...state, isValidatingForm: false }));
@@ -499,7 +499,7 @@ var createForm = (schema, options) => {
499
499
  const result2 = await validateAsyncOnForm();
500
500
  return result2.kind === "valid";
501
501
  }
502
- const seqs = new Map(paths.map((p) => [p, nextSeq(p)]));
502
+ const passes = new Map(paths.map((p) => [p, claimPass(p)]));
503
503
  const valuesAtStart = store.getState().values;
504
504
  store.setState((state) => ({
505
505
  ...state,
@@ -514,7 +514,9 @@ var createForm = (schema, options) => {
514
514
  (k) => !inScopeOfAny(k, paths)
515
515
  );
516
516
  const live = store.getState();
517
- const stillCurrent = paths.filter((p) => sequences.get(p) === seqs.get(p));
517
+ const stillCurrent = paths.filter(
518
+ (p) => sequences.get(p) === passes.get(p)
519
+ );
518
520
  if (live.values !== valuesAtStart) {
519
521
  if (stillCurrent.length > 0) {
520
522
  store.setState((state) => ({
@@ -555,7 +557,7 @@ var createForm = (schema, options) => {
555
557
  result.kind === "invalid" ? result.errors : emptyErrors
556
558
  );
557
559
  }
558
- const seq = nextSeq(path);
560
+ const pass = claimPass(path);
559
561
  const valuesAtStart = store.getState().values;
560
562
  store.setState((state) => ({
561
563
  ...state,
@@ -563,7 +565,7 @@ var createForm = (schema, options) => {
563
565
  }));
564
566
  const scoped = await scopedFieldErrorsAsync(path, valuesAtStart);
565
567
  const live = store.getState();
566
- const stillOwns = sequences.get(path) === seq;
568
+ const stillOwns = sequences.get(path) === pass;
567
569
  if (!stillOwns || live.values !== valuesAtStart) {
568
570
  if (stillOwns) {
569
571
  store.setState((state) => ({
@@ -642,11 +644,12 @@ var createForm = (schema, options) => {
642
644
  submitCount: state.submitCount + 1
643
645
  }));
644
646
  try {
647
+ const pass = claimPass(SUBMIT_SEQ_KEY);
645
648
  const valuesAtStart = store.getState().values;
646
649
  const result = await validateAsync(schema, valuesAtStart);
647
- const staleValues = store.getState().values !== valuesAtStart;
650
+ const stale = sequences.get(SUBMIT_SEQ_KEY) !== pass || store.getState().values !== valuesAtStart;
648
651
  if (result.kind === "invalid") {
649
- if (!staleValues) {
652
+ if (!stale) {
650
653
  store.setState((state) => {
651
654
  const committed = commitFullPass(state, result.errors);
652
655
  return {
@@ -664,11 +667,11 @@ var createForm = (schema, options) => {
664
667
  };
665
668
  });
666
669
  }
667
- const merged = staleValues ? result.errors : store.getState().errors;
670
+ const merged = stale ? result.errors : store.getState().errors;
668
671
  onInvalid?.(merged);
669
672
  return { kind: "invalid", errors: merged };
670
673
  }
671
- if (!staleValues) {
674
+ if (!stale) {
672
675
  store.setState((state) => ({
673
676
  ...state,
674
677
  ...commitFullPass(state, emptyErrors)
@@ -921,7 +924,13 @@ var createForm = (schema, options) => {
921
924
  errors: reuseErrorRefs(
922
925
  mergeErrorChannels(schemaErrors, serverErrors),
923
926
  snap.errors ?? emptyErrors
924
- )
927
+ ),
928
+ // In-flight state is owned by live passes, never by snapshots: a
929
+ // snapshotted flag has no pass left to clear it (the pass that set
930
+ // it cleared its ORIGINAL entry when it settled), so restoring one
931
+ // would stick it forever.
932
+ isValidating: emptyBools,
933
+ isValidatingForm: false
925
934
  };
926
935
  })
927
936
  });
@@ -1077,7 +1086,10 @@ function useField(form, pathArg, options) {
1077
1086
  [form, path]
1078
1087
  );
1079
1088
  const setError = useCallback(
1080
- (errors) => form.setError(path, errors),
1089
+ // Normalize the string shorthand HERE, before forwarding: custom
1090
+ // FieldFormApi implementations written against the pre-0.4 surface are
1091
+ // typed for readonly string[] and must never receive a bare string.
1092
+ (errors) => form.setError(path, typeof errors === "string" ? [errors] : errors),
1081
1093
  [form, path]
1082
1094
  );
1083
1095
  const clearError = useCallback(
@@ -1346,9 +1358,14 @@ var focusableControls = (scope) => [
1346
1358
  ].filter(
1347
1359
  (el) => !el.matches(':disabled, [type="hidden"]') && el.closest("dialog:not([open])") === null
1348
1360
  );
1361
+ var holdsFocus = (el) => {
1362
+ const rootNode = el.getRootNode();
1363
+ const active = rootNode instanceof Document || rootNode instanceof ShadowRoot ? rootNode.activeElement : document.activeElement;
1364
+ return active === el;
1365
+ };
1349
1366
  var focusFirstOf = (candidates) => candidates.some((el) => {
1350
1367
  el.focus();
1351
- return document.activeElement === el;
1368
+ return holdsFocus(el);
1352
1369
  });
1353
1370
  var nameMatchesAny = (el, paths) => {
1354
1371
  const name = el.getAttribute("name");
@@ -1368,11 +1385,10 @@ var focusFirstError = (errors, root) => {
1368
1385
  const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls : [];
1369
1386
  return focusFirstOf(candidates);
1370
1387
  };
1371
- var focusField = (path, root) => focusFirstOf(
1372
- focusableControls(root ?? document).filter(
1373
- (el) => nameMatchesAny(el, [path])
1374
- )
1375
- );
1388
+ var focusField = (path, root) => {
1389
+ const controls = focusableControls(root ?? document);
1390
+ return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls) : focusFirstOf(controls.filter((el) => nameMatchesAny(el, [path])));
1391
+ };
1376
1392
  var EMPTY_ID_STATE = { items: [], ids: [], counter: 0 };
1377
1393
  var EMPTY_ITEMS = [];
1378
1394
  var reconcileIds = (prev, nextItems) => {