@grantcodes/ui 2.15.7 → 2.16.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 (300) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/custom-elements.json +476 -346
  3. package/package.json +23 -23
  4. package/src/components/accordion/accordion-item.component.js +17 -19
  5. package/src/components/accordion/accordion-item.css +40 -44
  6. package/src/components/accordion/accordion.component.js +7 -7
  7. package/src/components/accordion/accordion.css +5 -5
  8. package/src/components/accordion/accordion.js +6 -6
  9. package/src/components/accordion/accordion.react.js +6 -6
  10. package/src/components/accordion/accordion.stories.js +10 -10
  11. package/src/components/accordion/accordion.test.js +100 -100
  12. package/src/components/accordion/index.js +6 -6
  13. package/src/components/app-bar/app-bar.component.js +86 -92
  14. package/src/components/app-bar/app-bar.css +187 -203
  15. package/src/components/app-bar/app-bar.js +3 -3
  16. package/src/components/app-bar/app-bar.react.js +9 -9
  17. package/src/components/app-bar/app-bar.stories.js +20 -20
  18. package/src/components/app-bar/app-bar.test.js +190 -237
  19. package/src/components/app-bar/index.js +1 -1
  20. package/src/components/app-bar/nav-link.component.js +7 -9
  21. package/src/components/app-bar/nav-link.css +23 -28
  22. package/src/components/app-bar/nav-link.js +3 -3
  23. package/src/components/app-bar/nav-link.react.js +6 -6
  24. package/src/components/avatar/avatar.component.js +79 -68
  25. package/src/components/avatar/avatar.css +18 -18
  26. package/src/components/avatar/avatar.js +3 -3
  27. package/src/components/avatar/avatar.react.js +6 -6
  28. package/src/components/avatar/avatar.stories.js +24 -23
  29. package/src/components/avatar/avatar.test.js +128 -84
  30. package/src/components/avatar/index.js +1 -1
  31. package/src/components/badge/badge.component.js +23 -23
  32. package/src/components/badge/badge.css +39 -52
  33. package/src/components/badge/badge.js +3 -3
  34. package/src/components/badge/badge.react.js +6 -6
  35. package/src/components/badge/badge.stories.js +23 -23
  36. package/src/components/badge/badge.test.js +53 -68
  37. package/src/components/badge/index.js +1 -1
  38. package/src/components/breadcrumb/breadcrumb.component.js +95 -96
  39. package/src/components/breadcrumb/breadcrumb.css +42 -44
  40. package/src/components/breadcrumb/breadcrumb.js +6 -9
  41. package/src/components/breadcrumb/breadcrumb.react.js +9 -12
  42. package/src/components/breadcrumb/breadcrumb.stories.js +5 -5
  43. package/src/components/breadcrumb/breadcrumb.test.js +139 -151
  44. package/src/components/breadcrumb/index.js +1 -1
  45. package/src/components/button/button.component.js +101 -103
  46. package/src/components/button/button.css +90 -94
  47. package/src/components/button/button.js +3 -3
  48. package/src/components/button/button.react.js +6 -6
  49. package/src/components/button/button.stories.js +25 -25
  50. package/src/components/button/button.test.js +324 -344
  51. package/src/components/button/index.js +1 -1
  52. package/src/components/button-group/button-group.component.js +18 -18
  53. package/src/components/button-group/button-group.css +16 -16
  54. package/src/components/button-group/button-group.js +3 -3
  55. package/src/components/button-group/button-group.react.js +6 -6
  56. package/src/components/button-group/button-group.stories.js +23 -23
  57. package/src/components/button-group/button-group.test.js +54 -54
  58. package/src/components/button-group/index.js +1 -1
  59. package/src/components/card/card.component.js +31 -33
  60. package/src/components/card/card.css +85 -85
  61. package/src/components/card/card.js +4 -4
  62. package/src/components/card/card.react.js +6 -6
  63. package/src/components/card/card.stories.js +27 -26
  64. package/src/components/card/card.test.js +56 -56
  65. package/src/components/card/index.js +1 -1
  66. package/src/components/code-preview/code-preview.component.js +47 -48
  67. package/src/components/code-preview/code-preview.css +11 -11
  68. package/src/components/code-preview/code-preview.js +3 -3
  69. package/src/components/code-preview/code-preview.react.js +6 -6
  70. package/src/components/code-preview/code-preview.stories.js +29 -29
  71. package/src/components/code-preview/code-preview.test.js +112 -135
  72. package/src/components/code-preview/index.js +1 -1
  73. package/src/components/container/container.component.js +27 -27
  74. package/src/components/container/container.css +22 -22
  75. package/src/components/container/container.js +3 -3
  76. package/src/components/container/container.react.js +6 -6
  77. package/src/components/container/container.stories.js +44 -44
  78. package/src/components/container/container.test.js +54 -66
  79. package/src/components/container/index.js +1 -1
  80. package/src/components/countdown/countdown.component.js +160 -162
  81. package/src/components/countdown/countdown.css +50 -34
  82. package/src/components/countdown/countdown.js +3 -3
  83. package/src/components/countdown/countdown.react.js +6 -6
  84. package/src/components/countdown/countdown.stories.js +34 -34
  85. package/src/components/countdown/index.js +1 -1
  86. package/src/components/cta/cta.component.js +77 -81
  87. package/src/components/cta/cta.css +24 -24
  88. package/src/components/cta/cta.js +3 -3
  89. package/src/components/cta/cta.react.js +6 -6
  90. package/src/components/cta/cta.stories.js +32 -32
  91. package/src/components/cta/index.js +1 -1
  92. package/src/components/dialog/dialog.component.js +95 -58
  93. package/src/components/dialog/dialog.css +45 -50
  94. package/src/components/dialog/dialog.js +3 -3
  95. package/src/components/dialog/dialog.react.js +6 -6
  96. package/src/components/dialog/dialog.stories.js +38 -29
  97. package/src/components/dialog/dialog.test.js +188 -102
  98. package/src/components/dialog/index.js +1 -1
  99. package/src/components/dropdown/dropdown.component.js +220 -201
  100. package/src/components/dropdown/dropdown.css +60 -84
  101. package/src/components/dropdown/dropdown.js +6 -9
  102. package/src/components/dropdown/dropdown.react.js +15 -15
  103. package/src/components/dropdown/dropdown.stories.js +10 -10
  104. package/src/components/dropdown/dropdown.test.js +211 -157
  105. package/src/components/dropdown/index.js +1 -1
  106. package/src/components/dropzone/dropzone.component.js +126 -129
  107. package/src/components/dropzone/dropzone.css +35 -37
  108. package/src/components/dropzone/dropzone.js +3 -3
  109. package/src/components/dropzone/dropzone.react.js +6 -6
  110. package/src/components/dropzone/dropzone.stories.js +28 -28
  111. package/src/components/dropzone/dropzone.test.js +91 -99
  112. package/src/components/dropzone/index.js +1 -1
  113. package/src/components/feature-list/feature-list.component.js +78 -86
  114. package/src/components/feature-list/feature-list.css +37 -37
  115. package/src/components/feature-list/feature-list.js +3 -3
  116. package/src/components/feature-list/feature-list.react.js +6 -6
  117. package/src/components/feature-list/feature-list.stories.js +86 -90
  118. package/src/components/feature-list/index.js +1 -1
  119. package/src/components/footer/footer-column.component.js +7 -7
  120. package/src/components/footer/footer-column.css +22 -22
  121. package/src/components/footer/footer.component.js +19 -19
  122. package/src/components/footer/footer.css +38 -38
  123. package/src/components/footer/footer.js +8 -8
  124. package/src/components/footer/footer.react.js +9 -9
  125. package/src/components/footer/footer.stories.js +9 -9
  126. package/src/components/footer/footer.test.js +101 -111
  127. package/src/components/footer/index.js +2 -2
  128. package/src/components/form-field/form-field.component.js +167 -125
  129. package/src/components/form-field/form-field.css +24 -24
  130. package/src/components/form-field/form-field.js +3 -3
  131. package/src/components/form-field/form-field.react.js +6 -6
  132. package/src/components/form-field/form-field.stories.js +53 -44
  133. package/src/components/form-field/form-field.test.js +280 -134
  134. package/src/components/form-field/index.js +1 -1
  135. package/src/components/gallery/gallery-image.component.js +30 -30
  136. package/src/components/gallery/gallery-image.js +3 -3
  137. package/src/components/gallery/gallery-image.react.js +6 -6
  138. package/src/components/gallery/gallery.component.js +24 -17
  139. package/src/components/gallery/gallery.css +79 -72
  140. package/src/components/gallery/gallery.js +3 -3
  141. package/src/components/gallery/gallery.react.js +6 -6
  142. package/src/components/gallery/gallery.stories.js +69 -68
  143. package/src/components/gallery/gallery.test.js +95 -88
  144. package/src/components/gallery/index.js +2 -2
  145. package/src/components/hero/hero.component.js +53 -57
  146. package/src/components/hero/hero.css +27 -27
  147. package/src/components/hero/hero.js +3 -3
  148. package/src/components/hero/hero.react.js +6 -6
  149. package/src/components/hero/hero.stories.js +39 -39
  150. package/src/components/hero/index.js +1 -1
  151. package/src/components/icon/icon.component.js +10 -10
  152. package/src/components/icon/icon.css +13 -13
  153. package/src/components/icon/icon.js +4 -4
  154. package/src/components/icon/icon.react.js +6 -6
  155. package/src/components/icon/icon.stories.js +8 -9
  156. package/src/components/icon/icon.test.js +54 -55
  157. package/src/components/icon/index.js +1 -1
  158. package/src/components/loading/index.js +1 -1
  159. package/src/components/loading/loading.component.js +41 -14
  160. package/src/components/loading/loading.css +47 -30
  161. package/src/components/loading/loading.js +4 -4
  162. package/src/components/loading/loading.react.js +6 -6
  163. package/src/components/loading/loading.stories.js +19 -18
  164. package/src/components/loading/loading.test.js +77 -54
  165. package/src/components/logo-cloud/index.js +1 -1
  166. package/src/components/logo-cloud/logo-cloud.component.js +39 -43
  167. package/src/components/logo-cloud/logo-cloud.css +31 -31
  168. package/src/components/logo-cloud/logo-cloud.js +3 -3
  169. package/src/components/logo-cloud/logo-cloud.react.js +6 -6
  170. package/src/components/logo-cloud/logo-cloud.stories.js +80 -80
  171. package/src/components/map/index.js +1 -1
  172. package/src/components/map/map.component.js +103 -107
  173. package/src/components/map/map.css +22 -22
  174. package/src/components/map/map.js +3 -3
  175. package/src/components/map/map.react.js +6 -6
  176. package/src/components/map/map.stories.js +38 -38
  177. package/src/components/media-text/index.js +1 -1
  178. package/src/components/media-text/media-text.component.js +76 -70
  179. package/src/components/media-text/media-text.css +32 -32
  180. package/src/components/media-text/media-text.js +3 -3
  181. package/src/components/media-text/media-text.react.js +6 -6
  182. package/src/components/media-text/media-text.stories.js +45 -45
  183. package/src/components/media-text/media-text.test.js +72 -0
  184. package/src/components/newsletter/index.js +1 -1
  185. package/src/components/newsletter/newsletter.component.js +56 -64
  186. package/src/components/newsletter/newsletter.css +48 -48
  187. package/src/components/newsletter/newsletter.js +3 -3
  188. package/src/components/newsletter/newsletter.react.js +6 -6
  189. package/src/components/newsletter/newsletter.stories.js +35 -35
  190. package/src/components/notice/index.js +1 -1
  191. package/src/components/notice/notice.component.js +66 -55
  192. package/src/components/notice/notice.css +37 -38
  193. package/src/components/notice/notice.js +3 -3
  194. package/src/components/notice/notice.react.js +6 -6
  195. package/src/components/notice/notice.stories.js +54 -53
  196. package/src/components/notice/notice.test.js +210 -156
  197. package/src/components/pagination/index.js +1 -1
  198. package/src/components/pagination/pagination.component.js +44 -44
  199. package/src/components/pagination/pagination.css +6 -6
  200. package/src/components/pagination/pagination.js +4 -4
  201. package/src/components/pagination/pagination.react.js +6 -6
  202. package/src/components/pagination/pagination.stories.js +20 -18
  203. package/src/components/pagination/pagination.test.js +95 -102
  204. package/src/components/person/index.js +1 -1
  205. package/src/components/person/person.component.js +29 -29
  206. package/src/components/person/person.css +17 -17
  207. package/src/components/person/person.js +4 -4
  208. package/src/components/person/person.react.js +6 -6
  209. package/src/components/person/person.stories.js +34 -33
  210. package/src/components/person/person.test.js +55 -68
  211. package/src/components/pricing/index.js +1 -1
  212. package/src/components/pricing/pricing.component.js +58 -70
  213. package/src/components/pricing/pricing.css +65 -65
  214. package/src/components/pricing/pricing.js +3 -3
  215. package/src/components/pricing/pricing.react.js +6 -6
  216. package/src/components/pricing/pricing.stories.js +96 -96
  217. package/src/components/sidebar/index.js +1 -1
  218. package/src/components/sidebar/sidebar.component.js +157 -130
  219. package/src/components/sidebar/sidebar.css +118 -115
  220. package/src/components/sidebar/sidebar.js +4 -4
  221. package/src/components/sidebar/sidebar.react.js +10 -10
  222. package/src/components/sidebar/sidebar.stories.js +10 -10
  223. package/src/components/sidebar/sidebar.test.js +250 -230
  224. package/src/components/stats/index.js +1 -1
  225. package/src/components/stats/stats.component.js +42 -50
  226. package/src/components/stats/stats.css +29 -29
  227. package/src/components/stats/stats.js +3 -3
  228. package/src/components/stats/stats.react.js +6 -6
  229. package/src/components/stats/stats.stories.js +40 -40
  230. package/src/components/tabs/index.js +2 -2
  231. package/src/components/tabs/internal/tabs-button.component.js +16 -27
  232. package/src/components/tabs/internal/tabs-button.js +3 -3
  233. package/src/components/tabs/internal/tabs-button.react.js +6 -6
  234. package/src/components/tabs/internal/tabs-item.component.js +30 -30
  235. package/src/components/tabs/tab.component.js +9 -11
  236. package/src/components/tabs/tab.js +3 -3
  237. package/src/components/tabs/tab.react.js +6 -6
  238. package/src/components/tabs/tabs.component.js +107 -109
  239. package/src/components/tabs/tabs.css +50 -59
  240. package/src/components/tabs/tabs.js +4 -4
  241. package/src/components/tabs/tabs.react.js +6 -6
  242. package/src/components/tabs/tabs.stories.js +23 -22
  243. package/src/components/tabs/tabs.test.js +201 -147
  244. package/src/components/testimonials/index.js +1 -1
  245. package/src/components/testimonials/testimonials.component.js +47 -51
  246. package/src/components/testimonials/testimonials.css +31 -31
  247. package/src/components/testimonials/testimonials.js +3 -3
  248. package/src/components/testimonials/testimonials.react.js +6 -6
  249. package/src/components/testimonials/testimonials.stories.js +49 -51
  250. package/src/components/toast/index.js +1 -1
  251. package/src/components/toast/toast.component.js +217 -148
  252. package/src/components/toast/toast.css +101 -99
  253. package/src/components/toast/toast.js +6 -9
  254. package/src/components/toast/toast.react.js +12 -12
  255. package/src/components/toast/toast.stories.js +75 -49
  256. package/src/components/toast/toast.test.js +260 -221
  257. package/src/components/tooltip/index.js +1 -1
  258. package/src/components/tooltip/tooltip.component.js +56 -49
  259. package/src/components/tooltip/tooltip.css +61 -50
  260. package/src/components/tooltip/tooltip.js +4 -4
  261. package/src/components/tooltip/tooltip.react.js +6 -6
  262. package/src/components/tooltip/tooltip.stories.js +24 -23
  263. package/src/components/tooltip/tooltip.test.js +143 -155
  264. package/src/constructor.test.js +31 -0
  265. package/src/css/base.css +1 -0
  266. package/src/css/colors.stories.js +120 -122
  267. package/src/css/elements/a.css +28 -28
  268. package/src/css/elements/forms/button.css +1 -1
  269. package/src/css/elements/forms/input.css +80 -80
  270. package/src/css/elements/forms/label.css +3 -3
  271. package/src/css/elements/media/image.css +2 -2
  272. package/src/css/elements.stories.js +48 -49
  273. package/src/css/helpers.css +8 -8
  274. package/src/css/layers.stories.js +5 -5
  275. package/src/css/reset.css +102 -102
  276. package/src/css/themes/todomap.css +1 -1
  277. package/src/css/tokens.stories.js +50 -53
  278. package/src/css/typography.css +30 -32
  279. package/src/css/typography.stories.js +92 -92
  280. package/src/css/util/focus-ring.css +12 -20
  281. package/src/css/view-transitions.css +16 -0
  282. package/src/exports.test.js +89 -105
  283. package/src/icons.js +1 -1
  284. package/src/lib/generate-id.js +1 -1
  285. package/src/lib/slot-presence-controller.js +48 -0
  286. package/src/lib/styles/all.css +38 -37
  287. package/src/lib/styles/focus-ring.css +12 -20
  288. package/src/lib/view-transition.js +42 -0
  289. package/src/lib/view-transition.test.js +105 -0
  290. package/src/main.js +29 -29
  291. package/src/pages/agency.stories.js +59 -59
  292. package/src/pages/blog-post.stories.js +48 -48
  293. package/src/pages/saas-landing.stories.js +180 -180
  294. package/src/react.js +44 -44
  295. package/src/react.test.js +42 -51
  296. package/src/test-utils/assert-helpers.js +39 -58
  297. package/src/test-utils/events.js +32 -34
  298. package/src/test-utils/fixture.js +31 -31
  299. package/src/test-utils/index.js +3 -3
  300. package/src/types.d.ts +3 -3
