@formancy/react 0.9.0 → 0.11.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.
@@ -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/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;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;wBCdP,aAAa,OAAO,oCAAiB,IAAA;;;UC7CpC;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";
@@ -122,7 +122,7 @@ function focusControl(control) {
122
122
  if (control === null) return;
123
123
  control.dispatchEvent(new Event("formancy-reveal", { bubbles: true }));
124
124
  control.focus();
125
- if (document.activeElement !== control) control.querySelector("input, select, textarea, button")?.focus();
125
+ if (document.activeElement !== control) control.querySelector("input, select, textarea, button, [data-formancy-part$=\"-unavailable\"]")?.focus();
126
126
  }
127
127
  //#endregion
128
128
  //#region src/use-submit.ts
@@ -384,6 +384,16 @@ function placedPaths(nodes, into = []) {
384
384
  return into;
385
385
  }
386
386
  //#endregion
387
+ //#region src/focus-row.ts
388
+ function focusRow(repeater, target, add) {
389
+ if (target === "add") return focusControl(add);
390
+ const row = repeater.querySelectorAll("[data-formancy-part=\"row\"], [data-formancy-part=\"datagrid-row\"]")[target.row];
391
+ if (row === void 0) return;
392
+ const part = target.parts.map((name) => row.querySelector(`[data-formancy-part="${name}"]`)).find((found) => found !== null);
393
+ const first = row.querySelector("input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), button:not(:disabled), [contenteditable=\"true\"], [tabindex=\"0\"]");
394
+ focusControl(part ?? first);
395
+ }
396
+ //#endregion
387
397
  //#region src/review.tsx
388
398
  function SubmitButton({ submitLabel, onSubmit, onFailedNavigate }) {
389
399
  const engine = useFormEngine();
@@ -464,7 +474,10 @@ function Review({ onChange, children }) {
464
474
  "data-formancy-part": "review-answer",
465
475
  children: [
466
476
  /* @__PURE__ */ jsx("dt", { children: answer.label }),
467
- /* @__PURE__ */ jsx("dd", { children: answer.said }),
477
+ /* @__PURE__ */ jsx("dd", {
478
+ ...answer.answered ? {} : { "data-formancy-part": "review-unanswered" },
479
+ children: answer.said
480
+ }),
468
481
  /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsxs("button", {
469
482
  type: "button",
470
483
  onClick: () => onChange(answer),
@@ -556,7 +569,8 @@ function useSourcedOptions(field, query, enabled = true) {
556
569
  const debounceMs = source?.debounceMs ?? DEBOUNCE_MS;
557
570
  const minQueryLength = source?.minQueryLength ?? MIN_QUERY;
558
571
  const maxRows = source?.maxRows ?? MAX_ROWS;
559
- const stored = typeof field.value === "string" && field.value !== "" ? field.value : void 0;
572
+ const stored = storedValues(field.value);
573
+ const storedKey = stored.join("\0");
560
574
  const path = field.path;
561
575
  const disabled = field.disabled;
562
576
  const asked = useRef(0);
@@ -628,18 +642,19 @@ function useSourcedOptions(field, query, enabled = true) {
628
642
  ]);
629
643
  const askedFor = useRef(/* @__PURE__ */ new Set());
630
644
  useEffect(() => {
631
- if (!enabled || source === void 0 || stored === void 0) return;
632
- if (named.has(stored) || askedFor.current.has(stored)) return;
633
- askedFor.current.add(stored);
645
+ if (!enabled || source === void 0 || storedKey === "") return;
646
+ const unnamed = storedKey.split("\0").filter((value) => !named.has(value) && !askedFor.current.has(value));
647
+ if (unnamed.length === 0) return;
648
+ for (const value of unnamed) askedFor.current.add(value);
634
649
  const controller = new AbortController();
635
650
  source.resolve({
636
651
  kind: "labels",
637
652
  source: name ?? "",
638
653
  path,
639
654
  query: "",
640
- values: [stored],
655
+ values: unnamed,
641
656
  locale: engine.locale(),
642
- limit: 1,
657
+ limit: unnamed.length,
643
658
  signal: controller.signal
644
659
  }).then((answer) => {
645
660
  const accepted = acceptRemoteAnswer(answer);
@@ -656,7 +671,7 @@ function useSourcedOptions(field, query, enabled = true) {
656
671
  source,
657
672
  name,
658
673
  path,
659
- stored,
674
+ storedKey,
660
675
  named,
661
676
  engine
662
677
  ]);
@@ -667,13 +682,11 @@ function useSourcedOptions(field, query, enabled = true) {
667
682
  })),
668
683
  remote: null
669
684
  };
670
- const offered = [...rows];
671
- if (stored !== void 0 && !offered.some((row) => row.value === stored)) offered.unshift({
672
- value: stored,
673
- label: named.get(stored) ?? stored
674
- });
675
685
  return {
676
- options: offered,
686
+ options: [...stored.filter((value) => !rows.some((row) => row.value === value)).map((value) => ({
687
+ value,
688
+ label: named.get(value) ?? value
689
+ })), ...rows],
677
690
  remote: {
678
691
  unavailable: source === void 0,
679
692
  busy,
@@ -684,6 +697,11 @@ function useSourcedOptions(field, query, enabled = true) {
684
697
  }
685
698
  };
686
699
  }
700
+ function storedValues(value) {
701
+ if (typeof value === "string") return value === "" ? [] : [value];
702
+ if (!Array.isArray(value)) return [];
703
+ return value.filter((item) => typeof item === "string" && item !== "");
704
+ }
687
705
  //#endregion
688
706
  //#region src/uploads.ts
689
707
  const UploaderContext = createContext(void 0);
@@ -773,6 +791,24 @@ function OptionPicture({ option }) {
773
791
  }), " "] });
774
792
  }
