@furystack/shades-common-components 12.3.0 → 12.5.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 (268) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/esm/components/app-bar-link.spec.js +16 -19
  3. package/esm/components/app-bar-link.spec.js.map +1 -1
  4. package/esm/components/app-bar.spec.js +6 -4
  5. package/esm/components/app-bar.spec.js.map +1 -1
  6. package/esm/components/avatar.spec.js +9 -9
  7. package/esm/components/avatar.spec.js.map +1 -1
  8. package/esm/components/breadcrumb.spec.js +2 -2
  9. package/esm/components/breadcrumb.spec.js.map +1 -1
  10. package/esm/components/button-group.d.ts +32 -0
  11. package/esm/components/button-group.d.ts.map +1 -1
  12. package/esm/components/button-group.js +26 -2
  13. package/esm/components/button-group.js.map +1 -1
  14. package/esm/components/button-group.spec.js +127 -11
  15. package/esm/components/button-group.spec.js.map +1 -1
  16. package/esm/components/button.spec.js +4 -4
  17. package/esm/components/button.spec.js.map +1 -1
  18. package/esm/components/cache-view.spec.js +2 -3
  19. package/esm/components/cache-view.spec.js.map +1 -1
  20. package/esm/components/carousel.spec.js +47 -47
  21. package/esm/components/carousel.spec.js.map +1 -1
  22. package/esm/components/circular-progress.spec.js +2 -2
  23. package/esm/components/command-palette/command-palette-input.spec.js +23 -19
  24. package/esm/components/command-palette/command-palette-input.spec.js.map +1 -1
  25. package/esm/components/command-palette/command-palette-suggestion-list.spec.js +27 -27
  26. package/esm/components/command-palette/command-palette-suggestion-list.spec.js.map +1 -1
  27. package/esm/components/command-palette/index.spec.js +64 -51
  28. package/esm/components/command-palette/index.spec.js.map +1 -1
  29. package/esm/components/context-menu/context-menu.spec.js +33 -33
  30. package/esm/components/context-menu/context-menu.spec.js.map +1 -1
  31. package/esm/components/data-grid/body.spec.js +13 -13
  32. package/esm/components/data-grid/body.spec.js.map +1 -1
  33. package/esm/components/data-grid/data-grid-row.spec.js +8 -8
  34. package/esm/components/data-grid/data-grid-row.spec.js.map +1 -1
  35. package/esm/components/data-grid/data-grid.d.ts +40 -2
  36. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  37. package/esm/components/data-grid/data-grid.js +7 -10
  38. package/esm/components/data-grid/data-grid.js.map +1 -1
  39. package/esm/components/data-grid/data-grid.spec.js +71 -28
  40. package/esm/components/data-grid/data-grid.spec.js.map +1 -1
  41. package/esm/components/data-grid/filters/boolean-filter.d.ts +12 -0
  42. package/esm/components/data-grid/filters/boolean-filter.d.ts.map +1 -0
  43. package/esm/components/data-grid/filters/boolean-filter.js +27 -0
  44. package/esm/components/data-grid/filters/boolean-filter.js.map +1 -0
  45. package/esm/components/data-grid/filters/boolean-filter.spec.d.ts +2 -0
  46. package/esm/components/data-grid/filters/boolean-filter.spec.d.ts.map +1 -0
  47. package/esm/components/data-grid/filters/boolean-filter.spec.js +114 -0
  48. package/esm/components/data-grid/filters/boolean-filter.spec.js.map +1 -0
  49. package/esm/components/data-grid/filters/date-filter.d.ts +12 -0
  50. package/esm/components/data-grid/filters/date-filter.d.ts.map +1 -0
  51. package/esm/components/data-grid/filters/date-filter.js +109 -0
  52. package/esm/components/data-grid/filters/date-filter.js.map +1 -0
  53. package/esm/components/data-grid/filters/date-filter.spec.d.ts +2 -0
  54. package/esm/components/data-grid/filters/date-filter.spec.d.ts.map +1 -0
  55. package/esm/components/data-grid/filters/date-filter.spec.js +145 -0
  56. package/esm/components/data-grid/filters/date-filter.spec.js.map +1 -0
  57. package/esm/components/data-grid/filters/enum-filter.d.ts +16 -0
  58. package/esm/components/data-grid/filters/enum-filter.d.ts.map +1 -0
  59. package/esm/components/data-grid/filters/enum-filter.js +72 -0
  60. package/esm/components/data-grid/filters/enum-filter.js.map +1 -0
  61. package/esm/components/data-grid/filters/enum-filter.spec.d.ts +2 -0
  62. package/esm/components/data-grid/filters/enum-filter.spec.d.ts.map +1 -0
  63. package/esm/components/data-grid/filters/enum-filter.spec.js +136 -0
  64. package/esm/components/data-grid/filters/enum-filter.spec.js.map +1 -0
  65. package/esm/components/data-grid/filters/filter-dropdown.d.ts +6 -0
  66. package/esm/components/data-grid/filters/filter-dropdown.d.ts.map +1 -0
  67. package/esm/components/data-grid/filters/filter-dropdown.js +41 -0
  68. package/esm/components/data-grid/filters/filter-dropdown.js.map +1 -0
  69. package/esm/components/data-grid/filters/filter-dropdown.spec.d.ts +2 -0
  70. package/esm/components/data-grid/filters/filter-dropdown.spec.d.ts.map +1 -0
  71. package/esm/components/data-grid/filters/filter-dropdown.spec.js +69 -0
  72. package/esm/components/data-grid/filters/filter-dropdown.spec.js.map +1 -0
  73. package/esm/components/data-grid/filters/filter-styles.d.ts +24 -0
  74. package/esm/components/data-grid/filters/filter-styles.d.ts.map +1 -0
  75. package/esm/components/data-grid/filters/filter-styles.js +25 -0
  76. package/esm/components/data-grid/filters/filter-styles.js.map +1 -0
  77. package/esm/components/data-grid/filters/index.d.ts +7 -0
  78. package/esm/components/data-grid/filters/index.d.ts.map +1 -0
  79. package/esm/components/data-grid/filters/index.js +7 -0
  80. package/esm/components/data-grid/filters/index.js.map +1 -0
  81. package/esm/components/data-grid/filters/number-filter.d.ts +12 -0
  82. package/esm/components/data-grid/filters/number-filter.d.ts.map +1 -0
  83. package/esm/components/data-grid/filters/number-filter.js +65 -0
  84. package/esm/components/data-grid/filters/number-filter.js.map +1 -0
  85. package/esm/components/data-grid/filters/number-filter.spec.d.ts +2 -0
  86. package/esm/components/data-grid/filters/number-filter.spec.d.ts.map +1 -0
  87. package/esm/components/data-grid/filters/number-filter.spec.js +142 -0
  88. package/esm/components/data-grid/filters/number-filter.spec.js.map +1 -0
  89. package/esm/components/data-grid/filters/string-filter.d.ts +12 -0
  90. package/esm/components/data-grid/filters/string-filter.d.ts.map +1 -0
  91. package/esm/components/data-grid/filters/string-filter.js +63 -0
  92. package/esm/components/data-grid/filters/string-filter.js.map +1 -0
  93. package/esm/components/data-grid/filters/string-filter.spec.d.ts +2 -0
  94. package/esm/components/data-grid/filters/string-filter.spec.d.ts.map +1 -0
  95. package/esm/components/data-grid/filters/string-filter.spec.js +128 -0
  96. package/esm/components/data-grid/filters/string-filter.spec.js.map +1 -0
  97. package/esm/components/data-grid/footer.d.ts.map +1 -1
  98. package/esm/components/data-grid/footer.js +24 -9
  99. package/esm/components/data-grid/footer.js.map +1 -1
  100. package/esm/components/data-grid/footer.spec.js +38 -36
  101. package/esm/components/data-grid/footer.spec.js.map +1 -1
  102. package/esm/components/data-grid/header.d.ts +6 -9
  103. package/esm/components/data-grid/header.d.ts.map +1 -1
  104. package/esm/components/data-grid/header.js +51 -117
  105. package/esm/components/data-grid/header.js.map +1 -1
  106. package/esm/components/data-grid/header.spec.js +116 -187
  107. package/esm/components/data-grid/header.spec.js.map +1 -1
  108. package/esm/components/data-grid/index.d.ts +1 -0
  109. package/esm/components/data-grid/index.d.ts.map +1 -1
  110. package/esm/components/data-grid/index.js +1 -0
  111. package/esm/components/data-grid/index.js.map +1 -1
  112. package/esm/components/data-grid/selection-cell.spec.js +8 -8
  113. package/esm/components/data-grid/selection-cell.spec.js.map +1 -1
  114. package/esm/components/drawer/drawer-toggle-button.spec.js +22 -22
  115. package/esm/components/drawer/drawer-toggle-button.spec.js.map +1 -1
  116. package/esm/components/drawer/index.spec.js +36 -36
  117. package/esm/components/drawer/index.spec.js.map +1 -1
  118. package/esm/components/dropdown.spec.js +38 -30
  119. package/esm/components/dropdown.spec.js.map +1 -1
  120. package/esm/components/fab.spec.js +4 -4
  121. package/esm/components/fab.spec.js.map +1 -1
  122. package/esm/components/form.d.ts +5 -2
  123. package/esm/components/form.d.ts.map +1 -1
  124. package/esm/components/form.js +28 -6
  125. package/esm/components/form.js.map +1 -1
  126. package/esm/components/form.spec.js +227 -20
  127. package/esm/components/form.spec.js.map +1 -1
  128. package/esm/components/grid.spec.js +3 -3
  129. package/esm/components/grid.spec.js.map +1 -1
  130. package/esm/components/image.spec.js +55 -52
  131. package/esm/components/image.spec.js.map +1 -1
  132. package/esm/components/inputs/autocomplete.spec.js +7 -14
  133. package/esm/components/inputs/autocomplete.spec.js.map +1 -1
  134. package/esm/components/inputs/checkbox.spec.js +22 -22
  135. package/esm/components/inputs/checkbox.spec.js.map +1 -1
  136. package/esm/components/inputs/input-number.spec.js +47 -47
  137. package/esm/components/inputs/input-number.spec.js.map +1 -1
  138. package/esm/components/inputs/input.spec.js +53 -53
  139. package/esm/components/inputs/input.spec.js.map +1 -1
  140. package/esm/components/inputs/radio-group.spec.js +14 -14
  141. package/esm/components/inputs/radio-group.spec.js.map +1 -1
  142. package/esm/components/inputs/radio.spec.js +16 -16
  143. package/esm/components/inputs/radio.spec.js.map +1 -1
  144. package/esm/components/inputs/select.spec.js +74 -74
  145. package/esm/components/inputs/select.spec.js.map +1 -1
  146. package/esm/components/inputs/slider.spec.js +16 -16
  147. package/esm/components/inputs/slider.spec.js.map +1 -1
  148. package/esm/components/inputs/switch.spec.js +24 -24
  149. package/esm/components/inputs/switch.spec.js.map +1 -1
  150. package/esm/components/inputs/text-area.spec.js +17 -17
  151. package/esm/components/inputs/text-area.spec.js.map +1 -1
  152. package/esm/components/linear-progress.spec.js +2 -2
  153. package/esm/components/list/list.spec.js +36 -36
  154. package/esm/components/list/list.spec.js.map +1 -1
  155. package/esm/components/markdown/markdown-display.spec.js +15 -15
  156. package/esm/components/markdown/markdown-display.spec.js.map +1 -1
  157. package/esm/components/markdown/markdown-editor.spec.js +8 -8
  158. package/esm/components/markdown/markdown-editor.spec.js.map +1 -1
  159. package/esm/components/markdown/markdown-input.spec.js +17 -17
  160. package/esm/components/markdown/markdown-input.spec.js.map +1 -1
  161. package/esm/components/menu/menu.spec.js +28 -28
  162. package/esm/components/menu/menu.spec.js.map +1 -1
  163. package/esm/components/modal.spec.js +15 -18
  164. package/esm/components/modal.spec.js.map +1 -1
  165. package/esm/components/noty-list.spec.js +25 -23
  166. package/esm/components/noty-list.spec.js.map +1 -1
  167. package/esm/components/page-container/index.spec.js +16 -16
  168. package/esm/components/page-container/index.spec.js.map +1 -1
  169. package/esm/components/page-container/page-header.spec.js +16 -16
  170. package/esm/components/page-container/page-header.spec.js.map +1 -1
  171. package/esm/components/page-layout/index.spec.js +29 -29
  172. package/esm/components/page-layout/index.spec.js.map +1 -1
  173. package/esm/components/paper.spec.js +3 -3
  174. package/esm/components/paper.spec.js.map +1 -1
  175. package/esm/components/rating.spec.js +61 -61
  176. package/esm/components/rating.spec.js.map +1 -1
  177. package/esm/components/skeleton.spec.js +10 -6
  178. package/esm/components/skeleton.spec.js.map +1 -1
  179. package/esm/components/suggest/suggest-input.spec.js +4 -10
  180. package/esm/components/suggest/suggest-input.spec.js.map +1 -1
  181. package/esm/components/tabs.spec.js +30 -30
  182. package/esm/components/tabs.spec.js.map +1 -1
  183. package/esm/components/tree/tree.spec.js +27 -27
  184. package/esm/components/tree/tree.spec.js.map +1 -1
  185. package/esm/components/typography.spec.js +3 -3
  186. package/esm/components/typography.spec.js.map +1 -1
  187. package/esm/components/wizard/index.spec.js +5 -5
  188. package/esm/components/wizard/index.spec.js.map +1 -1
  189. package/esm/utils/promisify-animation.d.ts.map +1 -1
  190. package/esm/utils/promisify-animation.js +3 -0
  191. package/esm/utils/promisify-animation.js.map +1 -1
  192. package/package.json +2 -2
  193. package/src/components/app-bar-link.spec.tsx +16 -19
  194. package/src/components/app-bar.spec.tsx +6 -4
  195. package/src/components/avatar.spec.tsx +9 -9
  196. package/src/components/breadcrumb.spec.tsx +2 -2
  197. package/src/components/button-group.spec.tsx +155 -11
  198. package/src/components/button-group.tsx +49 -2
  199. package/src/components/button.spec.tsx +4 -4
  200. package/src/components/cache-view.spec.tsx +3 -3
  201. package/src/components/carousel.spec.tsx +47 -47
  202. package/src/components/circular-progress.spec.tsx +2 -2
  203. package/src/components/command-palette/command-palette-input.spec.tsx +23 -19
  204. package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +27 -27
  205. package/src/components/command-palette/index.spec.tsx +64 -51
  206. package/src/components/context-menu/context-menu.spec.tsx +33 -33
  207. package/src/components/data-grid/body.spec.tsx +13 -13
  208. package/src/components/data-grid/data-grid-row.spec.tsx +8 -8
  209. package/src/components/data-grid/data-grid.spec.tsx +106 -28
  210. package/src/components/data-grid/data-grid.tsx +44 -11
  211. package/src/components/data-grid/filters/boolean-filter.spec.tsx +142 -0
  212. package/src/components/data-grid/filters/boolean-filter.tsx +45 -0
  213. package/src/components/data-grid/filters/date-filter.spec.tsx +181 -0
  214. package/src/components/data-grid/filters/date-filter.tsx +162 -0
  215. package/src/components/data-grid/filters/enum-filter.spec.tsx +168 -0
  216. package/src/components/data-grid/filters/enum-filter.tsx +119 -0
  217. package/src/components/data-grid/filters/filter-dropdown.spec.tsx +89 -0
  218. package/src/components/data-grid/filters/filter-dropdown.tsx +60 -0
  219. package/src/components/data-grid/filters/filter-styles.ts +26 -0
  220. package/src/components/data-grid/filters/index.ts +6 -0
  221. package/src/components/data-grid/filters/number-filter.spec.tsx +174 -0
  222. package/src/components/data-grid/filters/number-filter.tsx +115 -0
  223. package/src/components/data-grid/filters/string-filter.spec.tsx +157 -0
  224. package/src/components/data-grid/filters/string-filter.tsx +112 -0
  225. package/src/components/data-grid/footer.spec.tsx +38 -36
  226. package/src/components/data-grid/footer.tsx +21 -8
  227. package/src/components/data-grid/header.spec.tsx +128 -212
  228. package/src/components/data-grid/header.tsx +95 -183
  229. package/src/components/data-grid/index.tsx +1 -0
  230. package/src/components/data-grid/selection-cell.spec.tsx +8 -8
  231. package/src/components/drawer/drawer-toggle-button.spec.tsx +22 -22
  232. package/src/components/drawer/index.spec.tsx +36 -36
  233. package/src/components/dropdown.spec.tsx +38 -30
  234. package/src/components/fab.spec.tsx +4 -4
  235. package/src/components/form.spec.tsx +329 -20
  236. package/src/components/form.tsx +31 -8
  237. package/src/components/grid.spec.tsx +3 -3
  238. package/src/components/image.spec.tsx +55 -52
  239. package/src/components/inputs/autocomplete.spec.tsx +7 -14
  240. package/src/components/inputs/checkbox.spec.tsx +22 -22
  241. package/src/components/inputs/input-number.spec.tsx +47 -47
  242. package/src/components/inputs/input.spec.tsx +53 -53
  243. package/src/components/inputs/radio-group.spec.tsx +14 -14
  244. package/src/components/inputs/radio.spec.tsx +16 -16
  245. package/src/components/inputs/select.spec.tsx +74 -74
  246. package/src/components/inputs/slider.spec.tsx +16 -16
  247. package/src/components/inputs/switch.spec.tsx +24 -24
  248. package/src/components/inputs/text-area.spec.tsx +17 -17
  249. package/src/components/linear-progress.spec.tsx +2 -2
  250. package/src/components/list/list.spec.tsx +36 -36
  251. package/src/components/markdown/markdown-display.spec.tsx +15 -15
  252. package/src/components/markdown/markdown-editor.spec.tsx +8 -8
  253. package/src/components/markdown/markdown-input.spec.tsx +17 -17
  254. package/src/components/menu/menu.spec.tsx +28 -28
  255. package/src/components/modal.spec.tsx +15 -18
  256. package/src/components/noty-list.spec.tsx +25 -23
  257. package/src/components/page-container/index.spec.tsx +16 -16
  258. package/src/components/page-container/page-header.spec.tsx +16 -16
  259. package/src/components/page-layout/index.spec.tsx +29 -29
  260. package/src/components/paper.spec.tsx +3 -3
  261. package/src/components/rating.spec.tsx +61 -61
  262. package/src/components/skeleton.spec.tsx +10 -6
  263. package/src/components/suggest/suggest-input.spec.tsx +4 -10
  264. package/src/components/tabs.spec.tsx +30 -30
  265. package/src/components/tree/tree.spec.tsx +27 -27
  266. package/src/components/typography.spec.tsx +3 -3
  267. package/src/components/wizard/index.spec.tsx +5 -5
  268. package/src/utils/promisify-animation.ts +3 -0
