@sectile/vue 0.7.0 → 0.9.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.
Files changed (319) hide show
  1. package/README.md +4 -2
  2. package/dist/accordion.d.ts +0 -1
  3. package/dist/alert-dialog.d.ts +0 -2
  4. package/dist/alert-dialog.js.map +1 -1
  5. package/dist/calendar.d.ts +2 -3
  6. package/dist/calendar.js +14 -14
  7. package/dist/calendar.js.map +1 -1
  8. package/dist/carousel.d.ts +22 -1
  9. package/dist/carousel.js +7 -1
  10. package/dist/carousel.js.map +1 -1
  11. package/dist/cascade-list.d.ts +339 -0
  12. package/dist/cascade-list.js +240 -0
  13. package/dist/cascade-list.js.map +1 -0
  14. package/dist/cascade-select.d.ts +11 -42
  15. package/dist/cascade-select.js +19 -27
  16. package/dist/cascade-select.js.map +1 -1
  17. package/dist/checkbox-group.d.ts +9 -1
  18. package/dist/checkbox-group.js +57 -13
  19. package/dist/checkbox-group.js.map +1 -1
  20. package/dist/checkbox.d.ts +0 -1
  21. package/dist/checkbox.js +15 -8
  22. package/dist/checkbox.js.map +1 -1
  23. package/dist/color-picker.d.ts +0 -1
  24. package/dist/combobox.d.ts +0 -1
  25. package/dist/combobox.js +46 -6
  26. package/dist/combobox.js.map +1 -1
  27. package/dist/data-grid.d.ts +207 -0
  28. package/dist/data-grid.js +107 -0
  29. package/dist/data-grid.js.map +1 -0
  30. package/dist/data-table.d.ts +239 -0
  31. package/dist/data-table.js +120 -0
  32. package/dist/data-table.js.map +1 -0
  33. package/dist/data-tree-grid.d.ts +218 -0
  34. package/dist/data-tree-grid.js +95 -0
  35. package/dist/data-tree-grid.js.map +1 -0
  36. package/dist/date-field.d.ts +1 -2
  37. package/dist/date-field.js +1 -1
  38. package/dist/date-field.js.map +1 -1
  39. package/dist/date-picker.d.ts +37 -6
  40. package/dist/date-picker.js +3 -1
  41. package/dist/date-picker.js.map +1 -1
  42. package/dist/date-range-field.d.ts +2 -3
  43. package/dist/date-range-field.js +24 -6
  44. package/dist/date-range-field.js.map +1 -1
  45. package/dist/date-range-picker.d.ts +37 -6
  46. package/dist/date-range-picker.js +3 -1
  47. package/dist/date-range-picker.js.map +1 -1
  48. package/dist/date-time-field.d.ts +1 -2
  49. package/dist/date-time-field.js +1 -1
  50. package/dist/date-time-field.js.map +1 -1
  51. package/dist/date-time-picker.d.ts +38 -7
  52. package/dist/date-time-picker.js +3 -1
  53. package/dist/date-time-picker.js.map +1 -1
  54. package/dist/date-time-range-picker.d.ts +38 -7
  55. package/dist/date-time-range-picker.js +3 -1
  56. package/dist/date-time-range-picker.js.map +1 -1
  57. package/dist/dialog.d.ts +0 -2
  58. package/dist/dialog.js.map +1 -1
  59. package/dist/disclosure.d.ts +0 -1
  60. package/dist/drawer.d.ts +1 -2
  61. package/dist/editable.d.ts +0 -1
  62. package/dist/editable.js +25 -6
  63. package/dist/editable.js.map +1 -1
  64. package/dist/feed.d.ts +0 -1
  65. package/dist/form.d.ts +88 -115
  66. package/dist/form.js +461 -233
  67. package/dist/form.js.map +1 -1
  68. package/dist/grid.d.ts +0 -1
  69. package/dist/grid.js +51 -6
  70. package/dist/grid.js.map +1 -1
  71. package/dist/host-provider.d.ts +0 -13
  72. package/dist/host-provider.js +0 -16
  73. package/dist/host-provider.js.map +1 -1
  74. package/dist/index.d.ts +14 -39
  75. package/dist/index.js +6 -19
  76. package/dist/index.js.map +1 -1
  77. package/dist/internal/cascade-choice.d.ts +204 -0
  78. package/dist/internal/cascade-choice.js +161 -0
  79. package/dist/internal/cascade-choice.js.map +1 -0
  80. package/dist/internal/collection.d.ts +3 -8
  81. package/dist/internal/collection.js +14 -20
  82. package/dist/internal/collection.js.map +1 -1
  83. package/dist/internal/controlled-state.d.ts +0 -1
  84. package/dist/internal/date-picker.d.ts +78 -9
  85. package/dist/internal/date-picker.js +116 -27
  86. package/dist/internal/date-picker.js.map +1 -1
  87. package/dist/internal/decimal-input.d.ts +1 -0
  88. package/dist/internal/decimal-input.js +24 -0
  89. package/dist/internal/decimal-input.js.map +1 -0
  90. package/dist/internal/form-control-inventory.d.ts +4 -1
  91. package/dist/internal/form-control-inventory.js +1 -0
  92. package/dist/internal/form-control-inventory.js.map +1 -1
  93. package/dist/internal/form-control.d.ts +100 -2
  94. package/dist/internal/form-control.js +103 -1
  95. package/dist/internal/form-control.js.map +1 -1
  96. package/dist/internal/native-field.d.ts +0 -1
  97. package/dist/internal/native-field.js +11 -2
  98. package/dist/internal/native-field.js.map +1 -1
  99. package/dist/internal/native-input.d.ts +0 -1
  100. package/dist/internal/part-contract.d.ts +0 -1
  101. package/dist/internal/popup.d.ts +5 -27
  102. package/dist/internal/popup.js +15 -21
  103. package/dist/internal/popup.js.map +1 -1
  104. package/dist/internal/presence.d.ts +0 -1
  105. package/dist/internal/scheduled-task.d.ts +5 -0
  106. package/dist/internal/scheduled-task.js +28 -0
  107. package/dist/internal/scheduled-task.js.map +1 -0
  108. package/dist/internal/tabs-context.d.ts +2 -3
  109. package/dist/internal/tabular-components.d.ts +27 -0
  110. package/dist/internal/tabular-components.js +26 -0
  111. package/dist/internal/tabular-components.js.map +1 -0
  112. package/dist/internal/tabular-parts.d.ts +57 -0
  113. package/dist/internal/tabular-parts.js +239 -0
  114. package/dist/internal/tabular-parts.js.map +1 -0
  115. package/dist/internal/tabular-profile.d.ts +75 -0
  116. package/dist/internal/tabular-profile.js +211 -0
  117. package/dist/internal/tabular-profile.js.map +1 -0
  118. package/dist/internal/virtual-collection-model.d.ts +42 -0
  119. package/dist/internal/virtual-collection-model.js +241 -0
  120. package/dist/internal/virtual-collection-model.js.map +1 -0
  121. package/dist/internal/virtual-collection.d.ts +38 -0
  122. package/dist/internal/virtual-collection.js +87 -0
  123. package/dist/internal/virtual-collection.js.map +1 -0
  124. package/dist/{virtual.d.ts → internal/virtual-core.d.ts} +9 -4
  125. package/dist/{virtual.js → internal/virtual-core.js} +7 -7
  126. package/dist/internal/virtual-core.js.map +1 -0
  127. package/dist/internal/virtual-list.d.ts +214 -0
  128. package/dist/internal/virtual-list.js +401 -0
  129. package/dist/internal/virtual-list.js.map +1 -0
  130. package/dist/listbox.d.ts +0 -1
  131. package/dist/listbox.js +52 -6
  132. package/dist/listbox.js.map +1 -1
  133. package/dist/menu-button.d.ts +0 -1
  134. package/dist/menu.d.ts +0 -1
  135. package/dist/menubar.d.ts +0 -1
  136. package/dist/meter-group.d.ts +369 -0
  137. package/dist/meter-group.js +271 -0
  138. package/dist/meter-group.js.map +1 -0
  139. package/dist/meter.d.ts +191 -0
  140. package/dist/meter.js +133 -0
  141. package/dist/meter.js.map +1 -0
  142. package/dist/month-picker.d.ts +37 -6
  143. package/dist/month-picker.js +3 -1
  144. package/dist/month-picker.js.map +1 -1
  145. package/dist/month-range-picker.d.ts +37 -6
  146. package/dist/month-range-picker.js +3 -1
  147. package/dist/month-range-picker.js.map +1 -1
  148. package/dist/multi-thumb-slider.d.ts +0 -1
  149. package/dist/navigation-menu.d.ts +0 -1
  150. package/dist/number-field.d.ts +0 -1
  151. package/dist/pagination.d.ts +23 -2
  152. package/dist/pagination.js +6 -1
  153. package/dist/pagination.js.map +1 -1
  154. package/dist/pin-input.d.ts +0 -1
  155. package/dist/pin-input.js +1 -1
  156. package/dist/pin-input.js.map +1 -1
  157. package/dist/popover.d.ts +0 -2
  158. package/dist/popover.js.map +1 -1
  159. package/dist/primitive.d.ts +0 -1
  160. package/dist/primitive.js +8 -4
  161. package/dist/primitive.js.map +1 -1
  162. package/dist/progress.d.ts +148 -0
  163. package/dist/progress.js +110 -0
  164. package/dist/progress.js.map +1 -0
  165. package/dist/quantity-field.d.ts +0 -1
  166. package/dist/radio-group.d.ts +0 -1
  167. package/dist/radio-group.js +1 -1
  168. package/dist/radio-group.js.map +1 -1
  169. package/dist/range-calendar.d.ts +2 -3
  170. package/dist/rating.d.ts +0 -1
  171. package/dist/rating.js +10 -1
  172. package/dist/rating.js.map +1 -1
  173. package/dist/reorder.d.ts +1 -2
  174. package/dist/select.d.ts +18 -44
  175. package/dist/select.js +11 -13
  176. package/dist/select.js.map +1 -1
  177. package/dist/slider.d.ts +0 -1
  178. package/dist/spin-button.d.ts +0 -1
  179. package/dist/spin-button.js +17 -4
  180. package/dist/spin-button.js.map +1 -1
  181. package/dist/stepper.d.ts +0 -1
  182. package/dist/stepper.js +19 -2
  183. package/dist/stepper.js.map +1 -1
  184. package/dist/switch.d.ts +0 -1
  185. package/dist/tabs.d.ts +0 -1
  186. package/dist/tabs.js +4 -22
  187. package/dist/tabs.js.map +1 -1
  188. package/dist/tags-input.d.ts +0 -1
  189. package/dist/tags-input.js +18 -5
  190. package/dist/tags-input.js.map +1 -1
  191. package/dist/temporal-provider.d.ts +21 -0
  192. package/dist/temporal-provider.js +30 -0
  193. package/dist/temporal-provider.js.map +1 -0
  194. package/dist/text.d.ts +0 -1
  195. package/dist/text.js +18 -4
  196. package/dist/text.js.map +1 -1
  197. package/dist/time-field.d.ts +1 -2
  198. package/dist/time-field.js +1 -1
  199. package/dist/time-field.js.map +1 -1
  200. package/dist/time-range-field.d.ts +2 -2
  201. package/dist/time-range-field.js +6 -4
  202. package/dist/time-range-field.js.map +1 -1
  203. package/dist/timer.d.ts +0 -1
  204. package/dist/toast.d.ts +11 -2
  205. package/dist/toast.js +22 -9
  206. package/dist/toast.js.map +1 -1
  207. package/dist/toggle-button.d.ts +0 -1
  208. package/dist/toggle-group.d.ts +0 -1
  209. package/dist/toggle-group.js +1 -1
  210. package/dist/toggle-group.js.map +1 -1
  211. package/dist/toolbar.d.ts +0 -1
  212. package/dist/tooltip.d.ts +1 -3
  213. package/dist/tooltip.js.map +1 -1
  214. package/dist/tree-grid.d.ts +0 -1
  215. package/dist/tree-grid.js +62 -8
  216. package/dist/tree-grid.js.map +1 -1
  217. package/dist/tree-view.d.ts +0 -1
  218. package/dist/tree-view.js +51 -10
  219. package/dist/tree-view.js.map +1 -1
  220. package/dist/virtual-core.d.ts +1 -0
  221. package/dist/virtual-core.js +2 -0
  222. package/dist/virtual-core.js.map +1 -0
  223. package/dist/virtual-grid.d.ts +188 -0
  224. package/dist/virtual-grid.js +306 -0
  225. package/dist/virtual-grid.js.map +1 -0
  226. package/dist/virtual-list.d.ts +1 -0
  227. package/dist/virtual-list.js +2 -0
  228. package/dist/virtual-list.js.map +1 -0
  229. package/dist/virtual-masonry.d.ts +198 -0
  230. package/dist/virtual-masonry.js +221 -0
  231. package/dist/virtual-masonry.js.map +1 -0
  232. package/dist/virtual-spatial.d.ts +157 -0
  233. package/dist/virtual-spatial.js +159 -0
  234. package/dist/virtual-spatial.js.map +1 -0
  235. package/dist/window-splitter.d.ts +0 -1
  236. package/dist/year-picker.d.ts +37 -6
  237. package/dist/year-picker.js +3 -1
  238. package/dist/year-picker.js.map +1 -1
  239. package/dist/year-range-picker.d.ts +37 -6
  240. package/dist/year-range-picker.js +3 -1
  241. package/dist/year-range-picker.js.map +1 -1
  242. package/package.json +173 -89
  243. package/dist/accordion.d.ts.map +0 -1
  244. package/dist/alert-dialog.d.ts.map +0 -1
  245. package/dist/calendar.d.ts.map +0 -1
  246. package/dist/carousel.d.ts.map +0 -1
  247. package/dist/cascade-select.d.ts.map +0 -1
  248. package/dist/checkbox-group.d.ts.map +0 -1
  249. package/dist/checkbox.d.ts.map +0 -1
  250. package/dist/color-picker.d.ts.map +0 -1
  251. package/dist/combobox.d.ts.map +0 -1
  252. package/dist/date-field.d.ts.map +0 -1
  253. package/dist/date-picker.d.ts.map +0 -1
  254. package/dist/date-range-field.d.ts.map +0 -1
  255. package/dist/date-range-picker.d.ts.map +0 -1
  256. package/dist/date-time-field.d.ts.map +0 -1
  257. package/dist/date-time-picker.d.ts.map +0 -1
  258. package/dist/date-time-range-picker.d.ts.map +0 -1
  259. package/dist/dialog.d.ts.map +0 -1
  260. package/dist/disclosure.d.ts.map +0 -1
  261. package/dist/drawer.d.ts.map +0 -1
  262. package/dist/editable.d.ts.map +0 -1
  263. package/dist/feed.d.ts.map +0 -1
  264. package/dist/form.d.ts.map +0 -1
  265. package/dist/grid.d.ts.map +0 -1
  266. package/dist/host-provider.d.ts.map +0 -1
  267. package/dist/index.d.ts.map +0 -1
  268. package/dist/internal/collection.d.ts.map +0 -1
  269. package/dist/internal/controlled-state.d.ts.map +0 -1
  270. package/dist/internal/date-picker.d.ts.map +0 -1
  271. package/dist/internal/form-control-inventory.d.ts.map +0 -1
  272. package/dist/internal/form-control.d.ts.map +0 -1
  273. package/dist/internal/native-field.d.ts.map +0 -1
  274. package/dist/internal/native-input.d.ts.map +0 -1
  275. package/dist/internal/part-contract.d.ts.map +0 -1
  276. package/dist/internal/popup.d.ts.map +0 -1
  277. package/dist/internal/presence.d.ts.map +0 -1
  278. package/dist/internal/tabs-context.d.ts.map +0 -1
  279. package/dist/listbox.d.ts.map +0 -1
  280. package/dist/menu-button.d.ts.map +0 -1
  281. package/dist/menu.d.ts.map +0 -1
  282. package/dist/menubar.d.ts.map +0 -1
  283. package/dist/month-picker.d.ts.map +0 -1
  284. package/dist/month-range-picker.d.ts.map +0 -1
  285. package/dist/multi-thumb-slider.d.ts.map +0 -1
  286. package/dist/navigation-menu.d.ts.map +0 -1
  287. package/dist/number-field.d.ts.map +0 -1
  288. package/dist/pagination.d.ts.map +0 -1
  289. package/dist/pin-input.d.ts.map +0 -1
  290. package/dist/popover.d.ts.map +0 -1
  291. package/dist/primitive.d.ts.map +0 -1
  292. package/dist/quantity-field.d.ts.map +0 -1
  293. package/dist/radio-group.d.ts.map +0 -1
  294. package/dist/range-calendar.d.ts.map +0 -1
  295. package/dist/rating.d.ts.map +0 -1
  296. package/dist/reorder.d.ts.map +0 -1
  297. package/dist/select.d.ts.map +0 -1
  298. package/dist/slider.d.ts.map +0 -1
  299. package/dist/spin-button.d.ts.map +0 -1
  300. package/dist/stepper.d.ts.map +0 -1
  301. package/dist/switch.d.ts.map +0 -1
  302. package/dist/tabs.d.ts.map +0 -1
  303. package/dist/tags-input.d.ts.map +0 -1
  304. package/dist/text.d.ts.map +0 -1
  305. package/dist/time-field.d.ts.map +0 -1
  306. package/dist/time-range-field.d.ts.map +0 -1
  307. package/dist/timer.d.ts.map +0 -1
  308. package/dist/toast.d.ts.map +0 -1
  309. package/dist/toggle-button.d.ts.map +0 -1
  310. package/dist/toggle-group.d.ts.map +0 -1
  311. package/dist/toolbar.d.ts.map +0 -1
  312. package/dist/tooltip.d.ts.map +0 -1
  313. package/dist/tree-grid.d.ts.map +0 -1
  314. package/dist/tree-view.d.ts.map +0 -1
  315. package/dist/virtual.d.ts.map +0 -1
  316. package/dist/virtual.js.map +0 -1
  317. package/dist/window-splitter.d.ts.map +0 -1
  318. package/dist/year-picker.d.ts.map +0 -1
  319. package/dist/year-range-picker.d.ts.map +0 -1