775
793
  //#endregion
794
+ //#region src/fields/computed-answer.tsx
795
+ function ComputedAnswerField({ path, label }) {
796
+ const field = useField(path);
797
+ const engine = useFormEngine();
798
+ const text = useFormText();
799
+ return /* @__PURE__ */ jsx(FieldShell, {
800
+ path,
801
+ field,
802
+ label,
803
+ children: /* @__PURE__ */ jsx("input", {
804
+ type: "text",
805
+ readOnly: true,
806
+ ...field.controlProps,
807
+ value: answerInWords(engine.schema(), field.def, field.value, text, engine.locale())
808
+ })
809
+ });
810
+ }
811
+ //#endregion
776
812
  //#region src/fields/typeahead-field.tsx
777
813
  function TypeaheadSelectField({ path, label, field }) {
778
814
  const text = useFormText();
@@ -961,6 +997,8 @@ function SelectField({ path, label }) {
961
997
  label,
962
998
  children: /* @__PURE__ */ jsx("p", {
963
999
  "data-formancy-part": "options-unavailable",
1000
+ id: field.controlProps.id,
1001
+ tabIndex: -1,
964
1002
  children: text("options.unavailable", { source: field.def.optionsSource ?? "" })
965
1003
  })
966
1004
  });
@@ -1057,6 +1095,8 @@ function FileField({ path, label }) {
1057
1095
  children: [
1058
1096
  upload === void 0 ? /* @__PURE__ */ jsx("p", {
1059
1097
  "data-formancy-part": "file-unavailable",
1098
+ id: field.controlProps.id,
1099
+ tabIndex: -1,
1060
1100
  children: text("file.unavailable")
1061
1101
  }) : /* @__PURE__ */ jsx("div", {
1062
1102
  "data-formancy-part": "file-dropzone",
@@ -1474,14 +1514,30 @@ function SelectBoxesSwitch(props) {
1474
1514
  function TagPickerField({ path, label }) {
1475
1515
  const field = useField(path);
1476
1516
  const text = useFormText();
1477
- const options = useResolvedOptions(field);
1478
1517
  const chosen = Array.isArray(field.value) ? field.value.map(String) : [];
1479
1518
  const [query, setQuery] = useState("");
1480
1519
  const [open, setOpen] = useState(false);
1481
1520
  const listboxId = `${field.ids.control}:listbox`;
1521
+ const written = useResolvedOptions(field);
1522
+ const sourced = useSourcedOptions({
1523
+ ...field,
1524
+ path
1525
+ }, query);
1526
+ const remote = sourced.remote;
1527
+ const options = remote === null ? written : sourced.options;
1482
1528
  const remaining = options.filter((option) => !chosen.includes(option.value));
1483
- const matches = narrowOptionsByLabel(remaining, query);
1529
+ const matches = remote === null ? narrowOptionsByLabel(remaining, query) : remaining;
1484
1530
  const expanded = open && !field.disabled && matches.length > 0;
1531
+ const said = remote === null ? "" : optionsStatus(text, {
1532
+ control: "typeahead",
1533
+ open: open && !field.disabled,
1534
+ disabled: field.disabled,
1535
+ failed: remote.failed,
1536
+ busy: remote.busy,
1537
+ needs: remote.needs,
1538
+ query,
1539
+ completeness: remote.completeness
1540
+ });
1485
1541
  const add = (value) => {
1486
1542
  field.setValue(ticked(field.def, chosen, value, true));
1487
1543
  setQuery("");
@@ -1491,92 +1547,141 @@ function TagPickerField({ path, label }) {
1491
1547
  field.setValue(chosen.filter((candidate) => candidate !== value));
1492
1548
  };
1493
1549
  const labelFor = (value) => options.find((option) => option.value === value)?.label ?? value;
1550
+ if (remote?.unavailable === true) return /* @__PURE__ */ jsx(FieldShell, {
1551
+ path,
1552
+ field,
1553
+ label,
1554
+ children: /* @__PURE__ */ jsx("p", {
1555
+ "data-formancy-part": "options-unavailable",
1556
+ id: field.controlProps.id,
1557
+ tabIndex: -1,
1558
+ children: text("options.unavailable", { source: field.def.optionsSource ?? "" })
1559
+ })
1560
+ });
1494
1561
  return /* @__PURE__ */ jsx(FieldShell, {
1495
1562
  path,
1496
1563
  field,
1497
1564
  label,
1498
1565
  children: /* @__PURE__ */ jsxs("div", {
1499
1566
  "data-formancy-part": "tagpicker",
1500
- children: [chosen.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
1501
- "data-formancy-part": "tagpicker-chips",
1502
- "aria-label": text("tagpicker.chosen", { label }),
1503
- children: chosen.map((value) => /* @__PURE__ */ jsxs("li", {
1504
- "data-formancy-part": "tagpicker-chip",
1505
- children: [labelFor(value), /* @__PURE__ */ jsx("button", {
1506
- type: "button",
1507
- "data-formancy-part": "tagpicker-remove",
1508
- "aria-label": text("tagpicker.remove", { option: labelFor(value) }),
1509
- disabled: field.disabled,
1510
- onClick: () => remove(value),
1511
- children: "×"
1512
- })]
1513
- }, value))
1514
- }), /* @__PURE__ */ jsxs("div", {
1515
- "data-formancy-part": "tagpicker-anchor",
1516
- children: [/* @__PURE__ */ jsx("input", {
1517
- type: "text",
1518
- role: "combobox",
1519
- ...field.controlProps,
1520
- "data-formancy-part": "tagpicker-input",
1521
- autoComplete: "off",
1522
- "aria-expanded": expanded,
1523
- "aria-controls": listboxId,
1524
- "aria-autocomplete": "list",
1525
- value: query,
1526
- onChange: (event) => {
1527
- setQuery(event.target.value);
1528
- setOpen(true);
1529
- },
1530
- onClick: () => setOpen(true),
1531
- onKeyDown: (event) => {
1532
- if (event.key === "Escape") {
1567
+ children: [
1568
+ chosen.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
1569
+ "data-formancy-part": "tagpicker-chips",
1570
+ "aria-label": text("tagpicker.chosen", { label }),
1571
+ children: chosen.map((value) => /* @__PURE__ */ jsxs("li", {
1572
+ "data-formancy-part": "tagpicker-chip",
1573
+ children: [labelFor(value), /* @__PURE__ */ jsx("button", {
1574
+ type: "button",
1575
+ "data-formancy-part": "tagpicker-remove",
1576
+ "aria-label": text("tagpicker.remove", { option: labelFor(value) }),
1577
+ disabled: field.disabled,
1578
+ onClick: () => remove(value),
1579
+ children: "×"
1580
+ })]
1581
+ }, value))
1582
+ }),
1583
+ /* @__PURE__ */ jsxs("div", {
1584
+ "data-formancy-part": "tagpicker-anchor",
1585
+ children: [/* @__PURE__ */ jsx("input", {
1586
+ type: "text",
1587
+ role: "combobox",
1588
+ ...field.controlProps,
1589
+ "data-formancy-part": "tagpicker-input",
1590
+ autoComplete: "off",
1591
+ "aria-expanded": expanded,
1592
+ "aria-controls": listboxId,
1593
+ "aria-autocomplete": "list",
1594
+ "aria-busy": remote?.busy === true ? true : void 0,
1595
+ value: query,
1596
+ onChange: (event) => {
1597
+ setQuery(event.target.value);
1598
+ setOpen(true);
1599
+ },
1600
+ onClick: () => setOpen(true),
1601
+ onKeyDown: (event) => {
1602
+ if (event.key === "Escape") {
1603
+ setOpen(false);
1604
+ return;
1605
+ }
1606
+ if (event.key === "Enter" && expanded && matches[0] !== void 0) {
1607
+ event.preventDefault();
1608
+ add(matches[0].value);
1609
+ return;
1610
+ }
1611
+ if (event.key === "Backspace" && query === "" && chosen.length > 0) remove(chosen[chosen.length - 1]);
1612
+ },
1613
+ onBlur: () => {
1533
1614
  setOpen(false);
1534
- return;
1615
+ field.touch();
1535
1616
  }
1536
- if (event.key === "Enter" && expanded && matches[0] !== void 0) {
1537
- event.preventDefault();
1538
- add(matches[0].value);
1539
- return;
1540
- }
1541
- if (event.key === "Backspace" && query === "" && chosen.length > 0) remove(chosen[chosen.length - 1]);
1542
- },
1543
- onBlur: () => {
1544
- setOpen(false);
1545
- field.touch();
1546
- }
1547
- }), /* @__PURE__ */ jsx("ul", {
1548
- id: listboxId,
1549
- role: "listbox",
1550
- "aria-label": text("options.suggestions", { label }),
1551
- "data-formancy-part": "tagpicker-listbox",
1552
- hidden: !expanded,
1553
- children: matches.map((option) => /* @__PURE__ */ jsx("li", {
1554
- role: "option",
1555
- "data-formancy-part": "tagpicker-option",
1556
- "aria-selected": false,
1557
- onMouseDown: (event) => event.preventDefault(),
1558
- onClick: () => add(option.value),
1559
- children: option.label
1560
- }, option.value))
1561
- })]
1562
- })]
1617
+ }), /* @__PURE__ */ jsx("ul", {
1618
+ id: listboxId,
1619
+ role: "listbox",
1620
+ "aria-label": text("options.suggestions", { label }),
1621
+ "data-formancy-part": "tagpicker-listbox",
1622
+ hidden: !expanded,
1623
+ children: matches.map((option) => /* @__PURE__ */ jsx("li", {
1624
+ role: "option",
1625
+ "data-formancy-part": "tagpicker-option",
1626
+ "aria-selected": false,
1627
+ onMouseDown: (event) => event.preventDefault(),
1628
+ onClick: () => add(option.value),
1629
+ children: option.label
1630
+ }, option.value))
1631
+ })]
1632
+ }),
1633
+ remote === null ? null : /* @__PURE__ */ jsx("p", {
1634
+ role: "status",
1635
+ "data-formancy-part": "tagpicker-status",
1636
+ "data-state": remote.failed ? "failed" : void 0,
1637
+ children: said
1638
+ })
1639
+ ]
1563
1640
  })
1564
1641
  });
1565
1642
  }
1566
1643
  function SelectBoxesField({ path, label }) {
1567
1644
  const field = useField(path);
1568
- const options = useResolvedOptions(field);
1645
+ const text = useFormText();
1646
+ const written = useResolvedOptions(field);
1647
+ const sourced = useSourcedOptions({
1648
+ ...field,
1649
+ path
1650
+ }, "");
1651
+ const remote = sourced.remote;
1652
+ const options = remote === null ? written : sourced.options;
1569
1653
  const showError = field.touched && field.errors.length > 0;
1570
1654
  const chosen = Array.isArray(field.value) ? field.value : [];
1655
+ const group = useRef(null);
1656
+ const again = useRef(null);
1657
+ const [retrying, setRetrying] = useState(false);
1658
+ const settled = remote !== null && !remote.busy && !remote.failed;
1659
+ useEffect(() => {
1660
+ if (!retrying || !settled) return;
1661
+ setRetrying(false);
1662
+ if (document.activeElement === again.current) group.current?.querySelector("input[type=\"checkbox\"]")?.focus();
1663
+ }, [retrying, settled]);
1664
+ const said = remote === null ? "" : optionsStatus(text, {
1665
+ control: "ticks",
1666
+ open: true,
1667
+ disabled: field.disabled,
1668
+ failed: remote.failed,
1669
+ busy: remote.busy,
1670
+ needs: remote.needs,
1671
+ query: "",
1672
+ completeness: remote.completeness
1673
+ });
1571
1674
  const toggle = (value, on) => {
1572
1675
  field.setValue(ticked(field.def, chosen, value, on));
1573
1676
  };
1574
1677
  return /* @__PURE__ */ jsxs("fieldset", {
1678
+ ref: group,
1575
1679
  id: field.ids.control,
1576
1680
  "data-formancy-part": "field",
1577
1681
  "data-formancy-field-path": path,
1578
1682
  "data-state": showError ? "invalid" : "valid",
1579
1683
  "aria-describedby": field.controlProps["aria-describedby"],
1684
+ "aria-busy": remote?.busy === true ? true : void 0,
1580
1685
  children: [
1581
1686
  /* @__PURE__ */ jsx("legend", {
1582
1687
  "data-formancy-part": "label",
@@ -1584,6 +1689,11 @@ function SelectBoxesField({ path, label }) {
1584
1689
  }),
1585
1690
  /* @__PURE__ */ jsx(RequiredHint, { field }),
1586
1691
  /* @__PURE__ */ jsx(Description, { field }),
1692
+ remote?.unavailable === true ? /* @__PURE__ */ jsx("p", {
1693
+ "data-formancy-part": "options-unavailable",
1694
+ tabIndex: -1,
1695
+ children: text("options.unavailable", { source: field.def.optionsSource ?? "" })
1696
+ }) : null,
1587
1697
  options.map((option) => {
1588
1698
  const optionId = `${field.ids.control}:${option.value}`;
1589
1699
  return /* @__PURE__ */ jsxs("span", {
@@ -1603,6 +1713,22 @@ function SelectBoxesField({ path, label }) {
1603
1713
  })]
1604
1714
  }, option.value);
1605
1715
  }),
1716
+ remote === null || remote.unavailable ? null : /* @__PURE__ */ jsx("p", {
1717
+ role: "status",
1718
+ "data-formancy-part": "checkbox-status",
1719
+ "data-state": remote.failed ? "failed" : void 0,
1720
+ children: said
1721
+ }),
1722
+ remote !== null && (remote.failed || retrying) && !field.disabled ? /* @__PURE__ */ jsx("button", {
1723
+ ref: again,
1724
+ type: "button",
1725
+ "data-formancy-part": "checkbox-retry",
1726
+ onClick: () => {
1727
+ setRetrying(true);
1728
+ remote.retry();
1729
+ },
1730
+ children: text("options.retry")
1731
+ }) : null,
1606
1732
  showError ? /* @__PURE__ */ jsx(ErrorText, { field }) : null
1607
1733
  ]
1608
1734
  });
@@ -2209,6 +2335,30 @@ function TextareaField({ path, label }) {
2209
2335
  });
2210
2336
  }
2211
2337
  //#endregion
2338
+ //#region src/fields.ts
2339
+ const DEFAULT_COMPONENTS = {
2340
+ text: TextField,
2341
+ textarea: TextareaField,
2342
+ number: NumberSwitch,
2343
+ checkbox: CheckboxField,
2344
+ date: DateField,
2345
+ time: TimeField,
2346
+ datetime: DateTimeField,
2347
+ select: SelectField,
2348
+ radio: RadioGroupField,
2349
+ selectboxes: SelectBoxesSwitch,
2350
+ ranking: RankingField,
2351
+ matrix: MatrixField,
2352
+ file: FileField,
2353
+ richtext: RichTextField,
2354
+ signature: SignatureField,
2355
+ hidden: null,
2356
+ static: StaticField,
2357
+ group: null,
2358
+ page: null,
2359
+ repeater: null
2360
+ };
2361
+ //#endregion
2212
2362
  //#region src/form.tsx
2213
2363
  function FormancyForm(props) {
2214
2364
  return useFormEngine().wizard() === void 0 ? /* @__PURE__ */ jsx(FlatForm, { ...props }) : /* @__PURE__ */ jsx(PagedForm, { ...props });
@@ -2324,7 +2474,7 @@ function FieldList({ labels, registry, page, layout }) {
2324
2474
  function FieldSlot({ path, fallbackLabel, registry }) {
2325
2475
  const field = useField(path);
2326
2476
  if (!field.visible) return null;
2327
- const Component = registry?.byPath?.[path] ?? registry?.byType?.[field.type] ?? DEFAULT_COMPONENTS[field.type];
2477
+ const Component = registry?.byPath?.[path] ?? (field.readOnly && !hasReadOnlyControl(field.def) ? ComputedAnswerField : void 0) ?? registry?.byType?.[field.type] ?? DEFAULT_COMPONENTS[field.type];
2328
2478
  if (Component === null) return null;
2329
2479
  const label = field.label ?? fallbackLabel ?? path;
2330
2480
  return /* @__PURE__ */ jsx(Component, {
@@ -2379,6 +2529,14 @@ function RepeaterSection({ wire, labels, registry }) {
2379
2529
  const label = engine.text(def?.label) ?? labels?.[wire] ?? wire;
2380
2530
  const addLabel = def?.addLabel ?? text("repeater.add", { label });
2381
2531
  const removeLabel = def?.removeLabel ?? text("repeater.remove", { label });
2532
+ const fieldset = useRef(null);
2533
+ const addButton = useRef(null);
2534
+ const [focusing, setFocusing] = useState(void 0);
2535
+ useEffect(() => {
2536
+ if (focusing === void 0 || fieldset.current === null) return;
2537
+ focusRow(fieldset.current, focusing, addButton.current);
2538
+ setFocusing(void 0);
2539
+ }, [focusing]);
2382
2540
  const fallbackFor = (instanceWire) => {
2383
2541
  const template = instanceWire.replace(/\[\d+\]/, "[]");
2384
2542
  return labels?.[template] ?? labels?.[instanceWire];
@@ -2397,7 +2555,13 @@ function RepeaterSection({ wire, labels, registry }) {
2397
2555
  type: "button",
2398
2556
  "data-formancy-part": "row-remove",
2399
2557
  disabled: field.disabled,
2400
- onClick: () => repeater.removeRow(index),
2558
+ onClick: () => {
2559
+ repeater.removeRow(index);
2560
+ setFocusing(repeater.rowCount === 1 ? "add" : {
2561
+ row: Math.min(index, repeater.rowCount - 2),
2562
+ parts: []
2563
+ });
2564
+ },
2401
2565
  children: /* @__PURE__ */ jsx("span", {
2402
2566
  "data-formancy-part": "row-action-text",
2403
2567
  children: text("repeater.removeRow", {
@@ -2407,11 +2571,17 @@ function RepeaterSection({ wire, labels, registry }) {
2407
2571
  })
2408
2572
  })
2409
2573
  }) : null,
2410
- index > 0 ? /* @__PURE__ */ jsx("button", {
2574
+ index > 0 && !field.readOnly ? /* @__PURE__ */ jsx("button", {
2411
2575
  type: "button",
2412
2576
  "data-formancy-part": "row-up",
2413
2577
  disabled: field.disabled,
2414
- onClick: () => repeater.moveRow(index, index - 1),
2578
+ onClick: () => {
2579
+ repeater.moveRow(index, index - 1);
2580
+ setFocusing({
2581
+ row: index - 1,
2582
+ parts: ["row-up", "row-down"]
2583
+ });
2584
+ },
2415
2585
  children: /* @__PURE__ */ jsx("span", {
2416
2586
  "data-formancy-part": "row-action-text",
2417
2587
  children: text("repeater.moveUp", {
@@ -2421,11 +2591,17 @@ function RepeaterSection({ wire, labels, registry }) {
2421
2591
  })
2422
2592
  })
2423
2593
  }) : null,
2424
- index < repeater.rowCount - 1 ? /* @__PURE__ */ jsx("button", {
2594
+ index < repeater.rowCount - 1 && !field.readOnly ? /* @__PURE__ */ jsx("button", {
2425
2595
  type: "button",
2426
2596
  "data-formancy-part": "row-down",
2427
2597
  disabled: field.disabled,
2428
- onClick: () => repeater.moveRow(index, index + 1),
2598
+ onClick: () => {
2599
+ repeater.moveRow(index, index + 1);
2600
+ setFocusing({
2601
+ row: index + 1,
2602
+ parts: ["row-down", "row-up"]
2603
+ });
2604
+ },
2429
2605
  children: /* @__PURE__ */ jsx("span", {
2430
2606
  "data-formancy-part": "row-action-text",
2431
2607
  children: text("repeater.moveDown", {
@@ -2458,6 +2634,7 @@ function RepeaterSection({ wire, labels, registry }) {
2458
2634
  if (!field.visible) return null;
2459
2635
  const showError = field.touched && field.errors.length > 0;
2460
2636
  return /* @__PURE__ */ jsxs("fieldset", {
2637
+ ref: fieldset,
2461
2638
  "data-formancy-part": "repeater",
2462
2639
  "data-formancy-field-path": wire,
2463
2640
  "data-state": showError ? "invalid" : "valid",
@@ -2484,37 +2661,27 @@ function RepeaterSection({ wire, labels, registry }) {
2484
2661
  }) : null, rows]
2485
2662
  }) : rows,
2486
2663
  showError ? /* @__PURE__ */ jsx(ErrorText, { field }) : null,
2664
+ def?.maxItems === void 0 || field.readOnly ? null : /* @__PURE__ */ jsx("p", {
2665
+ role: "status",
2666
+ "data-formancy-part": "repeater-status",
2667
+ children: repeater.canAdd ? "" : text("repeater.full", { max: def.maxItems })
2668
+ }),
2487
2669
  repeater.canAdd ? /* @__PURE__ */ jsx("button", {
2488
2670
  type: "button",
2671
+ ref: addButton,
2489
2672
  disabled: field.disabled,
2490
- onClick: () => repeater.addRow(),
2673
+ onClick: () => {
2674
+ repeater.addRow();
2675
+ setFocusing({
2676
+ row: repeater.rowCount,
2677
+ parts: []
2678
+ });
2679
+ },
2491
2680
  children: addLabel
2492
2681
  }) : null
2493
2682
  ]
2494
2683
  });
2495
2684
  }
2496
- const DEFAULT_COMPONENTS = {
2497
- text: TextField,
2498
- textarea: TextareaField,
2499
- number: NumberSwitch,
2500
- checkbox: CheckboxField,
2501
- date: DateField,
2502
- time: TimeField,
2503
- datetime: DateTimeField,
2504
- select: SelectField,
2505
- radio: RadioGroupField,
2506
- selectboxes: SelectBoxesSwitch,
2507
- ranking: RankingField,
2508
- matrix: MatrixField,
2509
- file: FileField,
2510
- richtext: RichTextField,
2511
- signature: SignatureField,
2512
- hidden: null,
2513
- static: StaticField,
2514
- group: null,
2515
- page: null,
2516
- repeater: null
2517
- };
2518
2685
  //#endregion
2519
2686
  //#region src/error-summary.tsx
2520
2687
  function ErrorSummary({ labels }) {