@@ -1,6 +1,6 @@
1
1
  import { Injector } from '@furystack/inject'
2
- import { createComponent, initializeShadeRoot } from '@furystack/shades'
3
- import { sleepAsync, using, usingAsync } from '@furystack/utils'
2
+ import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
3
+ import { using, usingAsync } from '@furystack/utils'
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
5
  import { Form, FormService } from './form.js'
6
6
 
@@ -35,6 +35,18 @@ describe('FormService', () => {
35
35
  expect(service.inputs.size).toBe(0)
36
36
  })
37
37
  })
38
+
39
+ it('should initialize isSubmitting as false', () => {
40
+ using(new FormService(), (service) => {
41
+ expect(service.isSubmitting.getValue()).toBe(false)
42
+ })
43
+ })
44
+
45
+ it('should initialize submitError as undefined', () => {
46
+ using(new FormService(), (service) => {
47
+ expect(service.submitError.getValue()).toBeUndefined()
48
+ })
49
+ })
38
50
  })
39
51
 
40
52
  describe('setFieldState', () => {
@@ -84,12 +96,18 @@ describe('FormService', () => {
84
96
  const validatedFormDataDisposeSpy = vi.spyOn(service.validatedFormData, Symbol.dispose)
85
97
  const rawFormDataDisposeSpy = vi.spyOn(service.rawFormData, Symbol.dispose)
86
98
  const validationResultDisposeSpy = vi.spyOn(service.validationResult, Symbol.dispose)
99
+ const fieldErrorsDisposeSpy = vi.spyOn(service.fieldErrors, Symbol.dispose)
100
+ const isSubmittingDisposeSpy = vi.spyOn(service.isSubmitting, Symbol.dispose)
101
+ const submitErrorDisposeSpy = vi.spyOn(service.submitError, Symbol.dispose)
87
102
 
88
103
  service[Symbol.dispose]()
89
104
 
90
105
  expect(validatedFormDataDisposeSpy).toHaveBeenCalled()
91
106
  expect(rawFormDataDisposeSpy).toHaveBeenCalled()
92
107
  expect(validationResultDisposeSpy).toHaveBeenCalled()
108
+ expect(fieldErrorsDisposeSpy).toHaveBeenCalled()
109
+ expect(isSubmittingDisposeSpy).toHaveBeenCalled()
110
+ expect(submitErrorDisposeSpy).toHaveBeenCalled()
93
111
  })
94
112
  })