package/dist/form.js CHANGED
@@ -1,10 +1,16 @@
1
- import { computed, defineComponent, getCurrentInstance, h, inject, mergeProps, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch, } from 'vue';
1
+ import { computed, defineComponent, h, inject, mergeProps, nextTick, onBeforeUnmount, onMounted, onScopeDispose, provide, shallowRef, watch, } from 'vue';
2
2
  import { appendFormFieldPath, createForm, encodeFormFieldPath, } from '@sectile/dom/form';
3
+ import { tryCreateFormFieldPath } from '@sectile/form/path';
4
+ import { compositeControlCapabilities, hiddenInputSubmissionCapabilities, hiddenSelectSubmissionCapabilities, hiddenValueSubmissionCapabilities, nativeInputControlCapabilities, provideFormControlFieldContext, provideFormControlOwner, useCompositeFormControl, useFormControl, useNativeInputFormControl, } from './internal/form-control.js';
5
+ import { useNextTickTask } from './internal/scheduled-task.js';
3
6
  import { Primitive } from './primitive.js';
4
7
  import { useHostId } from './host-provider.js';
8
+ export function defineFormSubmission(definition) {
9
+ return Object.freeze({ ...definition });
10
+ }
11
+ export { compositeControlCapabilities, hiddenInputSubmissionCapabilities, hiddenSelectSubmissionCapabilities, hiddenValueSubmissionCapabilities, nativeInputControlCapabilities, provideFormControlOwner, useCompositeFormControl, useFormControl, useNativeInputFormControl, };
5
12
  const formContextKey = Symbol('SectileForm');
