@formancy/react 0.8.0 → 0.10.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
@@ -109,6 +109,16 @@ export declare function useSubmit(): () => {
109
109
  errors: Record<string, string[]>;
110
110
  };
111
111
  //#endregion
112
+ //#region src/go-to-field.d.ts
113
+ /**
114
+ * Take the person to a question: to its page first, on a paged form, and to its control once
115
+ * that page is drawn — into a group's first option, since a fieldset takes no focus, and past a
116
+ * tab a layout hides it behind (0213). What the error summary's links and the review's Change do,
117
+ * one function for both and for a host's own list of questions; the Angular binding's
118
+ * `injectGoToField` is the same. A path no field has throws, as the engine does for one.
119
+ */
120
+ export declare function useGoToField(): (path: string) => void;
121
+ //#endregion
112
122
  //#region src/fields/internals.d.ts
113
123
  /**
114
124
  * The pieces every control shares: the shell it renders inside, the prop shapes, and the few helpers more than one of them needs.
@@ -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;;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;;;;;;;;;;;;;;;;;UCWzC;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;;;;;EAKA;EACA,YAAY,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;wBCPP,aAAa,OAAO,oCAAiB,IAAA;;;UCpDpC;EACf,SAAS;;;;;;;;;;;wBAYK,eAAe,UAAU,oCAAiB,IAAA;;;;;;;;;;;;;;;;;;;;wBCG1C,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;;;;;;;;;;;UC/Bb;;;;;EAKf;;EAEA,QAAQ;;EAER,WAAW,cAAc;;;;;;;;;;;;KAaf,YAAY,MAAM,MAAM,SAAS,kBAAkB,QAAQ;qBAI1D,kCAAgB,SAAA;;;;;;;;;wBAUb,eAAe;;;;;;;;;;;;;;;;;;;;;UChCd;;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/go-to-field.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;;;;;;;;;;;wBCD1C,iBAAiB;;;;;;;;;;;;;;;;UCYhB;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;;;;;EAKA;EACA,YAAY,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;wBCNP,aAAa,OAAO,oCAAiB,IAAA;;;UCrDpC;EACf,SAAS;;;;;;;;;;;wBAYK,eAAe,UAAU,oCAAiB,IAAA;;;;;;;;;;;;;;;;;;;;wBCG1C,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;;;;;;;;;;;UC/Bb;;;;;EAKf;;EAEA,QAAQ;;EAER,WAAW,cAAc;;;;;;;;;;;;KAaf,YAAY,MAAM,MAAM,SAAS,kBAAkB,QAAQ;qBAI1D,kCAAgB,SAAA;;;;;;;;;wBAUb,eAAe;;;;;;;;;;;;;;;;;;;;;UChCd;;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,6 +1,6 @@
1
1
  import { Fragment, createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
- import { cancelUploads, fieldUploads, formatPath, inOptionOrder, parsePath, placedGroup, placedPage, thumbnailSize, ticked } from "@formancy/core";
3
- import { answersToReview, countSentence, createFormText, errorSentence, optionsStatus } from "@formancy/core/words";
2
+ import { cancelUploads, fieldUploads, formatPath, hasReadOnlyControl, inOptionOrder, parsePath, placedGroup, placedPage, thumbnailSize, ticked } from "@formancy/core";
3
+ import { answerInWords, answersToReview, countSentence, createFormText, errorSentence, optionsStatus } from "@formancy/core/words";
4
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";
@@ -143,6 +143,24 @@ function useSubmit() {
143
143
  }, [engine]);
144
144
  }
145
145
  //#endregion
146
+ //#region src/go-to-field.ts
147
+ function useGoToField() {
148
+ const engine = useFormEngine();
149
+ const [focusing, setFocusing] = useState(void 0);
150
+ useEffect(() => {
151
+ if (focusing === void 0) return;
152
+ focusControl(document.getElementById(focusing.id));
153
+ setFocusing(void 0);
154
+ }, [focusing]);
155
+ return useCallback((path) => {
156
+ const at = parsePath(path);
157
+ const wizard = engine.wizard();
158
+ const page = engine.pageOf(at);
159
+ if (wizard !== void 0 && wizard.page() !== page) wizard.goTo(page);
160
+ setFocusing({ id: engine.getFieldSnapshot(at).ids.control });
161
+ }, [engine]);
162
+ }
163
+ //#endregion
146
164
  //#region src/layout.tsx
147
165
  function LayoutTree({ schema, nodes, locale, renderField, at = [], shows }) {
148
166
  return /* @__PURE__ */ jsx(Fragment$1, { children: nodes.map((node, index) => {
@@ -396,12 +414,7 @@ function useReview(goTo) {
396
414
  const engine = useFormEngine();
397
415
  const submit = useSubmit();
398
416
  const [reviewing, setReviewing] = useState(false);
399
- const [focusing, setFocusing] = useState(void 0);
400
- useEffect(() => {
401
- if (focusing === void 0) return;
402
- focusControl(document.getElementById(focusing));
403
- setFocusing(void 0);
404
- }, [focusing]);
417
+ const goToField = useGoToField();
405
418
  return {
406
419
  reviewing,
407
420
  check() {
@@ -411,8 +424,7 @@ function useReview(goTo) {
411
424
  },
412
425
  change(answer) {
413
426
  setReviewing(false);
414
- goTo?.(answer.page);
415
- setFocusing(answer.controlId);
427
+ goToField(answer.field);
416
428
  }
417
429
  };
418
430
  }
@@ -452,7 +464,10 @@ function Review({ onChange, children }) {
452
464
  "data-formancy-part": "review-answer",
453
465
  children: [
454
466
  /* @__PURE__ */ jsx("dt", { children: answer.label }),
455
- /* @__PURE__ */ jsx("dd", { children: answer.said }),
467
+ /* @__PURE__ */ jsx("dd", {
468
+ ...answer.answered ? {} : { "data-formancy-part": "review-unanswered" },
469
+ children: answer.said
470
+ }),
456
471
  /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsxs("button", {
457
472
  type: "button",
458
473
  onClick: () => onChange(answer),
@@ -544,7 +559,8 @@ function useSourcedOptions(field, query, enabled = true) {
544
559
  const debounceMs = source?.debounceMs ?? DEBOUNCE_MS;
545
560
  const minQueryLength = source?.minQueryLength ?? MIN_QUERY;
546
561
  const maxRows = source?.maxRows ?? MAX_ROWS;
547
- const stored = typeof field.value === "string" && field.value !== "" ? field.value : void 0;
562
+ const stored = storedValues(field.value);
563
+ const storedKey = stored.join("\0");
548
564
  const path = field.path;
549
565
  const disabled = field.disabled;
550
566
  const asked = useRef(0);
@@ -616,18 +632,19 @@ function useSourcedOptions(field, query, enabled = true) {
616
632
  ]);
617
633
  const askedFor = useRef(/* @__PURE__ */ new Set());
618
634
  useEffect(() => {
619
- if (!enabled || source === void 0 || stored === void 0) return;
620
- if (named.has(stored) || askedFor.current.has(stored)) return;
621
- askedFor.current.add(stored);
635
+ if (!enabled || source === void 0 || storedKey === "") return;
636
+ const unnamed = storedKey.split("\0").filter((value) => !named.has(value) && !askedFor.current.has(value));
637
+ if (unnamed.length === 0) return;
638
+ for (const value of unnamed) askedFor.current.add(value);
622
639
  const controller = new AbortController();
623
640
  source.resolve({
624
641
  kind: "labels",
625
642
  source: name ?? "",
626
643
  path,
627
644
  query: "",
628
- values: [stored],
645
+ values: unnamed,
629
646
  locale: engine.locale(),
630
- limit: 1,
647
+ limit: unnamed.length,
631
648
  signal: controller.signal
632
649
  }).then((answer) => {
633
650
  const accepted = acceptRemoteAnswer(answer);
@@ -644,7 +661,7 @@ function useSourcedOptions(field, query, enabled = true) {
644
661
  source,
645
662
  name,
646
663
  path,
647
- stored,
664
+ storedKey,
648
665
  named,
649
666
  engine
650
667
  ]);
@@ -655,13 +672,11 @@ function useSourcedOptions(field, query, enabled = true) {
655
672
  })),
656
673
  remote: null
657
674
  };
658
- const offered = [...rows];
659
- if (stored !== void 0 && !offered.some((row) => row.value === stored)) offered.unshift({
660
- value: stored,
661
- label: named.get(stored) ?? stored
662
- });
663
675
  return {
664
- options: offered,
676
+ options: [...stored.filter((value) => !rows.some((row) => row.value === value)).map((value) => ({
677
+ value,
678
+ label: named.get(value) ?? value
679
+ })), ...rows],
665
680
  remote: {
666
681
  unavailable: source === void 0,
667
682
  busy,
@@ -672,6 +687,11 @@ function useSourcedOptions(field, query, enabled = true) {
672
687
  }
673
688
  };
674
689
  }
690
+ function storedValues(value) {
691
+ if (typeof value === "string") return value === "" ? [] : [value];
692
+ if (!Array.isArray(value)) return [];
693
+ return value.filter((item) => typeof item === "string" && item !== "");
694
+ }
675
695
  //#endregion
676
696
  //#region src/uploads.ts
677
697
  const UploaderContext = createContext(void 0);
@@ -1008,6 +1028,7 @@ function RadioGroupField({ path, label }) {
1008
1028
  name: field.controlProps.name,
1009
1029
  value: option.value,
1010
1030
  checked: field.value === option.value,
1031
+ disabled: field.disabled,
1011
1032
  onChange: () => field.setValue(option.value),
1012
1033
  onBlur: () => field.touch()
1013
1034
  }), /* @__PURE__ */ jsxs("label", {
@@ -1021,6 +1042,24 @@ function RadioGroupField({ path, label }) {
1021
1042
  });
1022
1043
  }
1023
1044
  //#endregion
1045
+ //#region src/fields/computed-answer.tsx
1046
+ function ComputedAnswerField({ path, label }) {
1047
+ const field = useField(path);
1048
+ const engine = useFormEngine();
1049
+ const text = useFormText();
1050
+ return /* @__PURE__ */ jsx(FieldShell, {
1051
+ path,
1052
+ field,
1053
+ label,
1054
+ children: /* @__PURE__ */ jsx("input", {
1055
+ type: "text",
1056
+ readOnly: true,
1057
+ ...field.controlProps,
1058
+ value: answerInWords(engine.schema(), field.def, field.value, text, engine.locale())
1059
+ })
1060
+ });
1061
+ }
1062
+ //#endregion
1024
1063
  //#region src/fields/file-field.tsx
1025
1064
  function FileField({ path, label }) {
1026
1065
  const field = useField(path);
@@ -1461,14 +1500,30 @@ function SelectBoxesSwitch(props) {
1461
1500
  function TagPickerField({ path, label }) {
1462
1501
  const field = useField(path);
1463
1502
  const text = useFormText();
1464
- const options = useResolvedOptions(field);
1465
1503
  const chosen = Array.isArray(field.value) ? field.value.map(String) : [];
1466
1504
  const [query, setQuery] = useState("");
1467
1505
  const [open, setOpen] = useState(false);
1468
1506
  const listboxId = `${field.ids.control}:listbox`;
1507
+ const written = useResolvedOptions(field);
1508
+ const sourced = useSourcedOptions({
1509
+ ...field,
1510
+ path
1511
+ }, query);
1512
+ const remote = sourced.remote;
1513
+ const options = remote === null ? written : sourced.options;
1469
1514
  const remaining = options.filter((option) => !chosen.includes(option.value));
1470
- const matches = narrowOptionsByLabel(remaining, query);
1515
+ const matches = remote === null ? narrowOptionsByLabel(remaining, query) : remaining;
1471
1516
  const expanded = open && !field.disabled && matches.length > 0;
1517
+ const said = remote === null ? "" : optionsStatus(text, {
1518
+ control: "typeahead",
1519
+ open: open && !field.disabled,
1520
+ disabled: field.disabled,
1521
+ failed: remote.failed,
1522
+ busy: remote.busy,
1523
+ needs: remote.needs,
1524
+ query,
1525
+ completeness: remote.completeness
1526
+ });
1472
1527
  const add = (value) => {
1473
1528
  field.setValue(ticked(field.def, chosen, value, true));
1474
1529
  setQuery("");
@@ -1478,92 +1533,139 @@ function TagPickerField({ path, label }) {
1478
1533
  field.setValue(chosen.filter((candidate) => candidate !== value));
1479
1534
  };
1480
1535
  const labelFor = (value) => options.find((option) => option.value === value)?.label ?? value;
1536
+ if (remote?.unavailable === true) return /* @__PURE__ */ jsx(FieldShell, {
1537
+ path,
1538
+ field,
1539
+ label,
1540
+ children: /* @__PURE__ */ jsx("p", {
1541
+ "data-formancy-part": "options-unavailable",
1542
+ children: text("options.unavailable", { source: field.def.optionsSource ?? "" })
1543
+ })
1544
+ });
1481
1545
  return /* @__PURE__ */ jsx(FieldShell, {
1482
1546
  path,
1483
1547
  field,
1484
1548
  label,
1485
1549
  children: /* @__PURE__ */ jsxs("div", {
1486
1550
  "data-formancy-part": "tagpicker",
1487
- children: [chosen.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
1488
- "data-formancy-part": "tagpicker-chips",
1489
- "aria-label": text("tagpicker.chosen", { label }),
1490
- children: chosen.map((value) => /* @__PURE__ */ jsxs("li", {
1491
- "data-formancy-part": "tagpicker-chip",
1492
- children: [labelFor(value), /* @__PURE__ */ jsx("button", {
1493
- type: "button",
1494
- "data-formancy-part": "tagpicker-remove",
1495
- "aria-label": text("tagpicker.remove", { option: labelFor(value) }),
1496
- disabled: field.disabled,
1497
- onClick: () => remove(value),
1498
- children: "×"
1499
- })]
1500
- }, value))
1501
- }), /* @__PURE__ */ jsxs("div", {
1502
- "data-formancy-part": "tagpicker-anchor",
1503
- children: [/* @__PURE__ */ jsx("input", {
1504
- type: "text",
1505
- role: "combobox",
1506
- ...field.controlProps,
1507
- "data-formancy-part": "tagpicker-input",
1508
- autoComplete: "off",
1509
- "aria-expanded": expanded,
1510
- "aria-controls": listboxId,
1511
- "aria-autocomplete": "list",
1512
- value: query,
1513
- onChange: (event) => {
1514
- setQuery(event.target.value);
1515
- setOpen(true);
1516
- },
1517
- onClick: () => setOpen(true),
1518
- onKeyDown: (event) => {
1519
- if (event.key === "Escape") {
1551
+ children: [
1552
+ chosen.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
1553
+ "data-formancy-part": "tagpicker-chips",
1554
+ "aria-label": text("tagpicker.chosen", { label }),
1555
+ children: chosen.map((value) => /* @__PURE__ */ jsxs("li", {
1556
+ "data-formancy-part": "tagpicker-chip",
1557
+ children: [labelFor(value), /* @__PURE__ */ jsx("button", {
1558
+ type: "button",
1559
+ "data-formancy-part": "tagpicker-remove",
1560
+ "aria-label": text("tagpicker.remove", { option: labelFor(value) }),
1561
+ disabled: field.disabled,
1562
+ onClick: () => remove(value),
1563
+ children: "×"
1564
+ })]
1565
+ }, value))
1566
+ }),
1567
+ /* @__PURE__ */ jsxs("div", {
1568
+ "data-formancy-part": "tagpicker-anchor",
1569
+ children: [/* @__PURE__ */ jsx("input", {
1570
+ type: "text",
1571
+ role: "combobox",
1572
+ ...field.controlProps,
1573
+ "data-formancy-part": "tagpicker-input",
1574
+ autoComplete: "off",
1575
+ "aria-expanded": expanded,
1576
+ "aria-controls": listboxId,
1577
+ "aria-autocomplete": "list",
1578
+ "aria-busy": remote?.busy === true ? true : void 0,
1579
+ value: query,
1580
+ onChange: (event) => {
1581
+ setQuery(event.target.value);
1582
+ setOpen(true);
1583
+ },
1584
+ onClick: () => setOpen(true),
1585
+ onKeyDown: (event) => {
1586
+ if (event.key === "Escape") {
1587
+ setOpen(false);
1588
+ return;
1589
+ }
1590
+ if (event.key === "Enter" && expanded && matches[0] !== void 0) {
1591
+ event.preventDefault();
1592
+ add(matches[0].value);
1593
+ return;
1594
+ }
1595
+ if (event.key === "Backspace" && query === "" && chosen.length > 0) remove(chosen[chosen.length - 1]);
1596
+ },
1597
+ onBlur: () => {
1520
1598
  setOpen(false);
1521
- return;
1522
- }
1523
- if (event.key === "Enter" && expanded && matches[0] !== void 0) {
1524
- event.preventDefault();
1525
- add(matches[0].value);
1526
- return;
1599
+ field.touch();
1527
1600
  }
1528
- if (event.key === "Backspace" && query === "" && chosen.length > 0) remove(chosen[chosen.length - 1]);
1529
- },
1530
- onBlur: () => {
1531
- setOpen(false);
1532
- field.touch();
1533
- }
1534
- }), /* @__PURE__ */ jsx("ul", {
1535
- id: listboxId,
1536
- role: "listbox",
1537
- "aria-label": text("options.suggestions", { label }),
1538
- "data-formancy-part": "tagpicker-listbox",
1539
- hidden: !expanded,
1540
- children: matches.map((option) => /* @__PURE__ */ jsx("li", {
1541
- role: "option",
1542
- "data-formancy-part": "tagpicker-option",
1543
- "aria-selected": false,
1544
- onMouseDown: (event) => event.preventDefault(),
1545
- onClick: () => add(option.value),
1546
- children: option.label
1547
- }, option.value))
1548
- })]
1549
- })]
1601
+ }), /* @__PURE__ */ jsx("ul", {
1602
+ id: listboxId,
1603
+ role: "listbox",
1604
+ "aria-label": text("options.suggestions", { label }),
1605
+ "data-formancy-part": "tagpicker-listbox",
1606
+ hidden: !expanded,
1607
+ children: matches.map((option) => /* @__PURE__ */ jsx("li", {
1608
+ role: "option",
1609
+ "data-formancy-part": "tagpicker-option",
1610
+ "aria-selected": false,
1611
+ onMouseDown: (event) => event.preventDefault(),
1612
+ onClick: () => add(option.value),
1613
+ children: option.label
1614
+ }, option.value))
1615
+ })]
1616
+ }),
1617
+ remote === null ? null : /* @__PURE__ */ jsx("p", {
1618
+ role: "status",
1619
+ "data-formancy-part": "tagpicker-status",
1620
+ "data-state": remote.failed ? "failed" : void 0,
1621
+ children: said
1622
+ })
1623
+ ]
1550
1624
  })
1551
1625
  });
1552
1626
  }
1553
1627
  function SelectBoxesField({ path, label }) {
1554
1628
  const field = useField(path);
1555
- const options = useResolvedOptions(field);
1629
+ const text = useFormText();
1630
+ const written = useResolvedOptions(field);
1631
+ const sourced = useSourcedOptions({
1632
+ ...field,
1633
+ path
1634
+ }, "");
1635
+ const remote = sourced.remote;
1636
+ const options = remote === null ? written : sourced.options;
1556
1637
  const showError = field.touched && field.errors.length > 0;
1557
1638
  const chosen = Array.isArray(field.value) ? field.value : [];
1639
+ const group = useRef(null);
1640
+ const again = useRef(null);
1641
+ const [retrying, setRetrying] = useState(false);
1642
+ const settled = remote !== null && !remote.busy && !remote.failed;
1643
+ useEffect(() => {
1644
+ if (!retrying || !settled) return;
1645
+ setRetrying(false);
1646
+ if (document.activeElement === again.current) group.current?.querySelector("input[type=\"checkbox\"]")?.focus();
1647
+ }, [retrying, settled]);
1648
+ const said = remote === null ? "" : optionsStatus(text, {
1649
+ control: "ticks",
1650
+ open: true,
1651
+ disabled: field.disabled,
1652
+ failed: remote.failed,
1653
+ busy: remote.busy,
1654
+ needs: remote.needs,
1655
+ query: "",
1656
+ completeness: remote.completeness
1657
+ });
1558
1658
  const toggle = (value, on) => {
1559
1659
  field.setValue(ticked(field.def, chosen, value, on));
1560
1660
  };
1561
1661
  return /* @__PURE__ */ jsxs("fieldset", {
1662
+ ref: group,
1562
1663
  id: field.ids.control,
1563
1664
  "data-formancy-part": "field",
1564
1665
  "data-formancy-field-path": path,
1565
1666
  "data-state": showError ? "invalid" : "valid",
1566
1667
  "aria-describedby": field.controlProps["aria-describedby"],
1668
+ "aria-busy": remote?.busy === true ? true : void 0,
1567
1669
  children: [
1568
1670
  /* @__PURE__ */ jsx("legend", {
1569
1671
  "data-formancy-part": "label",
@@ -1571,6 +1673,10 @@ function SelectBoxesField({ path, label }) {
1571
1673
  }),
1572
1674
  /* @__PURE__ */ jsx(RequiredHint, { field }),
1573
1675
  /* @__PURE__ */ jsx(Description, { field }),
1676
+ remote?.unavailable === true ? /* @__PURE__ */ jsx("p", {
1677
+ "data-formancy-part": "options-unavailable",
1678
+ children: text("options.unavailable", { source: field.def.optionsSource ?? "" })
1679
+ }) : null,
1574
1680
  options.map((option) => {
1575
1681
  const optionId = `${field.ids.control}:${option.value}`;
1576
1682
  return /* @__PURE__ */ jsxs("span", {
@@ -1590,6 +1696,22 @@ function SelectBoxesField({ path, label }) {
1590
1696
  })]
1591
1697
  }, option.value);
1592
1698
  }),
1699
+ remote === null || remote.unavailable ? null : /* @__PURE__ */ jsx("p", {
1700
+ role: "status",
1701
+ "data-formancy-part": "checkbox-status",
1702
+ "data-state": remote.failed ? "failed" : void 0,
1703
+ children: said
1704
+ }),
1705
+ remote !== null && (remote.failed || retrying) && !field.disabled ? /* @__PURE__ */ jsx("button", {
1706
+ ref: again,
1707
+ type: "button",
1708
+ "data-formancy-part": "checkbox-retry",
1709
+ onClick: () => {
1710
+ setRetrying(true);
1711
+ remote.retry();
1712
+ },
1713
+ children: text("options.retry")
1714
+ }) : null,
1593
1715
  showError ? /* @__PURE__ */ jsx(ErrorText, { field }) : null
1594
1716
  ]
1595
1717
  });
@@ -1817,6 +1939,7 @@ function RatingField({ path, label }) {
1817
1939
  name: controlProps.id,
1818
1940
  value,
1819
1941
  checked: value === chosen,
1942
+ disabled: field.disabled,
1820
1943
  onChange: () => field.setValue(value),
1821
1944
  onBlur: () => field.touch()
1822
1945
  }), /* @__PURE__ */ jsx("span", {
@@ -1915,12 +2038,13 @@ function SignatureField({ path, label }) {
1915
2038
  };
1916
2039
  }, []);
1917
2040
  const onPointerDown = (event) => {
2041
+ if (field.disabled) return;
1918
2042
  before.current = drawn;
1919
2043
  stroke.current = [pointFrom(event)];
1920
2044
  surface.current?.setPointerCapture?.(event.pointerId);
1921
2045
  };
1922
2046
  const onPointerMove = (event) => {
1923
- if (stroke.current === null) return;
2047
+ if (stroke.current === null || field.disabled) return;
1924
2048
  stroke.current = [...stroke.current, pointFrom(event)];
1925
2049
  commit([...before.current, stroke.current]);
1926
2050
  };
@@ -1944,6 +2068,7 @@ function SignatureField({ path, label }) {
1944
2068
  viewBox: `0 0 ${String(width)} ${String(height)}`,
1945
2069
  role: "img",
1946
2070
  "aria-label": label,
2071
+ "aria-disabled": field.disabled ? true : void 0,
1947
2072
  onPointerDown,
1948
2073
  onPointerMove,
1949
2074
  onPointerUp,
@@ -1955,11 +2080,11 @@ function SignatureField({ path, label }) {
1955
2080
  }, at))
1956
2081
  }),
1957
2082
  /* @__PURE__ */ jsx("input", {
1958
- id: field.ids.control,
2083
+ ...field.controlProps,
1959
2084
  "data-formancy-part": "signature-typed",
1960
2085
  type: "text",
1961
2086
  value: answer.typed ?? "",
1962
- "aria-label": text("signature.typed"),
2087
+ "aria-label": text("signature.typed", { label }),
1963
2088
  onChange: (event) => {
1964
2089
  const typed = event.target.value;
1965
2090
  field.setValue(typed === "" ? null : { typed });
@@ -1969,6 +2094,7 @@ function SignatureField({ path, label }) {
1969
2094
  /* @__PURE__ */ jsx("button", {
1970
2095
  "data-formancy-part": "signature-clear",
1971
2096
  type: "button",
2097
+ disabled: field.disabled,
1972
2098
  onClick: () => {
1973
2099
  stroke.current = null;
1974
2100
  before.current = [];
@@ -2307,7 +2433,7 @@ function FieldList({ labels, registry, page, layout }) {
2307
2433
  function FieldSlot({ path, fallbackLabel, registry }) {
2308
2434
  const field = useField(path);
2309
2435
  if (!field.visible) return null;
2310
- const Component = registry?.byPath?.[path] ?? registry?.byType?.[field.type] ?? DEFAULT_COMPONENTS[field.type];
2436
+ const Component = registry?.byPath?.[path] ?? (field.readOnly && !hasReadOnlyControl(field.def) ? ComputedAnswerField : void 0) ?? registry?.byType?.[field.type] ?? DEFAULT_COMPONENTS[field.type];
2311
2437
  if (Component === null) return null;
2312
2438
  const label = field.label ?? fallbackLabel ?? path;
2313
2439
  return /* @__PURE__ */ jsx(Component, {
@@ -2508,12 +2634,7 @@ function ErrorSummary({ labels }) {
2508
2634
  const errors = useSyncExternalStore(subscribe, getErrors, getErrors);
2509
2635
  const region = useRef(null);
2510
2636
  const previousCount = useRef(0);
2511
- const [focusing, setFocusing] = useState(void 0);
2512
- useEffect(() => {
2513
- if (focusing === void 0) return;
2514
- focusControl(document.getElementById(focusing));
2515
- setFocusing(void 0);
2516
- }, [focusing]);
2637
+ const goToField = useGoToField();
2517
2638
  useEffect(() => {
2518
2639
  if (errors.length > 0 && previousCount.current === 0) region.current?.focus();
2519
2640
  previousCount.current = errors.length;
@@ -2534,10 +2655,7 @@ function ErrorSummary({ labels }) {
2534
2655
  href: `#${controlId}`,
2535
2656
  onClick: (event) => {
2536
2657
  event.preventDefault();
2537
- const wizard = engine.wizard();
2538
- const page = engine.pageOf(parsePath(path));
2539
- if (wizard !== void 0 && wizard.page() !== page) wizard.goTo(page);
2540
- setFocusing(controlId);
2658
+ goToField(path);
2541
2659
  },
2542
2660
  children: text("errors.entry", {
2543
2661
  label: labelFor(path),
@@ -2581,6 +2699,6 @@ function ResumeNotice({ migration, labels }) {
2581
2699
  });
2582
2700
  }
2583
2701
  //#endregion
2584
- export { ErrorSummary, FormancyForm, FormancyProvider, OptionsSourcesProvider, ResumeNotice, RichText, RichTextEditorProvider, ScannerProvider, UploaderProvider, useField, useFormEngine, useFormText, useOptionsSources, useRepeater, useRichTextEditorFactory, useScanner, useSubmit, useUploader, useWizard };
2702
+ export { ErrorSummary, FormancyForm, FormancyProvider, OptionsSourcesProvider, ResumeNotice, RichText, RichTextEditorProvider, ScannerProvider, UploaderProvider, useField, useFormEngine, useFormText, useGoToField, useOptionsSources, useRepeater, useRichTextEditorFactory, useScanner, useSubmit, useUploader, useWizard };
2585
2703
 
2586
2704
  //# sourceMappingURL=index.mjs.map