95
113
  })
@@ -126,7 +144,7 @@ describe('Form component', () => {
126
144
  ),
127
145
  })
128
146
 
129
- await sleepAsync(50)
147
+ await flushUpdates()
130
148
 
131
149
  const form = document.querySelector('form[is="shade-form"]')
132
150
  expect(form).not.toBeNull()
@@ -159,7 +177,7 @@ describe('Form component', () => {
159
177
  ),
160
178
  })
161
179
 
162
- await sleepAsync(50)
180
+ await flushUpdates()
163
181
 
164
182
  const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
165
183
  const input = form.querySelector('input[name="name"]') as HTMLInputElement
@@ -168,7 +186,7 @@ describe('Form component', () => {
168
186
  const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
169
187
  form.dispatchEvent(submitEvent)
170
188
 
171
- await sleepAsync(50)
189
+ await flushUpdates()
172
190
 
173
191
  expect(onSubmit).toHaveBeenCalledWith({ name: 'Test Name' })
174
192
  })
@@ -199,7 +217,7 @@ describe('Form component', () => {
199
217
  ),
200
218
  })
201
219
 
202
- await sleepAsync(50)
220
+ await flushUpdates()
203
221
 
204
222
  const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
205
223
  const nameInput = form.querySelector('input[name="name"]') as HTMLInputElement
