@formancy/react 0.5.0 → 0.7.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/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ComponentType, ReactElement, ReactNode } from "react";
2
2
  import { ControlProps, FieldSnapshot, FormEngine, Path } from "@formancy/core";
3
3
  import { FormText, FormText as FormText$1, FormWordId, FormWordValues, FormWords, FormWordsByLocale, FormWordsByLocale as FormWordsByLocale$1 } from "@formancy/core/words";
4
- import { FieldType, RemoteOption, RichCommand } from "@formancy/spec";
4
+ import { FieldType, RemoteOption, RemoteOptionsAnswer, RemoteOptionsAnswer as RemoteOptionsAnswer$1, RemoteOptionsPage, RichCommand } from "@formancy/spec";
5
5
  //#region src/context.d.ts
6
6
  export declare function FormancyProvider({ engine, words, children }: {
7
7
  engine: FormEngine;
@@ -58,6 +58,10 @@ interface RepeaterBinding {
58
58
  rowCount: number;
59
59
  /** Stable per-row keys, in row order. Never key a row by its index. */
60
60
  rowIds: readonly string[];
61
+ /** Whether the engine would add a row: under the repeater's `maxItems`, or unbounded. */
62
+ canAdd: boolean;
63
+ /** Whether the engine would remove a row: above the repeater's `minItems`. */
64
+ canRemove: boolean;
61
65
  addRow(): void;
62
66
  removeRow(index: number): void;
63
67
  /** Move a row. The keyboard route to reordering; a drag is a second route to this. */
@@ -246,7 +250,13 @@ interface OptionsRequest {
246
250
  * shows an opaque identifier. So the source answers in the right language instead.
247
251
  */
248
252
  locale: string;
249
- /** How many rows the control can show. A hint: the control caps what arrives anyway. */
253
+ /**
254
+ * How many rows the control can show. A hint: the control caps what arrives anyway.
255
+ *
256
+ * A source that pages reads `limit + 1` rows to know whether there are more, answers a
257
+ * page of `limit`, and never counts the rest: a count reads every matching row on every
258
+ * keystroke, which is the scan paging exists to avoid.
259
+ */
250
260
  limit: number;
251
261
  /** Aborted when the answer stops being wanted — a newer keystroke, or an unmount. */
252
262
  signal: AbortSignal;
@@ -261,10 +271,26 @@ interface OptionsRequest {
261
271
  * surface on a self-hosted instance.
262
272
  */
263
273
  interface OptionsSource {
264
- resolve(request: OptionsRequest): Promise<readonly RemoteOption[]>;
274
+ /**
275
+ * Every matching row, as a list — exactly as before a page could be answered, its true
276
+ * total included — or one page of them: `{ options, more, withheld? }`.
277
+ *
278
+ * `more` says rows match beyond these, and the control says so without a number, since
279
+ * nobody counted one. `withheld` counts rows the source matched and cannot hand over — a
280
+ * value a select could not store — because a source withholds and counts rather than
281
+ * filtering silently. A page with any other key, a `more` that is not a boolean or a
282
+ * `withheld` that is not a whole count is refused whole, and the field says its source
283
+ * failed rather than showing a list that looks complete
284
+ * ([0177](../../../docs/decisions/0177-a-source-may-say-its-list-goes-on.md)).
285
+ */
286
+ resolve(request: OptionsRequest): Promise<RemoteOptionsAnswer$1>;
265
287
  /** How long to wait after a keystroke before asking. The control has a default. */
266
288
  debounceMs?: number;
267
- /** Below this many characters, do not ask at all. The control says so on screen. */
289
+ /**
290
+ * Below this many characters, do not ask at all. The control says so on screen: a
291
+ * typeahead how many to type, and a plain select — whose query is always empty, so it
292
+ * never asks — that its answers are found by searching.
293
+ */
268
294
  minQueryLength?: number;
269
295
  /** How many rows to show at once. */
270
296
  maxRows?: number;
@@ -509,5 +535,5 @@ interface ResumeNoticeProps {
509
535
  }
510
536
  export declare function ResumeNotice({ migration, labels }: ResumeNoticeProps): ReactElement | null;
511
537
  //#endregion
512
- export type { ErrorSummaryProps, FieldBinding, FieldComponent, FieldComponentProps, FormText, FormWordId, FormWordValues, FormWords, FormWordsByLocale, FormancyFormProps, OptionsRequest, OptionsSource, OptionsSources, Registry, RepeaterBinding, ResumeMigration, ResumeNoticeProps, RichTextEditorFactory, RichTextEditorHandle, RichTextEditorMount, ScanRequest, Scanner, StoredFile, SubmitOutcome, UploadOptions, Uploader, WizardBinding };
538
+ export type { ErrorSummaryProps, FieldBinding, FieldComponent, FieldComponentProps, FormText, FormWordId, FormWordValues, FormWords, FormWordsByLocale, FormancyFormProps, OptionsRequest, OptionsSource, OptionsSources, Registry, RemoteOption, RemoteOptionsAnswer, RemoteOptionsPage, RepeaterBinding, ResumeMigration, ResumeNoticeProps, RichTextEditorFactory, RichTextEditorHandle, RichTextEditorMount, ScanRequest, Scanner, StoredFile, SubmitOutcome, UploadOptions, Uploader, WizardBinding };
513
539
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/context.tsx","../src/use-field.ts","../src/use-repeater.ts","../src/use-wizard.ts","../src/use-submit.ts","../src/fields/internals.tsx","../src/form.tsx","../src/error-summary.tsx","../src/rich-text.tsx","../src/options-source.ts","../src/scanning.ts","../src/uploads.ts","../src/rich-text-editor.ts","../src/resume-notice.tsx"],"mappings":";;;;;wBAsBgB,mBACd,QACA,OACA;EAEA,QAAQ;;;;;;;EAOR,QAAQ;EACR,WAAW;oBACZ,IAAA;wBAsBe,iBAAiB;;;;;;;;;wBAkBjB,eAAe;;;UCvEd,qBAAqB;EACpC,SAAS;EACT;;EAEA,cAAc;;EAEd;IAAc;IAAY;;;EAE1B;IAAc;;;;;;;;;;;;;wBAaA,SAAS,eAAe,OAAO;;;UCrB9B;EACf;;EAEA;EACA;EACA,UAAU;;EAEV,QAAQ,cAAc;;;;;;;wBAQR,YAAY,eAAe,OAAO;;;UCjBjC;EACf;EACA;;;EAGA;EACA;;EAEA,QAAQ;EACR;;EAEA,KAAK;;;;;;;wBAQS,aAAa;;;;;;;;;;;wBCTb;EAAqB;EAAa,QAAQ;;;;;;;;;;;;;;;;;UCQzC;EACf;EACA;;KAGU,iBAAiB,cAAc;UAE1B;EACf,SAAS,QAAQ,OAAO,WAAW;EACnC,SAAS,eAAe;;UAGT;EACf;EACA,QAAQ;;EAER;;UAGe;;;;;;;EAOf,SAAS;EACT,WAAW;;;;;;;EAOX;EACA;EACA,YAAY,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;wBCAP,aAAa,OAAO,oCAAiB,IAAA;;;UCpDpC;EACf,SAAS;;;;;;;;;;;wBAYK,eAAe,UAAU,oCAAiB,IAAA;;;;;;;;;;;;;;;;;;;;wBCI1C,WAAW;EAAY;IAAmB;;;;;;;;;;UCZzC;;;;;EAKf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;EASA;;EAEA;;EAEA,QAAQ;;;;;;;;;;;UAYO;EACf,QAAQ,SAAS,iBAAiB,iBAAiB;;EAEnD;;EAEA;;EAEA;;;;;;;;;;;;;KAcU,iBAAiB,SAAS,eAAe;qBAIxC,wCAAsB,SAAA,SAAA,eAAA;;;;;;;;;wBAUnB,qBAAqB;;;;;;;;;;;;UCxEpB;;EAEf;;EAEA;;;;;;;;;;;;;;;;;;;KAoBU,WAAW,SAAS,gBAAgB;qBAInC,iCAAe,SAAA;;;;;;;;wBASZ,cAAc;;;;;;;;;;;;;UCpCb;;EAEf;EACA;EACA;EACA;;EAEA;;;;;;;;;;UAWe;;;;;EAKf;;EAEA,QAAQ;;EAER,WAAW,cAAc;;;;;;;;;KAUf,YAAY,MAAM,MAAM,SAAS,kBAAkB,QAAQ;qBAI1D,kCAAgB,SAAA;;;;;;;;;wBAUb,eAAe;;;;;;;;;;;;;;;;;;;;;UC1Cd;;EAEf;;EAEA,WAAW;;;;;;;;;;;;;EAaX,MAAM,SAAS,aAAa;;EAE5B,WAAW,SAAS;EACpB;;UAGe;;WAEN,SAAS;;WAET;;WAEA,WAAW;WACX;;;;;;;;;;WAUA,YAAY,SAAS;;KAGpB,yBAAyB,OAAO,wBAAwB;qBAIvD,wCAAsB,SAAA;;;;;;;;;wBAUnB,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UC9C3B;WACN;WACA,SAAS;aAAyB;aAAuB;;;UAGnD;;WAEN,YAAY;;WAEZ,SAAS,SAAS;;wBAGb,eAAe,WAAW,UAAU,oBAAoB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/context.tsx","../src/use-field.ts","../src/use-repeater.ts","../src/use-wizard.ts","../src/use-submit.ts","../src/fields/internals.tsx","../src/form.tsx","../src/error-summary.tsx","../src/rich-text.tsx","../src/options-source.ts","../src/scanning.ts","../src/uploads.ts","../src/rich-text-editor.ts","../src/resume-notice.tsx"],"mappings":";;;;;wBAsBgB,mBACd,QACA,OACA;EAEA,QAAQ;;;;;;;EAOR,QAAQ;EACR,WAAW;oBACZ,IAAA;wBAsBe,iBAAiB;;;;;;;;;wBAkBjB,eAAe;;;UCvEd,qBAAqB;EACpC,SAAS;EACT;;EAEA,cAAc;;EAEd;IAAc;IAAY;;;EAE1B;IAAc;;;;;;;;;;;;;wBAaA,SAAS,eAAe,OAAO;;;UCrB9B;EACf;;EAEA;;EAEA;;EAEA;EACA;EACA,UAAU;;EAEV,QAAQ,cAAc;;;;;;;wBAQR,YAAY,eAAe,OAAO;;;UCrBjC;EACf;EACA;;;EAGA;EACA;;EAEA,QAAQ;EACR;;EAEA,KAAK;;;;;;;wBAQS,aAAa;;;;;;;;;;;wBCTb;EAAqB;EAAa,QAAQ;;;;;;;;;;;;;;;;;UCQzC;EACf;EACA;;KAGU,iBAAiB,cAAc;UAE1B;EACf,SAAS,QAAQ,OAAO,WAAW;EACnC,SAAS,eAAe;;UAGT;EACf;EACA,QAAQ;;EAER;;UAGe;;;;;;;EAOf,SAAS;EACT,WAAW;;;;;;;EAOX;EACA;EACA,YAAY,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;wBCCP,aAAa,OAAO,oCAAiB,IAAA;;;UCrDpC;EACf,SAAS;;;;;;;;;;;wBAYK,eAAe,UAAU,oCAAiB,IAAA;;;;;;;;;;;;;;;;;;;;wBCI1C,WAAW;EAAY;IAAmB;;;;;;;;;;UCZzC;;;;;EAKf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;EASA;;;;;;;;EAQA;;EAEA,QAAQ;;;;;;;;;;;UAYO;;;;;;;;;;;;;EAaf,QAAQ,SAAS,iBAAiB,QAAQ;;EAE1C;;;;;;EAMA;;EAEA;;;;;;;;;;;;;KAcU,iBAAiB,SAAS,eAAe;qBAIxC,wCAAsB,SAAA,SAAA,eAAA;;;;;;;;;wBAUnB,qBAAqB;;;;;;;;;;;;UC9FpB;;EAEf;;EAEA;;;;;;;;;;;;;;;;;;;KAoBU,WAAW,SAAS,gBAAgB;qBAInC,iCAAe,SAAA;;;;;;;;wBASZ,cAAc;;;;;;;;;;;;;UCpCb;;EAEf;EACA;EACA;EACA;;EAEA;;;;;;;;;;UAWe;;;;;EAKf;;EAEA,QAAQ;;EAER,WAAW,cAAc;;;;;;;;;KAUf,YAAY,MAAM,MAAM,SAAS,kBAAkB,QAAQ;qBAI1D,kCAAgB,SAAA;;;;;;;;;wBAUb,eAAe;;;;;;;;;;;;;;;;;;;;;UC1Cd;;EAEf;;EAEA,WAAW;;;;;;;;;;;;;EAaX,MAAM,SAAS,aAAa;;EAE5B,WAAW,SAAS;EACpB;;UAGe;;WAEN,SAAS;;WAET;;WAEA,WAAW;WACX;;;;;;;;;;WAUA,YAAY,SAAS;;KAGpB,yBAAyB,OAAO,wBAAwB;qBAIvD,wCAAsB,SAAA;;;;;;;;;wBAUnB,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UC9C3B;WACN;WACA,SAAS;aAAyB;aAAuB;;;UAGnD;;WAEN,YAAY;;WAEZ,SAAS,SAAS;;wBAGb,eAAe,WAAW,UAAU,oBAAoB"}
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Fragment, createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
2
  import { cancelUploads, fieldUploads, formatPath, parsePath, placedGroup, placedPage, thumbnailSize } from "@formancy/core";
3
- import { createFormText } from "@formancy/core/words";
4
- import { LAYOUT_LEAF_KINDS, acceptRemoteOptions, answerFromText, applyRichCommand, capRemoteOptions, datagridColumns, editMasked, formatMasked, isImageSource, layoutChildren, layoutNodeShows, maskIsNumeric, maskPlaceholder, narrowOptionsByLabel, parseRichText, resolveText, resolvedLocale } from "@formancy/spec";
3
+ import { createFormText, optionsStatus } from "@formancy/core/words";
4
+ import { LAYOUT_LEAF_KINDS, acceptRemoteAnswer, answerFromText, applyRichCommand, capRemoteAnswer, datagridColumns, editMasked, formatMasked, isImageSource, layoutChildren, layoutNodeShows, maskIsNumeric, maskPlaceholder, narrowOptionsByLabel, parseRichText, resolveText, resolvedLocale } from "@formancy/spec";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { flushSync } from "react-dom";
7
7
  import { encode } from "uqr";
@@ -103,6 +103,10 @@ function useRepeater(path) {
103
103
  const subscribe = useCallback((onChange) => engine.subscribeField(parsed, onChange), [engine, parsed]);
104
104
  const getRowCount = useCallback(() => engine.rowCount(parsed), [engine, parsed]);
105
105
  const rowCount = useSyncExternalStore(subscribe, getRowCount, getRowCount);
106
+ const getCanAdd = useCallback(() => engine.canAddRow(parsed), [engine, parsed]);
107
+ const canAdd = useSyncExternalStore(subscribe, getCanAdd, getCanAdd);
108
+ const getCanRemove = useCallback(() => engine.canRemoveRow(parsed), [engine, parsed]);
109
+ const canRemove = useSyncExternalStore(subscribe, getCanRemove, getCanRemove);
106
110
  const addRow = useCallback(() => engine.addRow(parsed), [engine, parsed]);
107
111
  const removeRow = useCallback((index) => engine.removeRow(parsed, index), [engine, parsed]);
108
112
  const moveRow = useCallback((from, to) => engine.moveRow(parsed, from, to), [engine, parsed]);
@@ -112,12 +116,16 @@ function useRepeater(path) {
112
116
  return useMemo(() => ({
113
117
  rowCount,
114
118
  rowIds,
119
+ canAdd,
120
+ canRemove,
115
121
  addRow,
116
122
  removeRow,
117
123
  moveRow
118
124
  }), [
119
125
  rowCount,
120
126
  rowIds,
127
+ canAdd,
128
+ canRemove,
121
129
  addRow,
122
130
  removeRow,
123
131
  moveRow
@@ -547,9 +555,13 @@ function useOptionsSources() {
547
555
  const DEBOUNCE_MS = 250;
548
556
  const MIN_QUERY = 0;
549
557
  const MAX_ROWS = 50;
558
+ /**
559
+ * Nothing left out, as one value: a reset that stores it again is the same state, so
560
+ * React bails out of the render rather than repeating one.
561
+ */
562
+ const COMPLETE = { kind: "complete" };
550
563
  function useSourcedOptions(field, query, enabled = true) {
551
564
  const engine = useFormEngine();
552
- const text = useFormText();
553
565
  const sources = useOptionsSources();
554
566
  const name = field.def.optionsSource;
555
567
  const source = name === void 0 ? void 0 : sources?.[name];
@@ -557,19 +569,28 @@ function useSourcedOptions(field, query, enabled = true) {
557
569
  const [named, setNamed] = useState(/* @__PURE__ */ new Map());
558
570
  const [busy, setBusy] = useState(false);
559
571
  const [failed, setFailed] = useState(false);
560
- const [capped, setCapped] = useState(null);
572
+ const [completeness, setCompleteness] = useState(COMPLETE);
573
+ const [attempt, setAttempt] = useState(0);
574
+ const retry = useCallback(() => setAttempt((was) => was + 1), []);
561
575
  const debounceMs = source?.debounceMs ?? DEBOUNCE_MS;
562
576
  const minQueryLength = source?.minQueryLength ?? MIN_QUERY;
563
577
  const maxRows = source?.maxRows ?? MAX_ROWS;
564
578
  const stored = typeof field.value === "string" && field.value !== "" ? field.value : void 0;
565
579
  const path = field.path;
580
+ const disabled = field.disabled;
566
581
  const asked = useRef(0);
567
582
  useEffect(() => {
568
- if (!enabled || source === void 0) return;
569
583
  const generation = asked.current += 1;
584
+ if (!enabled || source === void 0) return;
585
+ if (disabled) {
586
+ setBusy(false);
587
+ setFailed(false);
588
+ setCompleteness(COMPLETE);
589
+ return;
590
+ }
570
591
  if (query.trim().length < minQueryLength) {
571
592
  setRows([]);
572
- setCapped(null);
593
+ setCompleteness(COMPLETE);
573
594
  setBusy(false);
574
595
  return;
575
596
  }
@@ -588,23 +609,21 @@ function useSourcedOptions(field, query, enabled = true) {
588
609
  signal: controller.signal
589
610
  }).then((answer) => {
590
611
  if (generation !== asked.current) return;
591
- const accepted = acceptRemoteOptions(answer);
612
+ const accepted = acceptRemoteAnswer(answer);
592
613
  if (accepted === void 0) {
593
614
  setFailed(true);
594
615
  setRows([]);
595
- setCapped(null);
616
+ setCompleteness(COMPLETE);
596
617
  return;
597
618
  }
598
- const { shown, total, capped: wasCapped } = capRemoteOptions(accepted, maxRows);
599
- setRows(shown);
600
- setCapped(wasCapped ? {
601
- shown: shown.length,
602
- total
603
- } : null);
619
+ const settled = capRemoteAnswer(accepted, maxRows);
620
+ setRows(settled.shown);
621
+ setCompleteness(settled.completeness);
604
622
  }).catch(() => {
605
623
  if (generation !== asked.current) return;
606
624
  setFailed(true);
607
625
  setRows([]);
626
+ setCompleteness(COMPLETE);
608
627
  }).finally(() => {
609
628
  if (generation === asked.current) setBusy(false);
610
629
  });
@@ -615,6 +634,7 @@ function useSourcedOptions(field, query, enabled = true) {
615
634
  };
616
635
  }, [
617
636
  enabled,
637
+ disabled,
618
638
  source,
619
639
  name,
620
640
  path,
@@ -622,7 +642,8 @@ function useSourcedOptions(field, query, enabled = true) {
622
642
  minQueryLength,
623
643
  maxRows,
624
644
  debounceMs,
625
- engine
645
+ engine,
646
+ attempt
626
647
  ]);
627
648
  const askedFor = useRef(/* @__PURE__ */ new Set());
628
649
  useEffect(() => {
@@ -640,11 +661,11 @@ function useSourcedOptions(field, query, enabled = true) {
640
661
  limit: 1,
641
662
  signal: controller.signal
642
663
  }).then((answer) => {
643
- const accepted = acceptRemoteOptions(answer);
664
+ const accepted = acceptRemoteAnswer(answer);
644
665
  if (accepted === void 0) return;
645
666
  setNamed((was) => {
646
667
  const next = new Map(was);
647
- for (const row of accepted) next.set(row.value, row.label);
668
+ for (const row of accepted.options) next.set(row.value, row.label);
648
669
  return next;
649
670
  });
650
671
  }).catch(() => void 0);
@@ -677,33 +698,11 @@ function useSourcedOptions(field, query, enabled = true) {
677
698
  busy,
678
699
  failed,
679
700
  needs: minQueryLength,
680
- status: statusFor(text, {
681
- failed,
682
- busy,
683
- needs: minQueryLength,
684
- query,
685
- capped
686
- })
701
+ completeness,
702
+ retry
687
703
  }
688
704
  };
689
705
  }
690
- /**
691
- * The one thing the field says out loud, and never more than one.
692
- *
693
- * Success announces nothing: the listbox and `aria-activedescendant` are the
694
- * feedback, and a region that spoke on every successful keystroke would talk over
695
- * them. A failure is a sentence somebody can act on rather than a code.
696
- */
697
- function statusFor(text, { failed, busy, needs, query, capped }) {
698
- if (failed) return text("options.failed");
699
- if (query.trim().length < needs) return text("options.tooShort", { count: needs });
700
- if (busy) return text("options.searching");
701
- if (capped !== null) return text("options.capped", {
702
- shown: capped.shown,
703
- total: capped.total
704
- });
705
- return "";
706
- }
707
706
  //#endregion
708
707
  //#region src/uploads.ts
709
708
  const UploaderContext = createContext(void 0);
@@ -784,11 +783,11 @@ function FieldShell({ path, field, label, children }) {
784
783
  * Not the text: a theme that keyed off English prose would break in every other
785
784
  * language the form is offered in.
786
785
  */
787
- function statusState({ sourced, open, matches }) {
786
+ function statusState({ sourced, said, open, matches }) {
788
787
  if (sourced.remote === null) return open && matches === 0 ? "empty" : void 0;
789
788
  if (sourced.remote.busy) return "busy";
790
789
  if (sourced.remote.failed) return "failed";
791
- if (sourced.remote.status !== "") return "hint";
790
+ if (said !== "") return "hint";
792
791
  return open && matches === 0 ? "empty" : void 0;
793
792
  }
794
793
  /**
@@ -867,7 +866,11 @@ function TypeaheadSelectField({ path, label, field }) {
867
866
  * first clears the answer on the way out.
868
867
  */
869
868
  const [query, setQuery] = useState(null);
870
- const [open, setOpen] = useState(false);
869
+ const [opened, setOpen] = useState(false);
870
+ /** A field disabled by a rule while its list was showing has no list: nobody can
871
+ * choose from it, and its region would say "no options match" under a read-only
872
+ * answer. */
873
+ const open = opened && !field.disabled;
871
874
  /** The arrowed-over option by VALUE, not by index: the filtered list changes on
872
875
  * every keystroke and an index would point at a different row after one. */
873
876
  const [activeValue, setActiveValue] = useState(null);
@@ -876,6 +879,16 @@ function TypeaheadSelectField({ path, label, field }) {
876
879
  path
877
880
  }, query ?? "");
878
881
  const options = sourced.options;
882
+ const said = sourced.remote === null ? "" : optionsStatus(text, {
883
+ control: "typeahead",
884
+ open,
885
+ disabled: field.disabled,
886
+ failed: sourced.remote.failed,
887
+ busy: sourced.remote.busy,
888
+ needs: sourced.remote.needs,
889
+ query: query ?? "",
890
+ completeness: sourced.remote.completeness
891
+ });
879
892
  const chosen = options.find((option) => option.value === field.value);
880
893
  const matches = sourced.remote === null ? narrowOptionsByLabel(options, query ?? "") : options;
881
894
  /** Collapsed whenever there is nothing on the screen, so `aria-expanded` never
@@ -988,14 +1001,16 @@ function TypeaheadSelectField({ path, label, field }) {
988
1001
  "data-formancy-part": "typeahead-status",
989
1002
  ...statusState({
990
1003
  sourced,
1004
+ said,
991
1005
  open,
992
1006
  matches: matches.length
993
1007
  }) === void 0 ? {} : { "data-state": statusState({
994
1008
  sourced,
1009
+ said,
995
1010
  open,
996
1011
  matches: matches.length
997
1012
  }) },
998
- children: sourced.remote !== null && sourced.remote.status !== "" ? sourced.remote.status : open && matches.length === 0 && sourced.remote?.busy !== true ? text("options.noMatch") : ""
1013
+ children: said !== "" ? said : open && matches.length === 0 && sourced.remote?.busy !== true ? text("options.noMatch") : ""
999
1014
  })]
1000
1015
  });
1001
1016
  }
@@ -1031,6 +1046,17 @@ function SelectField({ path, label }) {
1031
1046
  path
1032
1047
  }, "", field.def.widget !== "typeahead");
1033
1048
  const options = sourced.options;
1049
+ const remote = sourced.remote;
1050
+ const said = remote === null ? "" : optionsStatus(text, {
1051
+ control: "select",
1052
+ open: true,
1053
+ disabled: field.disabled,
1054
+ failed: remote.failed,
1055
+ busy: remote.busy,
1056
+ needs: remote.needs,
1057
+ query: "",
1058
+ completeness: remote.completeness
1059
+ });
1034
1060
  if (sourced.remote?.unavailable === true) return /* @__PURE__ */ jsx(FieldShell, {
1035
1061
  path,
1036
1062
  field,
@@ -1053,15 +1079,18 @@ function SelectField({ path, label }) {
1053
1079
  ...field.controlProps,
1054
1080
  value: typeof field.value === "string" ? field.value : "",
1055
1081
  onChange: (event) => field.setValue(event.target.value === "" ? null : event.target.value),
1082
+ onFocus: () => {
1083
+ if (remote?.failed === true) remote.retry();
1084
+ },
1056
1085
  onBlur: () => field.touch(),
1057
1086
  children: [/* @__PURE__ */ jsx("option", { value: "" }), options.map((option) => /* @__PURE__ */ jsx("option", {
1058
1087
  value: option.value,
1059
1088
  children: option.label
1060
1089
  }, option.value))]
1061
- }), sourced.remote === null ? null : /* @__PURE__ */ jsx("p", {
1090
+ }), remote === null ? null : /* @__PURE__ */ jsx("p", {
1062
1091
  role: "status",
1063
1092
  "data-formancy-part": "select-status",
1064
- children: sourced.remote.status
1093
+ children: said
1065
1094
  })]
1066
1095
  });
1067
1096
  }
@@ -1070,6 +1099,7 @@ function RadioGroupField({ path, label }) {
1070
1099
  const options = useResolvedOptions(field);
1071
1100
  const showError = field.touched && field.errors.length > 0;
1072
1101
  return /* @__PURE__ */ jsxs("fieldset", {
1102
+ id: field.ids.control,
1073
1103
  "data-formancy-part": "field",
1074
1104
  "data-formancy-field-path": path,
1075
1105
  "data-state": showError ? "invalid" : "valid",
@@ -1700,7 +1730,7 @@ function TagPickerField({ path, label }) {
1700
1730
  const listboxId = `${field.ids.control}:listbox`;
1701
1731
  const remaining = options.filter((option) => !chosen.includes(option.value));
1702
1732
  const matches = narrowOptionsByLabel(remaining, query);
1703
- const expanded = open && matches.length > 0;
1733
+ const expanded = open && !field.disabled && matches.length > 0;
1704
1734
  const add = (value) => {
1705
1735
  const next = options.map((option) => option.value).filter((candidate) => candidate === value || chosen.includes(candidate));
1706
1736
  field.setValue(next);
@@ -1726,6 +1756,7 @@ function TagPickerField({ path, label }) {
1726
1756
  type: "button",
1727
1757
  "data-formancy-part": "tagpicker-remove",
1728
1758
  "aria-label": text("tagpicker.remove", { option: labelFor(value) }),
1759
+ disabled: field.disabled,
1729
1760
  onClick: () => remove(value),
1730
1761
  children: "×"
1731
1762
  })]
@@ -1792,6 +1823,7 @@ function SelectBoxesField({ path, label }) {
1792
1823
  field.setValue(next);
1793
1824
  };
1794
1825
  return /* @__PURE__ */ jsxs("fieldset", {
1826
+ id: field.ids.control,
1795
1827
  "data-formancy-part": "field",
1796
1828
  "data-formancy-field-path": path,
1797
1829
  "data-state": showError ? "invalid" : "valid",
@@ -2179,6 +2211,7 @@ function NumberFallback({ path, label }) {
2179
2211
  children: /* @__PURE__ */ jsx("input", {
2180
2212
  type: "number",
2181
2213
  ...field.controlProps,
2214
+ readOnly: field.readOnly,
2182
2215
  value: typeof field.value === "number" ? field.value : "",
2183
2216
  onChange: (event) => field.setValue(event.target.value === "" ? null : event.target.valueAsNumber),
2184
2217
  onBlur: () => field.touch()
@@ -2307,6 +2340,7 @@ function SignatureField({ path, label }) {
2307
2340
  }, at))
2308
2341
  }),
2309
2342
  /* @__PURE__ */ jsx("input", {
2343
+ id: field.ids.control,
2310
2344
  "data-formancy-part": "signature-typed",
2311
2345
  type: "text",
2312
2346
  value: answer.typed ?? "",
@@ -2372,6 +2406,7 @@ function DateField({ path, label }) {
2372
2406
  children: /* @__PURE__ */ jsx("input", {
2373
2407
  type: "date",
2374
2408
  ...field.controlProps,
2409
+ readOnly: field.readOnly,
2375
2410
  value: typeof field.value === "string" ? field.value : "",
2376
2411
  onChange: (event) => field.setValue(event.target.value === "" ? null : event.target.value),
2377
2412
  onBlur: () => field.touch()
@@ -2400,6 +2435,7 @@ function TimeField({ path, label }) {
2400
2435
  children: /* @__PURE__ */ jsx("input", {
2401
2436
  type: "time",
2402
2437
  ...field.controlProps,
2438
+ readOnly: field.readOnly,
2403
2439
  ...typeof field.def.earliest === "string" ? { min: field.def.earliest } : {},
2404
2440
  ...typeof field.def.latest === "string" ? { max: field.def.latest } : {},
2405
2441
  value: typeof field.value === "string" ? field.value : "",
@@ -2446,6 +2482,7 @@ function DateTimeField({ path, label }) {
2446
2482
  children: /* @__PURE__ */ jsx("input", {
2447
2483
  type: "datetime-local",
2448
2484
  ...field.controlProps,
2485
+ readOnly: field.readOnly,
2449
2486
  value: asLocal(),
2450
2487
  onChange: (event) => {
2451
2488
  const local = event.target.value;
@@ -2529,6 +2566,7 @@ function TextField({ path, label }) {
2529
2566
  children: [/* @__PURE__ */ jsx("input", {
2530
2567
  type: "text",
2531
2568
  ...field.controlProps,
2569
+ readOnly: field.readOnly,
2532
2570
  value: shown,
2533
2571
  placeholder: mask === void 0 ? void 0 : maskPlaceholder(mask),
2534
2572
  inputMode: mask !== void 0 && maskIsNumeric(mask) ? "numeric" : void 0,
@@ -2579,6 +2617,7 @@ function TextareaField({ path, label }) {
2579
2617
  label,
2580
2618
  children: /* @__PURE__ */ jsx("textarea", {
2581
2619
  ...field.controlProps,
2620
+ readOnly: field.readOnly,
2582
2621
  value: typeof field.value === "string" ? field.value : "",
2583
2622
  onChange: (event) => field.setValue(event.target.value),
2584
2623
  onBlur: () => field.touch()
@@ -2772,6 +2811,7 @@ function RepeaterSection({ wire, labels, registry }) {
2772
2811
  const engine = useFormEngine();
2773
2812
  const text = useFormText();
2774
2813
  const repeater = useRepeater(wire);
2814
+ const field = useField(wire);
2775
2815
  const def = engine.repeaters().find((candidate) => candidate.wire === wire)?.def;
2776
2816
  const label = engine.text(def?.label) ?? labels?.[wire] ?? wire;
2777
2817
  const addLabel = def?.addLabel ?? text("repeater.add", { label });
@@ -2823,9 +2863,10 @@ function RepeaterSection({ wire, labels, registry }) {
2823
2863
  * the consumer ([0004](../../../docs/decisions/0004-headless-core.md)), and a renderer
2824
2864
  * that shipped a glyph would be choosing one for every design system at once. */
2825
2865
  const rowButtons = (index) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
2826
- /* @__PURE__ */ jsx("button", {
2866
+ repeater.canRemove ? /* @__PURE__ */ jsx("button", {
2827
2867
  type: "button",
2828
2868
  "data-formancy-part": "row-remove",
2869
+ disabled: field.disabled,
2829
2870
  onClick: () => repeater.removeRow(index),
2830
2871
  children: /* @__PURE__ */ jsx("span", {
2831
2872
  "data-formancy-part": "row-action-text",
@@ -2835,10 +2876,11 @@ function RepeaterSection({ wire, labels, registry }) {
2835
2876
  count: repeater.rowCount
2836
2877
  })
2837
2878
  })
2838
- }),
2879
+ }) : null,
2839
2880
  index > 0 ? /* @__PURE__ */ jsx("button", {
2840
2881
  type: "button",
2841
2882
  "data-formancy-part": "row-up",
2883
+ disabled: field.disabled,
2842
2884
  onClick: () => repeater.moveRow(index, index - 1),
2843
2885
  children: /* @__PURE__ */ jsx("span", {
2844
2886
  "data-formancy-part": "row-action-text",
@@ -2852,6 +2894,7 @@ function RepeaterSection({ wire, labels, registry }) {
2852
2894
  index < repeater.rowCount - 1 ? /* @__PURE__ */ jsx("button", {
2853
2895
  type: "button",
2854
2896
  "data-formancy-part": "row-down",
2897
+ disabled: field.disabled,
2855
2898
  onClick: () => repeater.moveRow(index, index + 1),
2856
2899
  children: /* @__PURE__ */ jsx("span", {
2857
2900
  "data-formancy-part": "row-action-text",
@@ -2892,14 +2935,20 @@ function RepeaterSection({ wire, labels, registry }) {
2892
2935
  "data-formancy-part": "row",
2893
2936
  children: [engine.fieldPaths().filter((candidate) => candidate.startsWith(`${wire}[${String(index)}]`)).map(slot), rowButtons(index)]
2894
2937
  }, rowId));
2938
+ if (!field.visible) return null;
2939
+ const showError = field.touched && field.errors.length > 0;
2895
2940
  return /* @__PURE__ */ jsxs("fieldset", {
2896
2941
  "data-formancy-part": "repeater",
2897
2942
  "data-formancy-field-path": wire,
2943
+ "data-state": showError ? "invalid" : "valid",
2944
+ id: field.controlProps.id,
2945
+ "aria-describedby": field.controlProps["aria-describedby"],
2898
2946
  children: [
2899
2947
  /* @__PURE__ */ jsx("legend", {
2900
2948
  "data-formancy-part": "repeater-legend",
2901
2949
  children: label
2902
2950
  }),
2951
+ /* @__PURE__ */ jsx(RequiredHint, { field }),
2903
2952
  grid ? /* @__PURE__ */ jsxs("div", {
2904
2953
  "data-formancy-part": "datagrid",
2905
2954
  "data-columns": String(plan.length),
@@ -2913,11 +2962,17 @@ function RepeaterSection({ wire, labels, registry }) {
2913
2962
  }, entry.key))
2914
2963
  }) : null, rows]
2915
2964
  }) : rows,
2916
- /* @__PURE__ */ jsx("button", {
2965
+ showError ? /* @__PURE__ */ jsx("p", {
2966
+ "data-formancy-part": "error",
2967
+ ...field.errorProps,
2968
+ children: field.errors.join(", ")
2969
+ }) : null,
2970
+ repeater.canAdd ? /* @__PURE__ */ jsx("button", {
2917
2971
  type: "button",
2972
+ disabled: field.disabled,
2918
2973
  onClick: () => repeater.addRow(),
2919
2974
  children: addLabel
2920
- })
2975
+ }) : null
2921
2976
  ]
2922
2977
  });
2923
2978
  }