6
13
  const formFieldContextKey = Symbol('SectileFormField');
7
- const formControlOwnerKey = Symbol('SectileFormControlOwner');
8
14
  const emptyState = Object.freeze({
9
15
  validationGeneration: 0,
10
16
  validationStatus: 'idle',
@@ -48,16 +54,13 @@ const FormRootImpl = defineComponent({
48
54
  },
49
55
  slots: Object,
50
56
  setup(props, { attrs, emit, expose, slots }) {
51
- const root = ref(null);
52
- const summary = ref(null);
53
- const state = shallowRef(emptyState);
57
+ const root = shallowRef(null);
58
+ const summary = shallowRef(null);
54
59
  const connection = shallowRef(null);
55
60
  const participants = new Map();
61
+ const fieldDiagnostics = new Map();
56
62
  let syncConfiguredIssues = () => { };
57
- const sync = () => {
58
- if (connection.value !== null)
59
- state.value = connection.value.getSnapshot().state;
60
- };
63
+ let syncFieldDiagnostics = () => { };
61
64
  const register = (participant) => {
62
65
  const existing = participants.get(participant.id);
63
66
  existing?.unregister?.();
@@ -65,29 +68,27 @@ const FormRootImpl = defineComponent({
65
68
  participants.set(participant.id, registered);
66
69
  if (connection.value !== null)
67
70
  registered.unregister = connection.value.registerParticipant(participant);
68
- void nextTick(() => syncConfiguredIssues());
71
+ void nextTick(() => {
72
+ syncConfiguredIssues();
73
+ syncFieldDiagnostics();
74
+ });
69
75
  return () => {
70
76
  if (participants.get(participant.id) !== registered)
71
77
  return;
72
78
  registered.unregister?.();
73
79
  participants.delete(participant.id);
74
- sync();
75
- void nextTick(syncConfiguredIssues);
80
+ void nextTick(() => {
81
+ syncConfiguredIssues();
82
+ syncFieldDiagnostics();
83
+ });
76
84
  };
77
85
  };
78
- const settleSubmission = (target, generation, result) => {
79
- if (connection.value !== target)
80
- return;
81
- if (typeof result === 'object' && result !== null && result.ok === false) {
82
- target.submitFailed(generation, resolveIssueInputs(target, 'server', result.issues ?? [submissionErrorIssue()]));
83
- return;
84
- }
85
- target.submitSucceeded(generation);
86
- };
87
- const rejectSubmission = (target, generation, reason) => {
88
- if (connection.value !== target)
89
- return;
90
- target.submitFailed(generation, resolveIssueInputs(target, 'server', mapSubmissionError(props.mapSubmitError, reason)));
86
+ const setFieldDiagnostic = (id, issue) => {
87
+ if (issue === null)
88
+ fieldDiagnostics.delete(id);
89
+ else
90
+ fieldDiagnostics.set(id, issue);
91
+ void nextTick(syncFieldDiagnostics);
91
92
  };
92
93
  const submit = (payload) => {
93
94
  const handler = props.onSubmit;
@@ -96,26 +97,29 @@ const FormRootImpl = defineComponent({
96
97
  const target = connection.value;
97
98
  if (target === null)
98
99
  return;
99
- payload.event.preventDefault();
100
- if (target.getSnapshot().state.submissionStatus === 'submitting') {
101
- return;
102
- }
103
- const generation = target.submitStarted();
104
- if (generation === null)
105
- return;
106
100
  let result;
107
101
  try {
108
102
  result = handler(toFormSubmitEvent(payload));
109
103
  }
110
104
  catch (error) {
111
- rejectSubmission(target, generation, error);
112
- return;
105
+ return {
106
+ ok: false,
107
+ issues: resolveIssueInputs(target, 'server', mapSubmissionError(props.mapSubmitError, error)),
108
+ };
113
109
  }
110
+ const settle = (resolved) => (typeof resolved === 'object' && resolved !== null && resolved.ok === false
111
+ ? {
112
+ ok: false,
113
+ issues: resolveIssueInputs(target, 'server', resolved.issues ?? [submissionErrorIssue()]),
114
+ }
115
+ : { ok: true });
114
116
  if (isPromiseLike(result)) {
115
- void Promise.resolve(result).then((resolved) => settleSubmission(target, generation, resolved), (error) => rejectSubmission(target, generation, error));
116
- return;
117
+ return Promise.resolve(result).then(settle, (error) => ({
118
+ ok: false,
119
+ issues: resolveIssueInputs(target, 'server', mapSubmissionError(props.mapSubmitError, error)),
120
+ }));
117
121
  }
118
- settleSubmission(target, generation, result);
122
+ return settle(result);
119
123
  };
120
124
  const configuration = () => ({
121
125
  ...(summary.value === null ? {} : { summary: summary.value }),
@@ -123,10 +127,12 @@ const FormRootImpl = defineComponent({
123
127
  ...(props.validate === undefined ? {} : { validate: props.validate }),
124
128
  validateOn: props.validateOn,
125
129
  revalidateOn: props.revalidateOn,
126
- onSubmit: submit,
127
- onReset: () => emit('reset'),
130
+ ...(props.onSubmit === undefined ? {} : { onSubmit: submit }),
131
+ onReset: () => {
132
+ emit('reset');
133
+ void nextTick(syncFieldDiagnostics);
134
+ },
128
135
  onStateChange: (next) => {
129
- state.value = next;
130
136
  emit('stateChange', next);
131
137
  },
132
138
  });
@@ -136,6 +142,9 @@ const FormRootImpl = defineComponent({
136
142
  return;
137
143
  target.replaceIssues('form', resolveIssueInputs(target, 'form', props.issues));
138
144
  };
145
+ syncFieldDiagnostics = () => {
146
+ connection.value?.replaceIssues('field', [...fieldDiagnostics.values()]);
147
+ };
139
148
  const mount = () => {
140
149
  if (root.value === null)
141
150
  return;
@@ -148,16 +157,24 @@ const FormRootImpl = defineComponent({
148
157
  registered.unregister = connection.value.registerParticipant(registered.participant);
149
158
  }
150
159
  syncConfiguredIssues();
151
- sync();
160
+ syncFieldDiagnostics();
161
+ emit('stateChange', connection.value.state);
152
162
  };
153
- onMounted(() => { void nextTick(mount); });
154
- onBeforeUnmount(() => connection.value?.destroy());
163
+ const mountTask = useNextTickTask(mount);
164
+ onMounted(mountTask.schedule);
165
+ onBeforeUnmount(() => {
166
+ mountTask.cancel();
167
+ connection.value?.destroy();
168
+ connection.value = null;
169
+ });
155
170
  watch([
156
171
  summary,
157
172
  () => props.schema,
158
173
  () => props.validate,
159
174
  () => [...props.validateOn],
160
175
  () => [...props.revalidateOn],
176
+ () => props.onSubmit,
177
+ () => props.mapSubmitError,
161
178
  ], () => {
162
179
  void nextTick(() => connection.value?.reconfigure(configuration()));
163
180
  });
@@ -167,30 +184,47 @@ const FormRootImpl = defineComponent({
167
184
  submitSucceeded: (generation) => connection.value?.submitSucceeded(generation) ?? false,
168
185
  submitFailed: (generation, issues = []) => connection.value?.submitFailed(generation, issues) ?? false,
169
186
  replaceIssues: (source, issues) => connection.value?.replaceIssues(source, issues) ?? false,
187
+ reinitialize: (options) => connection.value?.reinitialize(options),
170
188
  reset: () => connection.value?.reset(),
171
189
  };
172
- const slotProps = computed(() => Object.freeze({
173
- state: state.value,
174
- validationStatus: state.value.validationStatus,
175
- validationTrigger: state.value.validationTrigger,
176
- validationIntent: state.value.validationIntent,
177
- submissionStatus: state.value.submissionStatus,
178
- valid: state.value.valid,
179
- touched: state.value.touched,
180
- dirty: state.value.dirty,
181
- submitted: state.value.submitted,
182
- submitCount: state.value.submitCount,
190
+ const formContext = {
191
+ summary,
192
+ register,
193
+ setFieldDiagnostic,
194
+ connection,
195
+ };
196
+ provide(formContextKey, formContext);
197
+ const validationStatus = useFormSelectorFromContext(formContext, () => (current) => current.validationStatus);
198
+ const submissionStatus = useFormSelectorFromContext(formContext, () => (current) => current.submissionStatus);
199
+ const selectedState = useFormSelectorFromContext(formContext, () => (current) => current);
200
+ const validationTrigger = useFormSelectorFromContext(formContext, () => (current) => current.validationTrigger);
201
+ const validationIntent = useFormSelectorFromContext(formContext, () => (current) => current.validationIntent);
202
+ const valid = useFormSelectorFromContext(formContext, () => (current) => current.valid);
203
+ const touched = useFormSelectorFromContext(formContext, () => (current) => current.touched);
204
+ const dirty = useFormSelectorFromContext(formContext, () => (current) => current.dirty);
205
+ const submitted = useFormSelectorFromContext(formContext, () => (current) => current.submitted);
206
+ const submitCount = useFormSelectorFromContext(formContext, () => (current) => current.submitCount);
207
+ const slotProps = Object.freeze({
208
+ get state() { return selectedState.value; },
209
+ get validationStatus() { return validationStatus.value; },
210
+ get validationTrigger() { return validationTrigger.value; },
211
+ get validationIntent() { return validationIntent.value; },
212
+ get submissionStatus() { return submissionStatus.value; },
213
+ get valid() { return valid.value; },
214
+ get touched() { return touched.value; },
215
+ get dirty() { return dirty.value; },
216
+ get submitted() { return submitted.value; },
217
+ get submitCount() { return submitCount.value; },
183
218
  ...actions,
184
- }));
185
- provide(formContextKey, { state, summary, register, connection });
219
+ });
186
220
  expose(actions);
187
221
  return () => h('form', mergeProps(attrs, {
188
222
  ref: (element) => { root.value = element; },
189
223
  'data-scope': 'form',
190
224
  'data-part': 'root',
191
- 'data-validation-status': state.value.validationStatus,
192
- 'data-submission-status': state.value.submissionStatus,
193
- }), slots['default']?.(slotProps.value) ?? []);
225
+ 'data-validation-status': validationStatus.value,
226
+ 'data-submission-status': submissionStatus.value,
227
+ }), slots['default']?.(slotProps) ?? []);
194
228
  },
195
229
  });
196
230
  export const FormRoot = FormRootImpl;
@@ -200,6 +234,7 @@ function toFormSubmitEvent(payload) {
200
234
  values: payload.values,
201
235
  submitter: payload.submitter,
202
236
  state: payload.state,
237
+ reinitialize: payload.reinitialize,
203
238
  nativeEvent: payload.event,
204
239
  get defaultPrevented() { return payload.event.defaultPrevented; },
205
240
  preventDefault: () => payload.event.preventDefault(),
@@ -216,10 +251,8 @@ function isPromiseLike(value) {
216
251
  function resolveIssueInputs(target, source, input) {
217
252
  const fields = target.getSnapshot().state.fields;
218
253
  return Object.freeze(input.map((issue, index) => {
219
- const name = issue.path === undefined ? undefined : encodeFormFieldPath(issue.path);
220
- const owner = name === undefined
221
- ? undefined
222
- : fields.find((field) => field.name === name);
254
+ const name = issue.path === undefined ? null : safeEncodeFormFieldPath(issue.path);
255
+ const owner = name === null ? undefined : findNamedFieldOwner(fields, name);
223
256
  return Object.freeze({
224
257
  id: issue.id ?? `form-${source}-issue-${index + 1}`,
225
258
  message: issue.message,
@@ -228,6 +261,23 @@ function resolveIssueInputs(target, source, input) {
228
261
  });
229
262
  }));
230
263
  }
264
+ function safeEncodeFormFieldPath(path) {
265
+ const result = tryCreateFormFieldPath(path);
266
+ return result.ok ? encodeFormFieldPath(result.value) : null;
267
+ }
268
+ function findNamedFieldOwner(fields, issueName) {
269
+ let owner;
270
+ for (const candidate of fields) {
271
+ if (candidate.name === null
272
+ || (issueName !== candidate.name
273
+ && !issueName.startsWith(`${candidate.name}.`)
274
+ && !issueName.startsWith(`${candidate.name}[`)))
275
+ continue;
276
+ if (owner === undefined || candidate.name.length > (owner.name?.length ?? 0))
277
+ owner = candidate;
278
+ }
279
+ return owner;
280
+ }
231
281
  function mapSubmissionError(mapper, reason) {
232
282
  if (mapper !== undefined) {
233
283
  try {
@@ -250,6 +300,99 @@ function submissionErrorIssue() {
250
300
  message: 'Form submission failed.',
251
301
  });
252
302
  }
303
+ function useFormSelectorFromContext(context, selectorSource, equalsSource = () => Object.is) {
304
+ const initialSelector = selectorSource();
305
+ const selected = shallowRef(initialSelector(context.connection.value?.state ?? emptyState));
306
+ let unsubscribe;
307
+ const stop = watch([context.connection, selectorSource, equalsSource], ([target, selector, equals]) => {
308
+ unsubscribe?.();
309
+ unsubscribe = undefined;
310
+ const next = selector(target?.state ?? emptyState);
311
+ if (!equals(selected.value, next))
312
+ selected.value = next;
313
+ if (target !== null) {
314
+ unsubscribe = target.subscribeForm(selector, (value) => {
315
+ selected.value = value;
316
+ }, { equals });
317
+ }
318
+ }, { immediate: true, flush: 'sync' });
319
+ onScopeDispose(() => {
320
+ unsubscribe?.();
321
+ stop();
322
+ });
323
+ return selected;
324
+ }
325
+ function useFormFieldSelectorFromContext(context, idSource, selectorSource, equalsSource = () => Object.is) {
326
+ const initialSelector = selectorSource();
327
+ const selected = shallowRef(initialSelector(context.connection.value?.getField(idSource()) ?? null));
328
+ let unsubscribe;
329
+ const stop = watch([context.connection, idSource, selectorSource, equalsSource], ([target, id, selector, equals]) => {
330
+ unsubscribe?.();
331
+ unsubscribe = undefined;
332
+ const next = selector(target?.getField(id) ?? null);
333
+ if (!equals(selected.value, next))
334
+ selected.value = next;
335
+ if (target !== null) {
336
+ unsubscribe = target.subscribeField(id, selector, (value) => {
337
+ selected.value = value;
338
+ }, { equals });
339
+ }
340
+ }, { immediate: true, flush: 'sync' });
341
+ onScopeDispose(() => {
342
+ unsubscribe?.();
343
+ stop();
344
+ });
345
+ return selected;
346
+ }
347
+ export function useFormSelector(selector, options = {}) {
348
+ const context = useFormContext('useFormSelector');
349
+ return useFormSelectorFromContext(context, () => selector, () => options.equals ?? Object.is);
350
+ }
351
+ export function useFormFieldSelector(id, selector, options = {}) {
352
+ const context = useFormContext('useFormFieldSelector');
353
+ return useFormFieldSelectorFromContext(context, () => id, () => selector, () => options.equals ?? Object.is);
354
+ }
355
+ export function useFormFieldController(id) {
356
+ const context = useFormContext('useFormFieldController');
357
+ const state = useFormFieldSelectorFromContext(context, () => id, () => (field) => field);
358
+ return Object.freeze({
359
+ state,
360
+ setMeta: (meta) => (context.connection.value?.setFieldMeta(id, meta) ?? false),
361
+ replaceIssues: (source, issues) => (context.connection.value?.replaceFieldIssues(id, source, issues) ?? false),
362
+ upsertIssue: (issue) => (context.connection.value?.upsertFieldIssue(id, issue) ?? false),
363
+ removeIssue: (issueId) => (context.connection.value?.removeFieldIssue(id, issueId) ?? false),
364
+ clearIssues: (source) => (context.connection.value?.clearFieldIssues(id, source) ?? false),
365
+ });
366
+ }
367
+ const FormSelectorRuntime = defineComponent({
368
+ name: 'SectileFormSelector',
369
+ props: {
370
+ select: { type: Function, required: true },
371
+ equals: { type: Function, default: Object.is },
372
+ },
373
+ slots: Object,
374
+ setup(props, { slots }) {
375
+ const context = useFormContext('FormSelector');
376
+ const selected = useFormSelectorFromContext(context, () => props.select, () => props.equals);
377
+ return () => slots['default']?.({ selected: selected.value }) ?? [];
378
+ },
379
+ });
380
+ export const FormSelector = FormSelectorRuntime;
381
+ const FormFieldSelectorRuntime = defineComponent({
382
+ name: 'SectileFormFieldSelector',
383
+ props: {
384
+ id: { type: String, required: true },
385
+ select: { type: Function, required: true },
386
+ equals: { type: Function, default: Object.is },
387
+ },
388
+ slots: Object,
389
+ setup(props, { slots }) {
390
+ const context = useFormContext('FormFieldSelector');
391
+ const selected = useFormFieldSelectorFromContext(context, () => props.id, () => props.select, () => props.equals);
392
+ return () => slots['default']?.({ selected: selected.value }) ?? [];
393
+ },
394
+ });
395
+ export const FormFieldSelector = FormFieldSelectorRuntime;
253
396
  export const FormField = defineComponent({
254
397
  name: 'SectileFormField',
255
398
  inheritAttrs: false,
@@ -267,16 +410,21 @@ export const FormField = defineComponent({
267
410
  const formContext = useFormContext('FormField');
268
411
  const generatedId = useHostId();
269
412
  const id = computed(() => props.id ?? `form-field-${generatedId}`);
270
- const nameKey = computed(() => (props.name === undefined ? undefined : encodeFormFieldPath(props.name)));
271
- const root = ref(null);
413
+ const nameKey = computed(() => (props.name === undefined ? undefined : safeEncodeFormFieldPath(props.name) ?? undefined));
414
+ const root = shallowRef(null);
272
415
  const controls = shallowRef([]);
273
416
  const fallback = shallowRef(null);
417
+ const nativeFallbackProblem = shallowRef(null);
274
418
  const appliedAttributes = new Map();
275
419
  let observer;
276
420
  let unregister;
421
+ let diagnosticId;
422
+ let observedTargets = [];
277
423
  let warnedMultipleControls = false;
278
- const activeControl = computed(() => (controls.value.find((registration) => resolveElement(registration.element) !== null)
279
- ?? fallback.value));
424
+ const activeRegistrations = computed(() => controls.value.filter((registration) => resolveElement(registration.element) !== null));
425
+ const activeControl = computed(() => (activeRegistrations.value.length === 1
426
+ ? activeRegistrations.value[0]
427
+ : controls.value.length === 0 ? fallback.value : null));
280
428
  const labelMode = computed(() => activeControl.value?.labelMode ?? 'for');
281
429
  const semanticControl = () => {
282
430
  const registration = activeControl.value;
@@ -285,7 +433,14 @@ export const FormField = defineComponent({
285
433
  return resolveElement(registration.semanticControl ?? registration.element);
286
434
  };
287
435
  const effectiveControlId = computed(() => (semanticControl()?.getAttribute('id')?.trim() || `${id.value}-control`));
288
- const fieldState = computed(() => formContext.state.value.fields.find((field) => field.id === id.value));
436
+ const fieldState = useFormFieldSelectorFromContext(formContext, () => id.value, () => (field) => field);
437
+ const fieldActions = Object.freeze({
438
+ setMeta: (meta) => (formContext.connection.value?.setFieldMeta(id.value, meta) ?? false),
439
+ replaceIssues: (source, issues) => (formContext.connection.value?.replaceFieldIssues(id.value, source, issues) ?? false),
440
+ upsertIssue: (issue) => (formContext.connection.value?.upsertFieldIssue(id.value, issue) ?? false),
441
+ removeIssue: (issueId) => formContext.connection.value?.removeFieldIssue(id.value, issueId) ?? false,
442
+ clearIssues: (source) => (formContext.connection.value?.clearFieldIssues(id.value, source) ?? false),
443
+ });
289
444
  const slotProps = computed(() => {
290
445
  const current = fieldState.value;
291
446
  const descriptionId = `${id.value}-description`;
@@ -303,6 +458,7 @@ export const FormField = defineComponent({
303
458
  touched: current?.touched ?? false,
304
459
  dirty: current?.dirty ?? false,
305
460
  issues: current?.issues ?? [],
461
+ ...fieldActions,
306
462
  });
307
463
  });
308
464
  const attributesFor = (registration) => {
@@ -336,6 +492,13 @@ export const FormField = defineComponent({
336
492
  assign('readonly', true);
337
493
  else if (labelMode.value !== 'for' && props.readonly === true)
338
494
  assign('aria-readonly', 'true');
495
+ if (labelMode.value !== 'for') {
496
+ const refreshValue = () => {
497
+ queueMicrotask(() => formContext.connection.value?.refreshParticipant(id.value));
498
+ };
499
+ attributes['onClick'] = refreshValue;
500
+ attributes['onKeydown'] = refreshValue;
501
+ }
339
502
  return Object.freeze(attributes);
340
503
  };
341
504
  const restoreControlAttributes = () => {
@@ -365,8 +528,10 @@ export const FormField = defineComponent({
365
528
  const capabilities = submission.capabilities ?? nativeSubmissionCapabilities(element);
366
529
  const explicit = submission.explicit ?? registration.explicit;
367
530
  const attributes = {};
368
- if (capabilities.name === true && props.name !== undefined) {
369
- attributes['name'] = encodeSubmissionName(props.name, submission.relativeName);
531
+ if (capabilities.name === true && nameKey.value !== undefined) {
532
+ const submissionName = safeEncodeSubmissionName(nameKey.value, submission.relativeName);
533
+ if (submissionName !== null)
534
+ attributes['name'] = submissionName;
370
535
  }
371
536
  if (capabilities.form === true && props.form !== undefined)
372
537
  attributes['form'] = props.form;
@@ -382,47 +547,98 @@ export const FormField = defineComponent({
382
547
  const discoverNativeFallback = () => {
383
548
  if (controls.value.length > 0 || root.value === null) {
384
549
  fallback.value = null;
550
+ nativeFallbackProblem.value = null;
385
551
  return;
386
552
  }
387
553
  const candidates = nativeCandidates(root.value);
388
- const semantic = nativeSemanticControl(candidates);
389
- if (semantic === undefined) {
554
+ const discovered = createNativeFallbackRegistration(root.value, candidates);
555
+ nativeFallbackProblem.value = discovered.problem;
556
+ if (discovered.registration === null) {
390
557
  fallback.value = null;
391
558
  return;
392
559
  }
393
- const submissions = candidates.filter(isFormSubmissionElement).map((element) => ({
394
- element: () => element,
395
- capabilities: nativeSubmissionCapabilities(element),
396
- }));
560
+ const next = discovered.registration;
397
561
  const current = fallback.value;
398
562
  if (current !== null
399
- && resolveElement(current.semanticControl ?? current.element) === semantic
400
- && sameSubmissionElements(resolveSubmissionRegistrations(current), submissions))
563
+ && resolveElement(current.semanticControl ?? current.element)
564
+ === resolveElement(next.semanticControl ?? next.element)
565
+ && sameSubmissionElements(resolveSubmissionRegistrations(current), resolveSubmissionRegistrations(next)))
401
566
  return;
402
- fallback.value = {
403
- element: () => semantic,
404
- semanticControl: () => semantic,
405
- focusTarget: () => semantic,
406
- submissions,
407
- labelMode: nativeLabelMode(semantic),
408
- capabilities: nativeControlCapabilities(semantic),
409
- };
567
+ fallback.value = next;
568
+ };
569
+ const syncDiagnostic = () => {
570
+ const currentId = id.value;
571
+ if (diagnosticId !== undefined && diagnosticId !== currentId) {
572
+ formContext.setFieldDiagnostic(diagnosticId, null);
573
+ }
574
+ diagnosticId = currentId;
575
+ let message = null;
576
+ if (props.name !== undefined && nameKey.value === undefined) {
577
+ message = 'FormField received an invalid field path.';
578
+ }
579
+ else if (activeRegistrations.value.length > 1) {
580
+ message = 'FormField received multiple active controls. Register one composite control with explicit targets.';
581
+ }
582
+ else if (controls.value.length > 0 && activeRegistrations.value.length === 0) {
583
+ message = 'FormField has no mounted control target.';
584
+ }
585
+ else if (nativeFallbackProblem.value !== null) {
586
+ message = nativeFallbackProblem.value;
587
+ }
588
+ else {
589
+ const registration = activeControl.value;
590
+ if (registration !== null && registration !== undefined) {
591
+ const invalidRelativeName = resolveSubmissionRegistrations(registration).some((submission) => submission.relativeName !== undefined
592
+ && nameKey.value !== undefined
593
+ && safeEncodeSubmissionName(nameKey.value, submission.relativeName) === null);
594
+ if (invalidRelativeName)
595
+ message = 'FormField received an invalid relative submission path.';
596
+ }
597
+ }
598
+ formContext.setFieldDiagnostic(currentId, message === null ? null : Object.freeze({
599
+ id: `${currentId}:composition`,
600
+ fieldId: currentId,
601
+ message,
602
+ source: 'field',
603
+ }));
604
+ };
605
+ const refreshParticipantTargets = () => {
606
+ const registration = activeControl.value;
607
+ const next = registration === null || registration === undefined
608
+ ? root.value === null ? [] : [root.value]
609
+ : [
610
+ root.value,
611
+ resolveElement(registration.semanticControl ?? registration.element),
612
+ resolveElement(registration.focusTarget ?? registration.element),
613
+ resolveElement(registration.validationTarget ?? registration.element),
614
+ ...resolveSubmissionRegistrations(registration).map((submission) => resolveElement(submission.element)),
615
+ ].filter((element) => element !== null);
616
+ if (next.length === observedTargets.length
617
+ && next.every((element, index) => element === observedTargets[index]))
618
+ return;
619
+ observedTargets = Object.freeze([...next]);
620
+ formContext.connection.value?.refreshParticipant(id.value);
410
621
  };
411
622
  const registerControl = (registration) => {
412
623
  controls.value = [...controls.value, registration];
413
624
  fallback.value = null;
414
625
  void nextTick(() => {
415
- const activeRegistrations = controls.value.filter((candidate) => resolveElement(candidate.element) !== null);
416
- if (!warnedMultipleControls && activeRegistrations.length > 1) {
626
+ if (!warnedMultipleControls && activeRegistrations.value.length > 1) {
417
627
  warnedMultipleControls = true;
418
628
  console.warn('[Sectile] FormField received multiple active control registrations. Register one composite control with explicit semantic, focus, and submission targets.');
419
629
  }
420
630
  applyControlAttributes();
631
+ syncDiagnostic();
632
+ refreshParticipantTargets();
421
633
  });
422
634
  return () => {
423
635
  controls.value = controls.value.filter((candidate) => candidate !== registration);
424
636
  discoverNativeFallback();
425
- void nextTick(applyControlAttributes);
637
+ void nextTick(() => {
638
+ applyControlAttributes();
639
+ syncDiagnostic();
640
+ refreshParticipantTargets();
641
+ });
426
642
  };
427
643
  };
428
644
  const mount = () => {
@@ -435,6 +651,8 @@ export const FormField = defineComponent({
435
651
  observer = new MutationObserver(() => {
436
652
  discoverNativeFallback();
437
653
  applyControlAttributes();
654
+ syncDiagnostic();
655
+ refreshParticipantTargets();
438
656
  });
439
657
  observer.observe(root.value, { childList: true, subtree: true });
440
658
  applyControlAttributes();
@@ -449,6 +667,14 @@ export const FormField = defineComponent({
449
667
  : resolveElement(registration.focusTarget ?? registration.semanticControl ?? registration.element)
450
668
  ?? root.value;
451
669
  },
670
+ get validationTarget() {
671
+ const registration = activeControl.value;
672
+ return registration === undefined || registration === null
673
+ ? root.value
674
+ : resolveElement(registration.validationTarget
675
+ ?? registration.semanticControl
676
+ ?? registration.element) ?? root.value;
677
+ },
452
678
  get submissionElements() {
453
679
  const registration = activeControl.value;
454
680
  if (registration === undefined || registration === null)
@@ -465,34 +691,55 @@ export const FormField = defineComponent({
465
691
  control?.focus();
466
692
  return control !== null && document.activeElement === control;
467
693
  },
468
- ...(props.name === undefined ? {} : { name: props.name }),
694
+ reset: () => activeControl.value?.reset?.(),
695
+ get getValue() { return activeControl.value?.getValue; },
696
+ get isValueEqual() { return activeControl.value?.isValueEqual; },
697
+ ...(nameKey.value === undefined ? {} : { name: nameKey.value }),
469
698
  };
470
699
  unregister = formContext.register(participant);
700
+ observedTargets = Object.freeze([
701
+ participant.element,
702
+ participant.semanticControl ?? participant.element,
703
+ participant.focusTarget ?? participant.element,
704
+ participant.validationTarget ?? participant.element,
705
+ ...(participant.submissionElements ?? []),
706
+ ]);
707
+ syncDiagnostic();
471
708
  };
472
- onMounted(() => { void nextTick(mount); });
709
+ const mountTask = useNextTickTask(mount);
710
+ onMounted(mountTask.schedule);
473
711
  onBeforeUnmount(() => {
712
+ mountTask.cancel();
474
713
  observer?.disconnect();
714
+ observer = undefined;
475
715
  restoreControlAttributes();
716
+ if (diagnosticId !== undefined)
717
+ formContext.setFieldDiagnostic(diagnosticId, null);
476
718
  unregister?.();
719
+ unregister = undefined;
477
720
  });
478
- watch([id, nameKey], () => { void nextTick(mount); });
721
+ watch([id, nameKey], mountTask.schedule);
479
722
  watch([
480
723
  activeControl,
481
724
  effectiveControlId,
482
725
  labelMode,
483
- () => props.name,
726
+ nameKey,
484
727
  () => props.form,
485
728
  () => props.required,
486
729
  () => props.disabled,
487
730
  () => props.readonly,
488
731
  () => slotProps.value.valid,
489
- ], applyControlAttributes, { flush: 'post' });
732
+ ], () => {
733
+ applyControlAttributes();
734
+ syncDiagnostic();
735
+ }, { flush: 'post' });
490
736
  provide(formFieldContextKey, {
491
737
  slotProps,
492
738
  labelMode,
493
739
  registerControl,
494
740
  attributesFor,
495
741
  });
742
+ provideFormControlFieldContext({ registerControl, attributesFor });
496
743
  return () => h(Primitive, mergeProps(attrs, {
497
744
  as: props.as,
498
745
  asChild: props.asChild,
@@ -563,7 +810,8 @@ export const FormSummary = defineComponent({
563
810
  slots: Object,
564
811
  setup(props, { attrs, slots }) {
565
812
  const form = useFormContext('FormSummary');
566
- const slotProps = useRootSlotProps(form);
813
+ const valid = useFormSelectorFromContext(form, () => (state) => state.valid);
814
+ const slotProps = computed(() => Object.freeze({ valid: valid.value }));
567
815
  return () => h(Primitive, mergeProps(attrs, {
568
816
  as: props.as,
569
817
  asChild: props.asChild,
@@ -571,7 +819,7 @@ export const FormSummary = defineComponent({
571
819
  role: 'alert',
572
820
  'aria-live': 'polite',
573
821
  tabindex: -1,
574
- hidden: form.state.value.valid,
822
+ hidden: valid.value,
575
823
  'data-scope': 'form',
576
824
  'data-part': 'summary',
577
825
  }), { default: () => slots['default']?.(slotProps.value) });
@@ -581,55 +829,54 @@ export const FormReset = defineComponent({
581
829
  name: 'SectileFormReset', inheritAttrs: false,
582
830
  props: { ...partProps, as: { ...partProps.as, default: 'button' } },
583
831
  setup(props, { attrs, slots }) {
584
- const form = useFormContext('FormReset');
585
- const slotProps = useRootSlotProps(form);
586
832
  return () => h(Primitive, mergeProps(attrs, {
587
833
  as: props.as,
588
834
  asChild: props.asChild,
589
835
  ...(props.as === 'button' && !props.asChild ? { type: 'reset' } : {}),
590
836
  'data-scope': 'form',
591
837
  'data-part': 'reset',
592
- 'data-validation-status': form.state.value.validationStatus,
593
- 'data-submission-status': form.state.value.submissionStatus,
594
- }), { default: () => slots['default']?.(slotProps.value) });
838
+ }), { default: () => slots['default']?.() });
595
839
  },
596
840
  });
597
841
  export const FormSubmit = defineComponent({
598
842
  name: 'SectileFormSubmit', inheritAttrs: false,
599
843
  props: { ...partProps, as: { ...partProps.as, default: 'button' } },
844
+ slots: Object,
600
845
  setup(props, { attrs, slots }) {
601
846
  const form = useFormContext('FormSubmit');
602
- const slotProps = useRootSlotProps(form);
603
- return () => h(Primitive, mergeProps(attrs, {
847
+ const valid = useFormSelectorFromContext(form, () => (state) => state.valid);
848
+ const submissionStatus = useFormSelectorFromContext(form, () => (state) => state.submissionStatus);
849
+ const slotProps = computed(() => {
850
+ const submitting = submissionStatus.value === 'submitting';
851
+ return Object.freeze({
852
+ valid: valid.value,
853
+ submitting,
854
+ canSubmit: valid.value && !submitting,
855
+ submissionStatus: submissionStatus.value,
856
+ });
857
+ });
858
+ const blockPendingSubmit = (event) => {
859
+ if (!slotProps.value.submitting)
860
+ return;
861
+ event.preventDefault();
862
+ event.stopImmediatePropagation();
863
+ };
864
+ return () => h(Primitive, mergeProps({
865
+ onClickCapture: blockPendingSubmit,
866
+ }, attrs, {
604
867
  as: props.as,
605
868
  asChild: props.asChild,
606
869
  ...(props.as === 'button' && !props.asChild ? { type: 'submit' } : {}),
870
+ ...(props.as === 'button' && !props.asChild && slotProps.value.submitting
871
+ ? { disabled: true }
872
+ : {}),
873
+ ...(props.asChild && slotProps.value.submitting ? { 'aria-disabled': 'true' } : {}),
607
874
  'data-scope': 'form',
608
875
  'data-part': 'submit',
609
- 'data-validation-status': form.state.value.validationStatus,
610
- 'data-submission-status': form.state.value.submissionStatus,
876
+ 'data-submission-status': slotProps.value.submissionStatus,
611
877
  }), { default: () => slots['default']?.(slotProps.value) });
612
878
  },
613
879
  });
614
- function useRootSlotProps(form) {
615
- return computed(() => Object.freeze({
616
- state: form.state.value,
617
- validationStatus: form.state.value.validationStatus,
618
- validationTrigger: form.state.value.validationTrigger,
619
- validationIntent: form.state.value.validationIntent,
620
- submissionStatus: form.state.value.submissionStatus,
621
- valid: form.state.value.valid,
622
- touched: form.state.value.touched,
623
- dirty: form.state.value.dirty,
624
- submitted: form.state.value.submitted,
625
- submitCount: form.state.value.submitCount,
626
- submitStarted: () => form.connection.value?.submitStarted() ?? null,
627
- submitSucceeded: (generation) => form.connection.value?.submitSucceeded(generation) ?? false,
628
- submitFailed: (generation, issues = []) => form.connection.value?.submitFailed(generation, issues) ?? false,
629
- replaceIssues: (source, issues) => form.connection.value?.replaceIssues(source, issues) ?? false,
630
- reset: () => form.connection.value?.reset(),
631
- }));
632
- }
633
880
  function renderFieldPart(props, attrs, slots, part, attributes) {
634
881
  return h(Primitive, mergeProps(attributes, attrs, {
635
882
  as: props.as,
@@ -638,85 +885,6 @@ function renderFieldPart(props, attrs, slots, part, attributes) {
638
885
  'data-part': part,
639
886
  }), { default: () => slots.default?.() });
640
887
  }
641
- export function useFormControl(registration) {
642
- const field = inject(formFieldContextKey, null);
643
- const owned = inject(formControlOwnerKey, false);
644
- const instance = getCurrentInstance();
645
- const explicit = Object.freeze([
646
- ...new Set([
647
- ...(registration.explicit ?? []),
648
- ...explicitMetadataAttributes(instance?.vnode.props ?? null),
649
- ]),
650
- ]);
651
- const normalized = Object.freeze({ ...registration, explicit });
652
- const controlProps = computed(() => (field !== null && !owned ? field.attributesFor(normalized) : Object.freeze({})));
653
- let unregister;
654
- if (field !== null && !owned) {
655
- onMounted(() => { unregister = field.registerControl(normalized); });
656
- onBeforeUnmount(() => unregister?.());
657
- }
658
- return Object.freeze({ participating: field !== null && !owned, controlProps });
659
- }
660
- export const nativeInputControlCapabilities = Object.freeze({
661
- id: true,
662
- describedBy: true,
663
- invalid: true,
664
- required: true,
665
- disabled: true,
666
- readonly: true,
667
- });
668
- export const compositeControlCapabilities = Object.freeze({
669
- id: true,
670
- describedBy: true,
671
- invalid: true,
672
- labelledBy: true,
673
- });
674
- export const hiddenInputSubmissionCapabilities = Object.freeze({
675
- name: true,
676
- form: true,
677
- required: true,
678
- disabled: true,
679
- });
680
- export const hiddenSelectSubmissionCapabilities = hiddenInputSubmissionCapabilities;
681
- export const hiddenValueSubmissionCapabilities = Object.freeze({
682
- name: true,
683
- form: true,
684
- disabled: true,
685
- });
686
- export function useNativeInputFormControl(element) {
687
- return useFormControl({
688
- element: element,
689
- semanticControl: element,
690
- focusTarget: element,
691
- labelMode: 'for',
692
- capabilities: nativeInputControlCapabilities,
693
- });
694
- }
695
- export function useCompositeFormControl(options) {
696
- return useFormControl({
697
- element: options.root,
698
- semanticControl: options.root,
699
- focusTarget: options.focusTarget ?? options.root,
700
- labelMode: options.labelMode ?? 'labelledby',
701
- capabilities: compositeControlCapabilities,
702
- ...(options.submissions === undefined ? {} : { submissions: options.submissions }),
703
- });
704
- }
705
- export function provideFormControlOwner() {
706
- provide(formControlOwnerKey, true);
707
- }
708
- export function createTypedForm() {
709
- return Object.freeze({
710
- Root: FormRoot,
711
- Field: FormField,
712
- Label: FormLabel,
713
- Description: FormDescription,
714
- Message: FormMessage,
715
- Summary: FormSummary,
716
- Reset: FormReset,
717
- Submit: FormSubmit,
718
- });
719
- }
720
888
  function useFormContext(part) {
721
889
  const context = inject(formContextKey, null);
722
890
  if (context === null)
@@ -730,7 +898,7 @@ function useFormFieldContext(part) {
730
898
  return context;
731
899
  }
732
900
  function resolveElement(source) {
733
- return typeof source === 'function' ? source() : source.value;
901
+ return typeof source === 'function' ? source() : source.value ?? null;
734
902
  }
735
903
  function resolveSubmissionRegistrations(registration) {
736
904
  if (registration.submissions !== undefined) {
@@ -751,16 +919,91 @@ function sameSubmissionElements(left, right) {
751
919
  return left.length === right.length && left.every((submission, index) => (resolveElement(submission.element) === resolveElement(right[index].element)));
752
920
  }
753
921
  function nativeCandidates(root) {
754
- const selector = 'button, fieldset, input, select, textarea';
922
+ const selector = 'fieldset, input, select, textarea';
755
923
  const candidates = [
756
924
  ...(root.matches(selector) ? [root] : []),
757
925
  ...root.querySelectorAll(selector),
758
926
  ];
759
927
  return candidates.filter((candidate) => (candidate.closest('[data-scope="form"][data-part="field"]') === root));
760
928
  }
929
+ function createNativeFallbackRegistration(root, candidates) {
930
+ const semantic = nativeSemanticControl(candidates);
931
+ if (semantic === undefined || isHiddenInput(semantic)) {
932
+ return {
933
+ registration: null,
934
+ problem: 'FormField has no visible native semantic or focus target.',
935
+ };
936
+ }
937
+ const submissions = candidates.filter(isFormSubmissionElement);
938
+ const visible = candidates.filter((candidate) => !isHiddenInput(candidate));
939
+ const fieldset = visible.find((candidate) => candidate.tagName === 'FIELDSET');
940
+ if (fieldset !== undefined) {
941
+ return {
942
+ registration: nativeFallbackForElement(fieldset, submissions),
943
+ problem: null,
944
+ };
945
+ }
946
+ if (visible.length === 1) {
947
+ return {
948
+ registration: nativeFallbackForElement(semantic, submissions),
949
+ problem: null,
950
+ };
951
+ }
952
+ if (isNativeCheckedGroup(visible)) {
953
+ const focusTarget = visible.find((candidate) => !candidate.disabled) ?? visible[0];
954
+ return {
955
+ registration: {
956
+ element: () => root,
957
+ semanticControl: () => root,
958
+ focusTarget: () => focusTarget,
959
+ validationTarget: () => root,
960
+ submissions: submissions.map((element) => ({
961
+ element: () => element,
962
+ capabilities: Object.freeze({
963
+ ...nativeSubmissionCapabilities(element),
964
+ required: false,
965
+ }),
966
+ })),
967
+ labelMode: 'labelledby',
968
+ capabilities: compositeControlCapabilities,
969
+ },
970
+ problem: null,
971
+ };
972
+ }
973
+ return {
974
+ registration: null,
975
+ problem: 'FormField contains multiple unrelated native controls. Use a fieldset or one composite registration.',
976
+ };
977
+ }
978
+ function nativeFallbackForElement(semantic, submissions) {
979
+ return {
980
+ element: () => semantic,
981
+ semanticControl: () => semantic,
982
+ focusTarget: () => semantic,
983
+ validationTarget: () => semantic,
984
+ submissions: submissions.map((element) => ({
985
+ element: () => element,
986
+ capabilities: nativeSubmissionCapabilities(element),
987
+ })),
988
+ labelMode: nativeLabelMode(semantic),
989
+ capabilities: nativeControlCapabilities(semantic),
990
+ };
991
+ }
992
+ function isHiddenInput(element) {
993
+ return element.tagName === 'INPUT'
994
+ && element.getAttribute('type')?.toLowerCase() === 'hidden';
995
+ }
996
+ function isNativeCheckedGroup(candidates) {
997
+ if (candidates.length < 2)
998
+ return false;
999
+ const types = candidates.map((candidate) => (candidate.tagName === 'INPUT'
1000
+ ? candidate.type.toLowerCase()
1001
+ : null));
1002
+ return types.every((type) => type === types[0])
1003
+ && (types[0] === 'checkbox' || types[0] === 'radio');
1004
+ }
761
1005
  function nativeSemanticControl(candidates) {
762
- return candidates.find((candidate) => (candidate.tagName !== 'INPUT'
763
- || candidate.getAttribute('type')?.toLowerCase() !== 'hidden')) ?? candidates[0];
1006
+ return candidates.find((candidate) => !isHiddenInput(candidate)) ?? candidates[0];
764
1007
  }
765
1008
  function isFormSubmissionElement(element) {
766
1009
  return ['INPUT', 'SELECT', 'TEXTAREA'].includes(element.tagName);
@@ -798,14 +1041,21 @@ function nativeSubmissionCapabilities(element) {
798
1041
  readonly: tag === 'TEXTAREA' || (tag === 'INPUT' && supportsReadonly(element)),
799
1042
  });
800
1043
  }
801
- function encodeSubmissionName(base, relative) {
802
- if (relative === undefined)
803
- return encodeFormFieldPath(base);
804
- return encodeFormFieldPath(appendFormFieldPath(base, relative));
1044
+ function safeEncodeSubmissionName(base, relative) {
1045
+ try {
1046
+ if (relative === undefined)
1047
+ return safeEncodeFormFieldPath(base);
1048
+ return safeEncodeFormFieldPath(appendFormFieldPath(base, relative));
1049
+ }
1050
+ catch {
1051
+ return null;
1052
+ }
805
1053
  }
806
1054
  function applyMetadata(element, attributes, explicitAttributes, applied) {
807
1055
  const explicit = new Set(explicitAttributes ?? []);
808
1056
  for (const [name, value] of Object.entries(attributes)) {
1057
+ if (name.startsWith('on') && typeof value === 'function')
1058
+ continue;
809
1059
  if (value === undefined || value === false || explicit.has(name))
810
1060
  continue;
811
1061
  const previous = element.getAttribute(name);
@@ -825,28 +1075,6 @@ function applyMetadata(element, attributes, explicitAttributes, applied) {
825
1075
  element.setAttribute(name, next);
826
1076
  }
827
1077
  }
828
- function explicitMetadataAttributes(vnodeProps) {
829
- if (vnodeProps === null)
830
- return [];
831
- const aliases = {
832
- id: ['id'],
833
- name: ['name'],
834
- form: ['form'],
835
- required: ['required'],
836
- disabled: ['disabled'],
837
- readonly: ['readonly', 'readOnly'],
838
- 'aria-describedby': ['aria-describedby', 'ariaDescribedby'],
839
- 'aria-errormessage': ['aria-errormessage', 'ariaErrormessage'],
840
- 'aria-invalid': ['aria-invalid', 'ariaInvalid'],
841
- 'aria-labelledby': ['aria-labelledby', 'ariaLabelledby'],
842
- 'aria-disabled': ['aria-disabled', 'ariaDisabled'],
843
- 'aria-required': ['aria-required', 'ariaRequired'],
844
- 'aria-readonly': ['aria-readonly', 'ariaReadonly'],
845
- };
846
- return Object.entries(aliases)
847
- .filter(([, names]) => names.some((name) => Object.hasOwn(vnodeProps, name)))
848
- .map(([attribute]) => attribute);
849
- }
850
1078
  function supportsReadonly(element) {
851
1079
  return !new Set([
852
1080
  'button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range',