@@ -211,7 +229,7 @@ describe('Form component', () => {
211
229
  const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
212
230
  form.dispatchEvent(submitEvent)
213
231
 
214
- await sleepAsync(50)
232
+ await flushUpdates()
215
233
 
216
234
  expect(onSubmit).not.toHaveBeenCalled()
217
235
  })
@@ -240,7 +258,7 @@ describe('Form component', () => {
240
258
  ),
241
259
  })
242
260
 
243
- await sleepAsync(50)
261
+ await flushUpdates()
244
262
 
245
263
  const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
246
264
  const input = form.querySelector('input[name="email"]') as HTMLInputElement
@@ -249,7 +267,7 @@ describe('Form component', () => {
249
267
  const changeEvent = new Event('change', { bubbles: true })
250
268
  form.dispatchEvent(changeEvent)
251
269
 
252
- await sleepAsync(50)
270
+ await flushUpdates()
253
271
 
254
272
  const formInjector = (form as unknown as { injector: Injector }).injector
255
273
  const formService = formInjector.getInstance(FormService)
@@ -287,7 +305,7 @@ describe('Form component', () => {
287
305
  ),
288
306
  })
289
307
 
290
- await sleepAsync(50)
308
+ await flushUpdates()
291
309
 
292
310
  const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
293
311
  const input = form.querySelector('input[name="name"]') as HTMLInputElement
@@ -296,7 +314,7 @@ describe('Form component', () => {
296
314
  const changeEvent = new Event('change', { bubbles: true })
297
315
  form.dispatchEvent(changeEvent)
298
316
 
299
- await sleepAsync(50)
317
+ await flushUpdates()
300
318
 
301
319
  const formInjector = (form as unknown as { injector: Injector }).injector
302
320
  const formService = formInjector.getInstance(FormService)
@@ -306,7 +324,7 @@ describe('Form component', () => {
306
324
  const resetEvent = new Event('reset', { bubbles: true })
307
325
  form.dispatchEvent(resetEvent)
308
326
 
309
- await sleepAsync(50)
327
+ await flushUpdates()
310
328
 
311
329
  expect(formService.rawFormData.getValue()).toBeNull()
312
330
  expect(formService.validationResult.getValue()).toEqual({ isValid: null })
@@ -336,7 +354,7 @@ describe('Form component', () => {
336
354
  ),
337
355
  })
338
356
 
339
- await sleepAsync(50)
357
+ await flushUpdates()
340
358
 
341
359
  const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
342
360
  const input = form.querySelector('input[name="username"]') as HTMLInputElement
@@ -345,7 +363,7 @@ describe('Form component', () => {
345
363
  const changeEvent = new Event('change', { bubbles: true })
346
364
  form.dispatchEvent(changeEvent)
347
365
 
348
- await sleepAsync(50)
366
+ await flushUpdates()
349
367
 
350
368
  const formInjector = (form as unknown as { injector: Injector }).injector
351
369
  const formService = formInjector.getInstance(FormService)
@@ -376,7 +394,7 @@ describe('Form component', () => {
376
394
  ),
377
395
  })
378
396
 
379
- await sleepAsync(50)
397
+ await flushUpdates()
380
398
 
381
399
  const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
382
400
  const input = form.querySelector('input[name="title"]') as HTMLInputElement
@@ -385,7 +403,7 @@ describe('Form component', () => {
385
403
  const changeEvent = new Event('change', { bubbles: true })
386
404
  form.dispatchEvent(changeEvent)
387
405
 
388
- await sleepAsync(50)
406
+ await flushUpdates()
389
407
 
390
408
  const formInjector = (form as unknown as { injector: Injector }).injector
391
409
  const formService = formInjector.getInstance(FormService)
@@ -412,7 +430,7 @@ describe('Form component', () => {
412
430
  ),
413
431
  })
414
432
 
415
- await sleepAsync(50)
433
+ await flushUpdates()
416
434
 
417
435
  const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
418
436
 
@@ -441,7 +459,7 @@ describe('Form component', () => {
441
459
  ),
442
460
  })
443
461
 
444
- await sleepAsync(50)
462
+ await flushUpdates()
445
463
 
446
464
  const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
447
465
  const formInjector = (form as unknown as { injector: Injector }).injector
@@ -477,7 +495,7 @@ describe('Form component', () => {
477
495
  ),
478
496
  })
479
497
 
480
- await sleepAsync(50)
498
+ await flushUpdates()
481
499
 
482
500
  const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
483
501
  const input = form.querySelector('input[name="required"]') as HTMLInputElement
@@ -485,7 +503,298 @@ describe('Form component', () => {
485
503
  const invalidEvent = new Event('invalid', { bubbles: true })
486
504
  input.dispatchEvent(invalidEvent)
487
505
 
488
- await sleepAsync(50)
506
+ await flushUpdates()
507
+ })
508
+ })
509
+
510
+ it('should set isSubmitting during async onSubmit and reset after', async () => {
511
+ await usingAsync(new Injector(), async (injector) => {
512
+ const rootElement = document.getElementById('root') as HTMLDivElement
513
+
514
+ let resolveSubmit: () => void
515
+ const submitPromise = new Promise<void>((resolve) => {
516
+ resolveSubmit = resolve
517
+ })
518
+
519
+ type FormData = { name: string }
520
+
521
+ initializeShadeRoot({
522
+ injector,
523
+ rootElement,
524
+ jsxElement: (
525
+ <Form<FormData>
526
+ onSubmit={() => submitPromise}
527
+ validate={(data): data is FormData => {
528
+ const d = data as Record<string, unknown>
529
+ return typeof d.name === 'string'
530
+ }}
531
+ >
532
+ <input name="name" type="text" />
533
+ <button type="submit">Submit</button>
534
+ </Form>
535
+ ),
536
+ })
537
+
538
+ await flushUpdates()
539
+
540
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
541
+ const input = form.querySelector('input[name="name"]') as HTMLInputElement
542
+ input.value = 'Test'
543
+
544
+ const formInjector = (form as unknown as { injector: Injector }).injector
545
+ const formService = formInjector.getInstance(FormService)
546
+
547
+ expect(formService.isSubmitting.getValue()).toBe(false)
548
+
549
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
550
+ form.dispatchEvent(submitEvent)
551
+
552
+ await flushUpdates()
553
+ expect(formService.isSubmitting.getValue()).toBe(true)
554
+
555
+ resolveSubmit!()
556
+ await flushUpdates()
557
+ expect(formService.isSubmitting.getValue()).toBe(false)
558
+ })
559
+ })
560
+
561
+ it('should reset isSubmitting to false and set submitError when onSubmit throws', async () => {
562
+ await usingAsync(new Injector(), async (injector) => {
563
+ const rootElement = document.getElementById('root') as HTMLDivElement
564
+
565
+ type FormData = { name: string }
566
+
567
+ const submitError = new Error('Submit failed')
568
+
569
+ initializeShadeRoot({
570
+ injector,
571
+ rootElement,
572
+ jsxElement: (
573
+ <Form<FormData>
574
+ onSubmit={async () => {
575
+ throw submitError
576
+ }}
577
+ validate={(data): data is FormData => {
578
+ const d = data as Record<string, unknown>
579
+ return typeof d.name === 'string'
580
+ }}
581
+ >
582
+ <input name="name" type="text" />
583
+ <button type="submit">Submit</button>
584
+ </Form>
585
+ ),
586
+ })
587
+
588
+ await flushUpdates()
589
+
590
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
591
+ const input = form.querySelector('input[name="name"]') as HTMLInputElement
592
+ input.value = 'Test'
593
+
594
+ const formInjector = (form as unknown as { injector: Injector }).injector
595
+ const formService = formInjector.getInstance(FormService)
596
+
597
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
598
+ form.dispatchEvent(submitEvent)
599
+
600
+ await flushUpdates()
601
+ expect(formService.isSubmitting.getValue()).toBe(false)
602
+ expect(formService.submitError.getValue()).toBe(submitError)
603
+ })
604
+ })
605
+
606
+ it('should clear submitError before a new submission', async () => {
607
+ await usingAsync(new Injector(), async (injector) => {
608
+ const rootElement = document.getElementById('root') as HTMLDivElement
609
+
610
+ let shouldThrow = true
611
+ let resolveSubmit: () => void
612
+
613
+ type FormData = { name: string }
614
+
615
+ initializeShadeRoot({
616
+ injector,
617
+ rootElement,
618
+ jsxElement: (
619
+ <Form<FormData>
620
+ onSubmit={async () => {
621
+ if (shouldThrow) {
622
+ throw new Error('First submit fails')
623
+ }
624
+ return new Promise<void>((resolve) => {
625
+ resolveSubmit = resolve
626
+ })
627
+ }}
628
+ validate={(data): data is FormData => {
629
+ const d = data as Record<string, unknown>
630
+ return typeof d.name === 'string'
631
+ }}
632
+ >
633
+ <input name="name" type="text" />
634
+ <button type="submit">Submit</button>
635
+ </Form>
636
+ ),
637
+ })
638
+
639
+ await flushUpdates()
640
+
641
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
642
+ const input = form.querySelector('input[name="name"]') as HTMLInputElement
643
+ input.value = 'Test'
644
+
645
+ const formInjector = (form as unknown as { injector: Injector }).injector
646
+ const formService = formInjector.getInstance(FormService)
647
+
648
+ form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
649
+ await flushUpdates()
650
+ expect(formService.submitError.getValue()).toBeInstanceOf(Error)
651
+
652
+ shouldThrow = false
653
+ form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
654
+ await flushUpdates()
655
+ expect(formService.submitError.getValue()).toBeUndefined()
656
+ expect(formService.isSubmitting.getValue()).toBe(true)
657
+
658
+ resolveSubmit!()
659
+ await flushUpdates()
660
+ expect(formService.isSubmitting.getValue()).toBe(false)
661
+ })
662
+ })
663
+
664
+ it('should set inert on form element when disableOnSubmit is true during async submit', async () => {
665
+ await usingAsync(new Injector(), async (injector) => {
666
+ const rootElement = document.getElementById('root') as HTMLDivElement
667
+
668
+ let resolveSubmit: () => void
669
+ const submitPromise = new Promise<void>((resolve) => {
670
+ resolveSubmit = resolve
671
+ })
672
+
673
+ type FormData = { name: string }
674
+
675
+ initializeShadeRoot({
676
+ injector,
677
+ rootElement,
678
+ jsxElement: (
679
+ <Form<FormData>
680
+ onSubmit={() => submitPromise}
681
+ disableOnSubmit
682
+ validate={(data): data is FormData => {
683
+ const d = data as Record<string, unknown>
684
+ return typeof d.name === 'string'
685
+ }}
686
+ >
687
+ <input name="name" type="text" />
688
+ <button type="submit">Submit</button>
689
+ </Form>
690
+ ),
691
+ })
692
+
693
+ await flushUpdates()
694
+
695
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
696
+ const input = form.querySelector('input[name="name"]') as HTMLInputElement
697
+ input.value = 'Test'
698
+
699
+ expect(form.inert).toBeFalsy()
700
+
701
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
702
+ form.dispatchEvent(submitEvent)
703
+
704
+ await flushUpdates()
705
+ expect(form.inert).toBe(true)
706
+
707
+ resolveSubmit!()
708
+ await flushUpdates()
709
+ expect(form.inert).toBe(false)
710
+ })
711
+ })
712
+
713
+ it('should not set inert when disableOnSubmit is not provided', async () => {
714
+ await usingAsync(new Injector(), async (injector) => {
715
+ const rootElement = document.getElementById('root') as HTMLDivElement
716
+
717
+ let resolveSubmit: () => void
718
+ const submitPromise = new Promise<void>((resolve) => {
719
+ resolveSubmit = resolve
720
+ })
721
+
722
+ type FormData = { name: string }
723
+
724
+ initializeShadeRoot({
725
+ injector,
726
+ rootElement,
727
+ jsxElement: (
728
+ <Form<FormData>
729
+ onSubmit={() => submitPromise}
730
+ validate={(data): data is FormData => {
731
+ const d = data as Record<string, unknown>
732
+ return typeof d.name === 'string'
733
+ }}
734
+ >
735
+ <input name="name" type="text" />
736
+ <button type="submit">Submit</button>
737
+ </Form>
738
+ ),
739
+ })
740
+
741
+ await flushUpdates()
742
+
743
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
744
+ const input = form.querySelector('input[name="name"]') as HTMLInputElement
745
+ input.value = 'Test'
746
+
747
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
748
+ form.dispatchEvent(submitEvent)
749
+
750
+ await flushUpdates()
751
+ expect(form.inert).toBeFalsy()
752
+
753
+ resolveSubmit!()
754
+ await flushUpdates()
755
+ })
756
+ })
757
+
758
+ it('should remove inert even if onSubmit throws when disableOnSubmit is true', async () => {
759
+ await usingAsync(new Injector(), async (injector) => {
760
+ const rootElement = document.getElementById('root') as HTMLDivElement
761
+
762
+ type FormData = { name: string }
763
+
764
+ initializeShadeRoot({
765
+ injector,
766
+ rootElement,
767
+ jsxElement: (
768
+ <Form<FormData>
769
+ onSubmit={async () => {
770
+ throw new Error('Submit failed')
771
+ }}
772
+ disableOnSubmit
773
+ validate={(data): data is FormData => {
774
+ const d = data as Record<string, unknown>
775
+ return typeof d.name === 'string'
776
+ }}
777
+ >
778
+ <input name="name" type="text" />
779
+ <button type="submit">Submit</button>
780
+ </Form>
781
+ ),
782
+ })
783
+
784
+ await flushUpdates()
785
+
786
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
787
+ const input = form.querySelector('input[name="name"]') as HTMLInputElement
788
+ input.value = 'Test'
789
+
790
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
791
+ form.dispatchEvent(submitEvent)
792
+
793
+ await flushUpdates()
794
+ expect(form.inert).toBe(false)
795
+ const formInjector = (form as unknown as { injector: Injector }).injector
796
+ const formService = formInjector.getInstance(FormService)
797
+ expect(formService.isSubmitting.getValue()).toBe(false)
489
798
  })
490
799
  })
491
800
  })