@@ -1,28 +1,28 @@
1
- import { html } from "lit/static-html.js";
2
- import { getStorybookHelpers } from "@wc-toolkit/storybook-helpers";
3
- const { events, args, argTypes, template } = getStorybookHelpers(
4
- "grantcodes-form-field",
5
- );
6
- import "./form-field.js";
1
+ import { getStorybookHelpers } from '@wc-toolkit/storybook-helpers';
2
+ import { html } from 'lit/static-html.js';
3
+
4
+ const { events, args, argTypes, template } = getStorybookHelpers('grantcodes-form-field');
5
+
6
+ import './form-field.js';
7
7
 
8
8
  const meta = {
9
- title: "Components/FormField",
10
- component: "grantcodes-form-field",
11
- args: {
12
- ...args,
13
- label: "Label",
14
- error: "",
15
- help: "",
16
- slot: html`<input type="text" placeholder="This is a placeholder" />`,
17
- },
18
- argTypes,
19
- decorators: [(story) => html`<form>${story()}</form>`],
20
- render: (args) => template(args, args.slot),
21
- parameters: {
22
- actions: {
23
- handles: events,
24
- },
25
- },
9
+ title: 'Components/FormField',
10
+ component: 'grantcodes-form-field',
11
+ args: {
12
+ ...args,
13
+ label: 'Label',
14
+ error: '',
15
+ help: '',
16
+ slot: html`<input type="text" placeholder="This is a placeholder" />`,
17
+ },
18
+ argTypes,
19
+ decorators: [(story) => html`<form>${story()}</form>`],
20
+ render: (args) => template(args, args.slot),
21
+ parameters: {
22
+ actions: {
23
+ handles: events,
24
+ },
25
+ },
26
26
  };
27
27
 
28
28
  export default meta;
@@ -30,52 +30,61 @@ export default meta;
30
30
  export const FormField = {};
31
31
 
32
32
  export const FormFieldWithError = {
33
- args: {
34
- error: "This is an error",
35
- },
33
+ args: {
34
+ error: 'This is an error',
35
+ slot: html`<input type="text" placeholder="Type, then blur" required />`,
36
+ },
37
+ parameters: {
38
+ docs: {
39
+ description: {
40
+ story:
41
+ 'The error stays hidden until the field is interacted with or reports :user-invalid — type and blur to reveal it.',
42
+ },
43
+ },
44
+ },
36
45
  };
37
46
 
38
47
  export const FormFieldWithHelp = {
39
- args: {
40
- help: "This is help text",
41
- },
48
+ args: {
49
+ help: 'This is help text',
50
+ },
42
51
  };
43
52
 
44
53
  export const FormFieldWithSelect = {
45
54
  args: {
46
- slot: html`
55
+ slot: html`
47
56
  <select>
48
57
  <option value="1">Option 1</option>
49
58
  <option value="2">Option 2</option>
50
59
  <option value="3">Option 3</option>
51
60
  </select>
52
61
  `,
53
- },
62
+ },
54
63
  };
55
64
 
56
65
  export const FormFieldWithTextArea = {
57
- args: {
58
- slot: html`<textarea placeholder="This is a placeholder"></textarea>`,
59
- },
66
+ args: {
67
+ slot: html`<textarea placeholder="This is a placeholder"></textarea>`,
68
+ },
60
69
  };
61
70
 
62
71
  export const FormFieldWithCheckbox = {
63
72
  args: {
64
73
  direction: 'horizontal',
65
- slot: html`<input type="checkbox" value="1" name="name" />`,
66
- },
74
+ slot: html`<input type="checkbox" value="1" name="name" />`,
75
+ },
67
76
  };
68
77
 
69
78
  export const FormFieldWithRadio = {
70
79
  args: {
71
80
  direction: 'horizontal',
72
- slot: html`<input type="radio" />`,
73
- },
81
+ slot: html`<input type="radio" />`,
82
+ },
74
83
  };
75
84
 
76
85
  export const FormFieldWithRadioGroup = {
77
- args: {
78
- slot: html`
86
+ args: {
87
+ slot: html`
79
88
  <grantcodes-form-field label="Radio number 1" direction="horizontal">
80
89
  <input type="radio" name="radio-group" value="1" />
81
90
  </grantcodes-form-field>
@@ -86,12 +95,12 @@ export const FormFieldWithRadioGroup = {
86
95
  <input type="radio" name="radio-group" value="3" />
87
96
  </grantcodes-form-field>
88
97
  `,
89
- },
98
+ },
90
99
  };