@@ -29,6 +29,10 @@ export class FormService<T> {
29
29
 
30
30
  public inputs = new Set<HTMLInputElement>()
31
31
 
32
+ public isSubmitting = new ObservableValue<boolean>(false)
33
+
34
+ public submitError = new ObservableValue<unknown>(undefined)
35
+
32
36
  public setFieldState = (key: keyof T, validationResult: InputValidationResult, validity: ValidityState) => {
33
37
  this.fieldErrors.setValue({ ...this.fieldErrors.getValue(), [key]: { validationResult, validity } })
34
38
  }
@@ -37,13 +41,17 @@ export class FormService<T> {
37
41
  this.validatedFormData[Symbol.dispose]()
38
42
  this.rawFormData[Symbol.dispose]()
39
43
  this.validationResult[Symbol.dispose]()
44
+ this.fieldErrors[Symbol.dispose]()
45
+ this.isSubmitting[Symbol.dispose]()
46
+ this.submitError[Symbol.dispose]()
40
47
  }
41
48
  }
42
49
 
43
50
  type FormProps<T> = {
44
- onSubmit: (formData: T) => void
51
+ onSubmit: (formData: T) => void | Promise<void>
45
52
  onReset?: () => void
46
- validate: (formData: any) => formData is T
53
+ validate: (formData: unknown) => formData is T
54
+ disableOnSubmit?: boolean
47
55
  } & PartialElement<Omit<HTMLFormElement, 'onsubmit' | 'onchange' | 'onreset'>>