91
100
 
92
101
  export const FormFieldWithCheckboxGroup = {
93
- args: {
94
- slot: html`
102
+ args: {
103
+ slot: html`
95
104
  <grantcodes-form-field label="Checkbox number 1" direction="horizontal"
96
105
  ><input type="checkbox"
97
106
  /></grantcodes-form-field>
@@ -102,5 +111,5 @@ export const FormFieldWithCheckboxGroup = {
102
111
  ><input type="checkbox"
103
112
  /></grantcodes-form-field>
104
113
  `,
105
- },
114
+ },
106
115
  };
@@ -1,135 +1,281 @@
1
- import { describe, it, afterEach } from "node:test";
2
- import { strict as assert } from "node:assert";
3
- import { fixture, cleanup } from "../../test-utils/index.js";
4
- import "./form-field.js";
5
-
6
- describe("Form Field Component", () => {
7
- let element;
8
-
9
- afterEach(() => {
10
- cleanup(element);
11
- });
12
-
13
- it("should render with default properties", async () => {
14
- element = await fixture("grantcodes-form-field");
15
- assert.ok(element, "Element should be created");
16
- assert.ok(element.shadowRoot, "Element should have shadow root");
17
- });
18
-
19
- it("should display label text", async () => {
20
- element = await fixture("grantcodes-form-field", {
21
- label: "Username",
22
- });
23
-
24
- const labelElement = element.shadowRoot.querySelector(".form-field__label");
25
- assert.ok(labelElement, "Label element should exist");
26
- assert.strictEqual(
27
- labelElement.textContent,
28
- "Username",
29
- "Label text should match",
30
- );
31
- });
32
-
33
- it("should reflect label text for SSR client upgrade", async () => {
34
- element = await fixture("grantcodes-form-field", {
35
- label: "Username",
36
- });
37
-
38
- assert.strictEqual(
39
- element.getAttribute("label"),
40
- "Username",
41
- "Label should be serialized to an attribute for client upgrade",
42
- );
43
- });
44
-
45
- it("should display error message when error is set", async () => {
46
- element = await fixture("grantcodes-form-field", {
47
- label: "Email",
48
- error: "Invalid email format",
49
- });
50
-
51
- const errorElement = element.shadowRoot.querySelector(".form-field__error");
52
- assert.ok(errorElement, "Error element should exist");
53
- assert.ok(
54
- errorElement.textContent.includes("Invalid email format"),
55
- "Error message should be displayed",
56
- );
57
- });
58
-
59
- it("should not display error when no error is set", async () => {
60
- element = await fixture("grantcodes-form-field", {
61
- label: "Email",
62
- });
63
-
64
- const errorElement = element.shadowRoot.querySelector(".form-field__error");
65
- assert.ok(!errorElement, "Error element should not exist");
66
- });
67
-
68
- it("should display help text when help is provided", async () => {
69
- element = await fixture("grantcodes-form-field", {
70
- label: "Password",
71
- help: "Must be at least 8 characters",
72
- });
73
-
74
- const helpElement = element.shadowRoot.querySelector(".form-field__help");
75
- assert.ok(helpElement, "Help element should exist");
76
- assert.strictEqual(
77
- helpElement.textContent,
78
- "Must be at least 8 characters",
79
- "Help text should match",
80
- );
81
- });
82
-
83
- it("should not display help when no help is provided", async () => {
84
- element = await fixture("grantcodes-form-field", {
85
- label: "Password",
86
- });
87
-
88
- const helpElement = element.shadowRoot.querySelector(".form-field__help");
89
- assert.ok(
90
- !helpElement,
91
- "Help element should not exist when help is not provided",
92
- );
93
- });
94
-
95
- it("should generate unique IDs", async () => {
96
- const element1 = await fixture("grantcodes-form-field");
97
- const element2 = await fixture("grantcodes-form-field");
98
-
99
- assert.notStrictEqual(element1.id, element2.id, "IDs should be unique");
100
-
101
- cleanup(element1);
102
- cleanup(element2);
103
- });
104
-
105
- it("should have label element", async () => {
106
- element = await fixture("grantcodes-form-field", {
107
- label: "Test Field",
108
- });
109
-
110
- const label = element.shadowRoot.querySelector("label");
111
- assert.ok(label, "Label element should exist");
112
- });
113
-
114
- it("should render as fieldset for grouped inputs", async () => {
115
- element = await fixture("grantcodes-form-field", {
116
- label: "Select options",
117
- });
118
-
119
- // Add nested form fields to trigger group mode
120
- const nestedField = document.createElement("grantcodes-form-field");
121
- element.appendChild(nestedField);
122
-
123
- await element.updateComplete;
124
- // This would need to check if groupInput was set, which happens in firstUpdated
125
- });
126
-
127
- it("should have slot for input elements", async () => {
128
- element = await fixture("grantcodes-form-field", {
129
- label: "Input",
130
- });
131
-
132
- const slot = element.shadowRoot.querySelector("slot");
133
- assert.ok(slot, "Slot should exist for input elements");
134
- });
1
+ import { strict as assert } from 'node:assert';
2
+ import { afterEach, describe, it } from 'node:test';
3
+ import { cleanup, click, fixture } from '../../test-utils/index.js';
4
+ import './form-field.js';
5
+
6
+ describe('Form Field Component', () => {
7
+ let element;
8
+
9
+ afterEach(() => {
10
+ cleanup(element);
11
+ });
12
+
13
+ it('should render with default properties', async () => {
14
+ element = await fixture('grantcodes-form-field');
15
+ assert.ok(element, 'Element should be created');
16
+ assert.ok(element.shadowRoot, 'Element should have shadow root');
17
+ });
18
+
19
+ it('should display label text', async () => {
20
+ element = await fixture('grantcodes-form-field', {
21
+ label: 'Username',
22
+ });
23
+
24
+ const labelElement = element.shadowRoot.querySelector('.form-field__label');
25
+ assert.ok(labelElement, 'Label element should exist');
26
+ assert.strictEqual(labelElement.textContent, 'Username', 'Label text should match');
27
+ });
28
+
29
+ it('should reflect label text for SSR client upgrade', async () => {
30
+ element = await fixture('grantcodes-form-field', {
31
+ label: 'Username',
32
+ });
33
+
34
+ assert.strictEqual(
35
+ element.getAttribute('label'),
36
+ 'Username',
37
+ 'Label should be serialized to an attribute for client upgrade',
38
+ );
39
+ });
40
+
41
+ it('should display error message when error is set', async () => {
42
+ element = await fixture('grantcodes-form-field', {
43
+ label: 'Email',
44
+ error: 'Invalid email format',
45
+ });
46
+
47
+ const errorElement = element.shadowRoot.querySelector('.form-field__error');
48
+ assert.ok(errorElement, 'Error element should exist');
49
+ assert.ok(
50
+ errorElement.textContent.includes('Invalid email format'),
51
+ 'Error message should be displayed',
52
+ );
53
+ });
54
+
55
+ it('should not display error when no error is set', async () => {
56
+ element = await fixture('grantcodes-form-field', {
57
+ label: 'Email',
58
+ });
59
+
60
+ const errorElement = element.shadowRoot.querySelector('.form-field__error');
61
+ assert.ok(!errorElement, 'Error element should not exist');
62
+ });
63
+
64
+ it('should display help text when help is provided', async () => {
65
+ element = await fixture('grantcodes-form-field', {
66
+ label: 'Password',
67
+ help: 'Must be at least 8 characters',
68
+ });
69
+
70
+ const helpElement = element.shadowRoot.querySelector('.form-field__help');
71
+ assert.ok(helpElement, 'Help element should exist');
72
+ assert.strictEqual(
73
+ helpElement.textContent,
74
+ 'Must be at least 8 characters',
75
+ 'Help text should match',
76
+ );
77
+ });
78
+
79
+ it('should not display help when no help is provided', async () => {
80
+ element = await fixture('grantcodes-form-field', {
81
+ label: 'Password',
82
+ });
83
+
84
+ const helpElement = element.shadowRoot.querySelector('.form-field__help');
85
+ assert.ok(!helpElement, 'Help element should not exist when help is not provided');
86
+ });
87
+
88
+ it('should generate unique IDs', async () => {
89
+ const element1 = await fixture('grantcodes-form-field');
90
+ const element2 = await fixture('grantcodes-form-field');
91
+
92
+ assert.notStrictEqual(element1.id, element2.id, 'IDs should be unique');
93
+
94
+ cleanup(element1);
95
+ cleanup(element2);
96
+ });
97
+
98
+ it('should not copy the host id onto the inner control', async () => {
99
+ element = document.createElement('grantcodes-form-field');
100
+ element.label = 'Username';
101
+ element.innerHTML = '<input type="text" />';
102
+ document.body.appendChild(element);
103
+ await element.updateComplete;
104
+
105
+ const input = element.querySelector('input');
106
+ assert.notStrictEqual(
107
+ input.id,
108
+ element.id,
109
+ 'Inner control must not duplicate the host id',
110
+ );
111
+ });
112
+
113
+ it('should have label element', async () => {
114
+ element = await fixture('grantcodes-form-field', {
115
+ label: 'Test Field',
116
+ });
117
+
118
+ const label = element.shadowRoot.querySelector('label');
119
+ assert.ok(label, 'Label element should exist');
120
+ });
121
+
122
+ it('should render as fieldset for grouped inputs', async () => {
123
+ element = await fixture('grantcodes-form-field', {
124
+ label: 'Select options',
125
+ });
126
+
127
+ // Add nested form fields to trigger group mode
128
+ const nestedField = document.createElement('grantcodes-form-field');
129
+ element.appendChild(nestedField);
130
+
131
+ await element.updateComplete;
132
+ // This would need to check if groupInput was set, which happens in firstUpdated
133
+ });
134
+
135
+ it('should have slot for input elements', async () => {
136
+ element = await fixture('grantcodes-form-field', {
137
+ label: 'Input',
138
+ });
139
+
140
+ const slot = element.shadowRoot.querySelector('slot');
141
+ assert.ok(slot, 'Slot should exist for input elements');
142
+ });
143
+
144
+ it('should follow later error and help changes with aria-describedby', async () => {
145
+ element = document.createElement('grantcodes-form-field');
146
+ element.label = 'Email';
147
+ element.innerHTML = '<input type="text" />';
148
+ document.body.appendChild(element);
149
+ await element.updateComplete;
150
+
151
+ const input = element.querySelector('input');
152
+ assert.ok(!input.getAttribute('aria-describedby'), 'No description before error/help');
153
+
154
+ element.error = 'Invalid email';
155
+ element.help = 'Use your work email';
156
+ await element.updateComplete;
157
+
158
+ const describedBy = input.getAttribute('aria-describedby');
159
+ assert.ok(describedBy.includes(`${element.id}-error`), 'Error id should be described');
160
+ assert.ok(describedBy.includes(`${element.id}-help`), 'Help id should be described');
161
+ });
162
+
163
+ it('should drop aria-describedby when error and help are cleared', async () => {
164
+ element = document.createElement('grantcodes-form-field');
165
+ element.label = 'Email';
166
+ element.error = 'Invalid email';
167
+ element.innerHTML = '<input type="text" />';
168
+ document.body.appendChild(element);
169
+ await element.updateComplete;
170
+
171
+ const input = element.querySelector('input');
172
+ assert.ok(input.getAttribute('aria-describedby'), 'Error should be described initially');
173
+
174
+ element.error = undefined;
175
+ await element.updateComplete;
176
+
177
+ assert.strictEqual(
178
+ input.hasAttribute('aria-describedby'),
179
+ false,
180
+ 'Description should be removed when error and help are cleared',
181
+ );
182
+ });
183
+
184
+ it('should keep aria-invalid in sync with the error state', async () => {
185
+ element = document.createElement('grantcodes-form-field');
186
+ element.label = 'Email';
187
+ element.innerHTML = '<input type="email" />';
188
+ document.body.appendChild(element);
189
+ await element.updateComplete;
190
+
191
+ const input = element.querySelector('input');
192
+ assert.strictEqual(input.hasAttribute('aria-invalid'), false, 'No error, no aria-invalid');
193
+
194
+ element.error = 'Invalid email';
195
+ await element.updateComplete;
196
+ assert.strictEqual(input.getAttribute('aria-invalid'), 'true', 'Error sets aria-invalid');
197
+
198
+ element.error = undefined;
199
+ await element.updateComplete;
200
+ assert.strictEqual(input.hasAttribute('aria-invalid'), false, 'Clearing clears aria-invalid');
201
+ });
202
+
203
+ it('should hold the error message until the control is user-invalid', async () => {
204
+ element = document.createElement('grantcodes-form-field');
205
+ element.label = 'Email';
206
+ element.error = 'Invalid email';
207
+ element.innerHTML = '<input type="email" />';
208
+ document.body.appendChild(element);
209
+ await element.updateComplete;
210
+
211
+ const error = element.shadowRoot.querySelector('.form-field__error');
212
+ assert.ok(error, 'Error text should be rendered');
213
+ assert.strictEqual(error.textContent.trim(), 'Invalid email', 'The "Error: " prefix is gone');
214
+ assert.ok(error.hasAttribute('hidden'), 'Untouched control must not show the error yet');
215
+
216
+ // happy-dom cannot put a control into :user-invalid, so the state is stubbed.
217
+ const input = element.querySelector('input');
218
+ input.matches = (selector) => selector === ':user-invalid';
219
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
220
+ await element.updateComplete;
221
+
222
+ assert.strictEqual(
223
+ element.shadowRoot.querySelector('.form-field__error').hasAttribute('hidden'),
224
+ false,
225
+ 'Error should appear once the control is user-invalid',
226
+ );
227
+ });
228
+
229
+ it('should show the error when there is no control to validate', async () => {
230
+ element = await fixture('grantcodes-form-field', {
231
+ label: 'Email',
232
+ error: 'Invalid email',
233
+ });
234
+
235
+ const error = element.shadowRoot.querySelector('.form-field__error');
236
+ assert.ok(error, 'Error should render without a control');
237
+ assert.strictEqual(error.hasAttribute('hidden'), false, 'Nothing to validate, so show it');
238
+ });
239
+ it('should toggle a wrapped checkbox from the label text', async () => {
240
+ element = document.createElement('grantcodes-form-field');
241
+ element.label = 'Accept terms';
242
+ element.innerHTML = '<input type="checkbox" />';
243
+ document.body.appendChild(element);
244
+ await element.updateComplete;
245
+
246
+ const input = element.querySelector('input');
247
+ let changes = 0;
248
+ input.addEventListener('change', () => {
249
+ changes += 1;
250
+ });
251
+
252
+ click(element.shadowRoot.querySelector('.form-field__label'));
253
+
254
+ assert.strictEqual(input.checked, true, 'A label click must toggle the checkbox');
255
+ assert.strictEqual(changes, 1, 'It must behave like a real click and emit change');
256
+ });
257
+
258
+ it('should reveal a consumer-set error once the field is touched', async () => {
259
+ element = document.createElement('grantcodes-form-field');
260
+ element.label = 'Email';
261
+ element.error = 'Invalid email';
262
+ element.innerHTML = '<input type="text" />';
263
+ document.body.appendChild(element);
264
+ await element.updateComplete;
265
+
266
+ assert.ok(
267
+ element.shadowRoot.querySelector('.form-field__error').hasAttribute('hidden'),
268
+ 'Untouched field keeps the error hidden',
269
+ );
270
+
271
+ const input = element.querySelector('input');
272
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
273
+ await element.updateComplete;
274
+
275
+ assert.strictEqual(
276
+ element.shadowRoot.querySelector('.form-field__error').hasAttribute('hidden'),
277
+ false,
278
+ 'Touching the field reveals a consumer-set error without native invalidity',
279
+ );
280
+ });
135
281
  });
@@ -1 +1 @@
1
- export * from "./form-field.js";
1
+ export * from './form-field.js';
@@ -1,42 +1,42 @@
1
- import { LitElement } from "lit";
2
- import { html } from "lit/static-html.js";
3
- import galleryStyles from "./gallery.css" with { type: "css" };
1
+ import { LitElement } from 'lit';
2
+ import { html } from 'lit/static-html.js';
3
+ import galleryStyles from './gallery.css' with { type: 'css' };
4
4
 
5
5
  export class GrantCodesGalleryImage extends LitElement {
6
- static styles = [galleryStyles];
6
+ static styles = [galleryStyles];
7
7
 
8
- static properties = {
9
- width: { type: Number },
10
- height: { type: Number },
11
- src: { type: String },
12
- alt: { type: String },
13
- caption: { type: String },
14
- thumbnail: { type: String },
15
- };
8
+ static properties = {
9
+ width: { type: Number },
10
+ height: { type: Number },
11
+ src: { type: String },
12
+ alt: { type: String },
13
+ caption: { type: String },
14
+ thumbnail: { type: String },
15
+ };
16
16
 
17
- constructor() {
18
- super();
17
+ constructor() {
18
+ super();
19
19
 
20
- this.width = 0;
21
- this.height = 0;
22
- this.src = "";
23
- this.alt = "";
24
- this.caption = "";
25
- this.thumbnail = "";
26
- }
20
+ this.width = 0;
21
+ this.height = 0;
22
+ this.src = '';
23
+ this.alt = '';
24
+ this.caption = '';
25
+ this.thumbnail = '';
26
+ }
27
27
 
28
- captionTemplate() {
29
- if (!this.caption) {
30
- return html``;
31
- }
28
+ captionTemplate() {
29
+ if (!this.caption) {
30
+ return html``;
31
+ }
32
32
 
33
- return html`
33
+ return html`
34
34
  <figcaption class="gallery__image__caption">${this.caption}</figcaption>
35
35
  `;
36
- }
36
+ }
37
37
 
38
- render() {
39
- return html`
38
+ render() {
39
+ return html`
40
40
  <figure class="gallery__image">
41
41
  <img
42
42
  width=${this.width}
@@ -48,5 +48,5 @@ export class GrantCodesGalleryImage extends LitElement {
48
48
  ${this.captionTemplate()}
49
49
  </figure>
50
50
  `;
51
- }
51
+ }
52
52
  }
@@ -1,6 +1,6 @@
1
- import { GrantCodesGalleryImage } from "./gallery-image.component.js";
1
+ import { GrantCodesGalleryImage } from './gallery-image.component.js';
2
2
 
3
- export * from "./gallery-image.component.js";
3
+ export * from './gallery-image.component.js';
4
4
  export default GrantCodesGalleryImage;
5
5
 
6
- customElements.define("grantcodes-gallery-image", GrantCodesGalleryImage);
6
+ customElements.define('grantcodes-gallery-image', GrantCodesGalleryImage);