48
56
 
49
57
  export const Form: <T>(props: FormProps<T>, children: ChildrenList) => JSX.Element = Shade({
@@ -61,13 +69,14 @@ export const Form: <T>(props: FormProps<T>, children: ChildrenList) => JSX.Eleme
61
69
  // `injector` setter defined on the Shade base class.
62
70
  useHostProps({ injector: formInjector })
63
71
 
64
- const changeHandler = (ev: Event, shouldSubmit?: boolean) => {
72
+ const changeHandler = async (ev: Event, shouldSubmit?: boolean) => {
65
73
  formService.inputs.forEach((i) => {
66
74
  const e = document.createEvent('FocusEvent')
67
75
  e.initEvent('blur', true, true)
68
76
  i.dispatchEvent(e)
69
77
  })
70
- const formData = Object.fromEntries(new FormData(ev.currentTarget as HTMLFormElement).entries())
78
+ const formElement = ev.currentTarget as HTMLFormElement
79
+ const formData = Object.fromEntries(new FormData(formElement).entries())
71
80
  formService.rawFormData.setValue(formData)
72
81
  const currentFieldErrors = formService.fieldErrors.getValue()
73
82
 
@@ -80,7 +89,21 @@ export const Form: <T>(props: FormProps<T>, children: ChildrenList) => JSX.Eleme
80
89
  formService.validationResult.setValue({ isValid: true })
81
90
  formService.validatedFormData.setValue(formData)
82
91
  if (shouldSubmit) {
83
- props.onSubmit(formData)
92
+ formService.isSubmitting.setValue(true)
93
+ formService.submitError.setValue(undefined)
94
+ if (props.disableOnSubmit) {
95
+ formElement.inert = true
96
+ }
97
+ try {
98
+ await props.onSubmit(formData)
99
+ } catch (error) {
100
+ formService.submitError.setValue(error)
101
+ } finally {
102
+ formService.isSubmitting.setValue(false)
103
+ if (props.disableOnSubmit) {
104
+ formElement.inert = false
105
+ }
106
+ }
84
107
  }
85
108
  } else {
86
109
  formService.validationResult.setValue({ isValid: false, reason: 'validation-failed' })
@@ -89,14 +112,14 @@ export const Form: <T>(props: FormProps<T>, children: ChildrenList) => JSX.Eleme
89
112
 
90
113
  useHostProps({
91
114
  oninvalid: (ev: Event) => {
92
- changeHandler(ev)
115
+ void changeHandler(ev)
93
116
  },
94
117
  onsubmit: (ev: SubmitEvent) => {
95
118
  ev.preventDefault()
96
- changeHandler(ev, true)
119
+ void changeHandler(ev, true)
97
120
  },
98
121
  onchange: (ev: Event) => {
99
- changeHandler(ev)
122
+ void changeHandler(ev)
100
123
  },
101
124
  onreset: () => {
102
125
  formService.rawFormData.setValue(null)
@@ -1,6 +1,6 @@
1
1
  import { Injector } from '@furystack/inject'
2
- import { createComponent, initializeShadeRoot } from '@furystack/shades'
3
- import { sleepAsync, usingAsync } from '@furystack/utils'
2
+ import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
3
+ import { usingAsync } from '@furystack/utils'
4
4
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
5
  import { Grid, type GridProps, type HeaderCells, type RowCells } from './grid.js'
6
6
 
@@ -24,7 +24,7 @@ describe('Grid', () => {
24
24
  rootElement: root,
25
25
  jsxElement: <Grid {...props} />,
26
26
  })
27
- await sleepAsync(50)
27
+ await flushUpdates()
28
28
  const grid = document.querySelector('shade-grid') as HTMLElement
29
29
  return {
30
30
  injector,