@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,112 +1,102 @@
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 "./footer.js";
5
-
6
- describe("Footer 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-footer");
15
- assert.ok(element, "Element should be created");
16
- assert.ok(element.shadowRoot, "Element should have shadow root");
17
- });
18
-
19
- it("should have 3 columns by default", async () => {
20
- element = await fixture("grantcodes-footer");
21
- assert.strictEqual(element.columns, 3, "Should have 3 columns by default");
22
- });
23
-
24
- it("should render footer element", async () => {
25
- element = await fixture("grantcodes-footer");
26
- const footer = element.shadowRoot.querySelector("footer");
27
- assert.ok(footer, "Footer element should exist");
28
- });
29
-
30
- it("should render footer columns container", async () => {
31
- element = await fixture("grantcodes-footer");
32
- const columns = element.shadowRoot.querySelector(".footer__columns");
33
- assert.ok(columns, "Columns container should exist");
34
- });
35
-
36
- it("should apply columns CSS variable", async () => {
37
- element = await fixture("grantcodes-footer", {
38
- columns: 4,
39
- });
40
-
41
- const columns = element.shadowRoot.querySelector(".footer__columns");
42
- const style = columns.getAttribute("style");
43
- assert.ok(
44
- style.includes("--footer-columns: 4"),
45
- "Should set columns CSS variable",
46
- );
47
- });
48
-
49
- it("should have default content slot", async () => {
50
- element = await fixture("grantcodes-footer");
51
- const slot = element.shadowRoot.querySelector("slot:not([name])");
52
- assert.ok(slot, "Default slot should exist");
53
- });
54
-
55
- it("should render bottom section when content is provided", async () => {
56
- element = await fixture("grantcodes-footer");
57
-
58
- // Add content to bottom slot
59
- const bottomContent = document.createElement("div");
60
- bottomContent.setAttribute("slot", "bottom");
61
- bottomContent.textContent = "Bottom content";
62
- element.appendChild(bottomContent);
63
-
64
- await element.updateComplete;
65
- // Force a re-render to check the slot
66
- element.requestUpdate();
67
- await element.updateComplete;
68
-
69
- const bottom = element.shadowRoot.querySelector(".footer__bottom");
70
- assert.ok(bottom, "Bottom section should exist when content is slotted");
71
- });
72
-
73
- it("should support different column counts", async () => {
74
- const columnCounts = [1, 2, 3, 4, 5, 6];
75
-
76
- for (const count of columnCounts) {
77
- element = await fixture("grantcodes-footer", { columns: count });
78
- assert.strictEqual(
79
- element.columns,
80
- count,
81
- `Should support ${count} columns`,
82
- );
83
- cleanup(element);
84
- }
85
- });
86
-
87
- it("should have footer container", async () => {
88
- element = await fixture("grantcodes-footer");
89
- const container = element.shadowRoot.querySelector("grantcodes-container");
90
- assert.ok(container, "Footer container should exist");
91
- });
92
-
93
- it("should have footer class", async () => {
94
- element = await fixture("grantcodes-footer");
95
- const footer = element.shadowRoot.querySelector(".footer");
96
- assert.ok(footer, "Should have footer class");
97
- });
98
-
99
- it("should render slotted content", async () => {
100
- element = await fixture("grantcodes-footer");
101
- const content = document.createElement("div");
102
- content.textContent = "Footer content";
103
- element.appendChild(content);
104
-
105
- await element.updateComplete;
106
-
107
- assert.ok(
108
- element.querySelector("div"),
109
- "Slotted content should be present",
110
- );
111
- });
1
+ import { strict as assert } from 'node:assert';
2
+ import { afterEach, describe, it } from 'node:test';
3
+ import { cleanup, fixture } from '../../test-utils/index.js';
4
+ import './footer.js';
5
+
6
+ describe('Footer 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-footer');
15
+ assert.ok(element, 'Element should be created');
16
+ assert.ok(element.shadowRoot, 'Element should have shadow root');
17
+ });
18
+
19
+ it('should have 3 columns by default', async () => {
20
+ element = await fixture('grantcodes-footer');
21
+ assert.strictEqual(element.columns, 3, 'Should have 3 columns by default');
22
+ });
23
+
24
+ it('should render footer element', async () => {
25
+ element = await fixture('grantcodes-footer');
26
+ const footer = element.shadowRoot.querySelector('footer');
27
+ assert.ok(footer, 'Footer element should exist');
28
+ });
29
+
30
+ it('should render footer columns container', async () => {
31
+ element = await fixture('grantcodes-footer');
32
+ const columns = element.shadowRoot.querySelector('.footer__columns');
33
+ assert.ok(columns, 'Columns container should exist');
34
+ });
35
+
36
+ it('should apply columns CSS variable', async () => {
37
+ element = await fixture('grantcodes-footer', {
38
+ columns: 4,
39
+ });
40
+
41
+ const columns = element.shadowRoot.querySelector('.footer__columns');
42
+ const style = columns.getAttribute('style');
43
+ assert.ok(style.includes('--footer-columns: 4'), 'Should set columns CSS variable');
44
+ });
45
+
46
+ it('should have default content slot', async () => {
47
+ element = await fixture('grantcodes-footer');
48
+ const slot = element.shadowRoot.querySelector('slot:not([name])');
49
+ assert.ok(slot, 'Default slot should exist');
50
+ });
51
+
52
+ it('should render bottom section when content is provided', async () => {
53
+ element = await fixture('grantcodes-footer');
54
+
55
+ // Add content to bottom slot
56
+ const bottomContent = document.createElement('div');
57
+ bottomContent.setAttribute('slot', 'bottom');
58
+ bottomContent.textContent = 'Bottom content';
59
+ element.appendChild(bottomContent);
60
+
61
+ await element.updateComplete;
62
+ // Force a re-render to check the slot
63
+ element.requestUpdate();
64
+ await element.updateComplete;
65
+
66
+ const bottom = element.shadowRoot.querySelector('.footer__bottom');
67
+ assert.ok(bottom, 'Bottom section should exist when content is slotted');
68
+ });
69
+
70
+ it('should support different column counts', async () => {
71
+ const columnCounts = [1, 2, 3, 4, 5, 6];
72
+
73
+ for (const count of columnCounts) {
74
+ element = await fixture('grantcodes-footer', { columns: count });
75
+ assert.strictEqual(element.columns, count, `Should support ${count} columns`);
76
+ cleanup(element);
77
+ }
78
+ });
79
+
80
+ it('should have footer container', async () => {
81
+ element = await fixture('grantcodes-footer');
82
+ const container = element.shadowRoot.querySelector('grantcodes-container');
83
+ assert.ok(container, 'Footer container should exist');
84
+ });
85
+
86
+ it('should have footer class', async () => {
87
+ element = await fixture('grantcodes-footer');
88
+ const footer = element.shadowRoot.querySelector('.footer');
89
+ assert.ok(footer, 'Should have footer class');
90
+ });
91
+
92
+ it('should render slotted content', async () => {
93
+ element = await fixture('grantcodes-footer');
94
+ const content = document.createElement('div');
95
+ content.textContent = 'Footer content';
96
+ element.appendChild(content);
97
+
98
+ await element.updateComplete;
99
+
100
+ assert.ok(element.querySelector('div'), 'Slotted content should be present');
101
+ });
112
102
  });
@@ -1,2 +1,2 @@
1
- export * from "./footer.js";
2
- export { GrantCodesFooterColumn } from "./footer-column.component.js";
1
+ export * from './footer.js';
2
+ export { GrantCodesFooterColumn } from './footer-column.component.js';
@@ -1,162 +1,204 @@
1
- import { LitElement } from "lit";
2
- import { html } from "lit/static-html.js";
3
- import { classMap } from "lit/directives/class-map.js";
4
- import formFieldStyles from "./form-field.css" with { type: "css" };
5
- import { generateId } from "../../lib/generate-id.js";
1
+ import { LitElement } from 'lit';
2
+ import { classMap } from 'lit/directives/class-map.js';
3
+ import { html } from 'lit/static-html.js';
4
+ import { generateId } from '../../lib/generate-id.js';
5
+ import formFieldStyles from './form-field.css' with { type: 'css' };
6
6
 
7
7
  export class GrantCodesFormField extends LitElement {
8
- static formAssociated = true;
9
- static styles = [formFieldStyles];
8
+ static formAssociated = true;
9
+ static styles = [formFieldStyles];
10
10
 
11
- static properties = {
11
+ static properties = {
12
12
  label: { type: String, reflect: true },
13
- direction: { type: String },
14
- error: { type: String },
15
- help: { type: String },
16
- };
13
+ direction: { type: String },
14
+ error: { type: String },
15
+ help: { type: String },
16
+ };
17
17
 
18
- constructor() {
19
- super();
18
+ constructor() {
19
+ super();
20
20
 
21
- this.label = "";
22
- this.error = undefined;
23
- this.help = undefined;
21
+ this.label = '';
22
+ this.error = undefined;
23
+ this.help = undefined;
24
24
 
25
25
  this.groupInput = false;
26
26
 
27
27
  /**
28
- * Direction of the field. Generally want horizontal for checkboxes and radios.
29
- * @type {'vertical' | 'horizontal'}
30
- */
31
- this.direction = "vertical";
32
-
33
- /** @type {NodeListOf<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>} */
34
- this.inputElements;
35
-
36
- /** @type {NodeListOf<GrantCodesFormField>} */
37
- this.nestedFields;
38
-
39
- if (!this.id) {
40
- this.id = generateId("form-field");
41
- }
42
- }
43
-
44
- get errorId() {
45
- return `${this.id}-error`;
46
- }
47
-
48
- get helpId() {
49
- return `${this.id}-help`;
50
- }
51
-
52
- get ariaDescribedBy() {
53
- const ids = [];
54
- if (this.error) {
55
- ids.push(this.errorId);
56
- }
57
- if (this.help) {
58
- ids.push(this.helpId);
59
- }
60
- return ids.join(" ");
61
- }
62
-
63
- firstUpdated() {
64
- // Initialize inputs and nested fields if not already set
65
- if (!this.inputElements) {
66
- this.inputElements = this.querySelectorAll("input, select, textarea");
67
- }
68
- if (!this.nestedFields) {
69
- this.nestedFields = this.querySelectorAll("grantcodes-form-field");
70
- }
71
-
72
- const input = this.inputElements[0];
73
-
74
- if (this.nestedFields.length > 0) {
75
- this.groupInput = true;
76
- this.requestUpdate();
77
- }
78
-
79
- if (!input) {
80
- return;
81
- }
82
-
83
- input.id = this.id;
84
- input.setAttribute("aria-describedby", this.ariaDescribedBy);
85
-
86
- }
87
-
88
- handleLabelClick() {
89
- const input = this.inputElements[0];
90
- if (input) {
91
- input.focus();
92
- if (
93
- input instanceof HTMLInputElement &&
94
- (input.type === "checkbox" || input.type === "radio")
95
- ) {
96
- input.checked = !input.checked;
97
- }
98
- }
99
- }
100
-
101
- // handleError() {
102
- // if (this.error) {
103
- // const input = this.inputElements[0]
104
- // if (input) {
105
- // if (this.help) {
106
- // input.setAttribute('aria-describedby', `${this.errorId} ${this.helpId}`)
107
- // } else {
108
- // input.setAttribute('aria-describedby', this.errorId)
109
- // }
110
- // }
111
- // }
112
- // }
113
-
114
- errorTemplate() {
115
- if (!this.error) {
116
- return html``;
117
- }
118
-
119
- return html`
120
- <p class="form-field__error" id=${this.errorId}>Error: ${this.error}</p>
28
+ * Direction of the field. Generally want horizontal for checkboxes and radios.
29
+ * @type {'vertical' | 'horizontal'}
30
+ */
31
+ this.direction = 'vertical';
32
+
33
+ /** @type {NodeListOf<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>} */
34
+ this.inputElements;
35
+
36
+ /** @type {NodeListOf<GrantCodesFormField>} */
37
+ this.nestedFields;
38
+
39
+ this._revalidate = this._revalidate.bind(this);
40
+ this._touched = false;
41
+ }
42
+
43
+ connectedCallback() {
44
+ super.connectedCallback();
45
+ // Not in the constructor: a custom element that gains an attribute there is never upgraded.
46
+ if (!this.id) {
47
+ this.id = generateId('form-field');
48
+ }
49
+ this.addEventListener('input', this._revalidate);
50
+ // blur/invalid do not bubble, so they are caught on the way down.
51
+ this.addEventListener('blur', this._revalidate, true);
52
+ this.addEventListener('invalid', this._revalidate, true);
53
+ }
54
+
55
+ disconnectedCallback() {
56
+ this.removeEventListener('input', this._revalidate);
57
+ this.removeEventListener('blur', this._revalidate, true);
58
+ this.removeEventListener('invalid', this._revalidate, true);
59
+ super.disconnectedCallback();
60
+ }
61
+
62
+ _revalidate(event) {
63
+ // A consumer-set error should appear once the field has been interacted with.
64
+ if (event?.target?.matches?.('input, select, textarea')) {
65
+ this._touched = true;
66
+ }
67
+ this.requestUpdate();
68
+ }
69
+
70
+ get errorId() {
71
+ return `${this.id}-error`;
72
+ }
73
+
74
+ get helpId() {
75
+ return `${this.id}-help`;
76
+ }
77
+
78
+ get ariaDescribedBy() {
79
+ const ids = [];
80
+ if (this.error) {
81
+ ids.push(this.errorId);
82
+ }
83
+ if (this.help) {
84
+ ids.push(this.helpId);
85
+ }
86
+ return ids.join(' ');
87
+ }
88
+
89
+ /** Errors stay hidden until the field is interacted with or reports :user-invalid. */
90
+ get showError() {
91
+ if (!this.error) return false;
92
+ const controls = this.querySelectorAll('input, select, textarea');
93
+ if (controls.length === 0) return true;
94
+ if (this._touched) return true;
95
+ return Array.from(controls).some((control) => control.matches(':user-invalid'));
96
+ }
97
+
98
+ firstUpdated() {
99
+ // Initialize inputs and nested fields if not already set
100
+ if (!this.inputElements) {
101
+ this.inputElements = this.querySelectorAll('input, select, textarea');
102
+ }
103
+ if (!this.nestedFields) {
104
+ this.nestedFields = this.querySelectorAll('grantcodes-form-field');
105
+ }
106
+
107
+ const input = this.inputElements[0];
108
+
109
+ if (this.nestedFields.length > 0) {
110
+ this.groupInput = true;
111
+ this.requestUpdate();
112
+ }
113
+
114
+ if (!input) {
115
+ return;
116
+ }
117
+
118
+ this.syncControlAria();
119
+ }
120
+
121
+ updated(changedProperties) {
122
+ if (changedProperties.has('error') || changedProperties.has('help')) {
123
+ this.syncControlAria();
124
+ }
125
+ }
126
+
127
+ /** Mirrors the current error/help state onto the first control. */
128
+ syncControlAria() {
129
+ const input = this.inputElements?.[0];
130
+ if (!input) return;
131
+
132
+ if (this.ariaDescribedBy) {
133
+ input.setAttribute('aria-describedby', this.ariaDescribedBy);
134
+ } else {
135
+ input.removeAttribute('aria-describedby');
136
+ }
137
+
138
+ if (this.error) {
139
+ input.setAttribute('aria-invalid', 'true');
140
+ } else {
141
+ input.removeAttribute('aria-invalid');
142
+ }
143
+ }
144
+
145
+ handleLabelClick(event) {
146
+ const input = this.inputElements?.[0];
147
+ if (!input) return;
148
+
149
+ // The slotted control is not a DOM descendant of the shadow <label>, so the
150
+ // platform does not forward the click; do it here and suppress a duplicate.
151
+ event.preventDefault();
152
+ input.focus();
153
+ input.click();
154
+ }
155
+
156
+ errorTemplate() {
157
+ if (!this.error) {
158
+ return html``;
159
+ }
160
+
161
+ return html`
162
+ <p class="form-field__error" id=${this.errorId} ?hidden=${!this.showError}>${this.error}</p>
121
163
  `;
122
- }
164
+ }
123
165
 
124
- helpTemplate() {
125
- if (!this.help) {
126
- return html``;
127
- }
166
+ helpTemplate() {
167
+ if (!this.help) {
168
+ return html``;
169
+ }
128
170
 
129
- return html`
171
+ return html`
130
172
  <span class="form-field__help" id=${this.helpId}>${this.help}</span>
131
173
  `;
132
- }
174
+ }
133
175
 
134
176
  render() {
135
177
  const wrapperClass = classMap({
136
178
  'form-field': true,
137
- 'form-field--horizontal': this.direction === 'horizontal'
138
- })
139
- if (this.groupInput) {
140
- return html`
179
+ 'form-field--horizontal': this.direction === 'horizontal',
180
+ });
181
+ if (this.groupInput) {
182
+ return html`
141
183
  <fieldset class=${wrapperClass}>
142
184
  <legend class="form-field__label">${this.label}</legend>
143
- <slot></slot>
185
+ <slot @slotchange=${this._revalidate}></slot>
144
186
  ${this.errorTemplate()}
145
187
  </fieldset>
146
188
  `;
147
189
  }
148
190
 
149
- return html`
191
+ return html`
150
192
  <div class=${wrapperClass}>
151
193
  <label>
152
194
  <span class="form-field__label" @click=${this.handleLabelClick}
153
195
  >${this.label}</span
154
196
  >
155
197
  ${this.helpTemplate()}
156
- <slot></slot>
198
+ <slot @slotchange=${this._revalidate}></slot>
157
199
  </label>
158
200
  ${this.errorTemplate()}
159
201
  </div>
160
202
  `;
161
- }
203
+ }
162
204
  }
@@ -1,48 +1,48 @@
1
1
  *,
2
2
  *::before,
3
3
  *::after {
4
- box-sizing: border-box;
4
+ box-sizing: border-box;
5
5
  }
6
6
 
7
7
  .form-field {
8
- display: flex;
9
- flex-direction: column;
10
- gap: var(--g-spacing-xs);
11
- padding: 0;
12
- margin: 0;
13
- border: none;
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--g-spacing-xs);
11
+ padding: 0;
12
+ margin: 0;
13
+ border: none;
14
14
  }
15
15
 
16
16
  .form-field label,
17
17
  .form-field slot {
18
- display: flex;
19
- flex-direction: column;
20
- gap: inherit;
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: inherit;
21
21
  }
22
22
 
23
23
  .form-field--horizontal label {
24
- flex-direction: row-reverse;
25
- align-items: center;
26
- justify-content: flex-end;
27
- gap: var(--g-spacing-sm);
24
+ flex-direction: row-reverse;
25
+ align-items: center;
26
+ justify-content: flex-end;
27
+ gap: var(--g-spacing-sm);
28
28
  }
29
29
 
30
30
  .form-field__label {
31
- margin: 0;
32
- padding: 0;
31
+ margin: 0;
32
+ padding: 0;
33
33
 
34
- &:where(legend) {
35
- margin-bottom: var(--g-spacing-xs);
36
- }
34
+ &:where(legend) {
35
+ margin-bottom: var(--g-spacing-xs);
36
+ }
37
37
  }
38
38
 
39
39
  .form-field__help {
40
- font-size: var(--g-typography-body-sm-font-size);
41
- opacity: 0.7;
40
+ font-size: var(--g-typography-body-sm-font-size);
41
+ opacity: 0.7;
42
42
  }
43
43
 
44
44
  .form-field__error {
45
- margin: 0;
46
- font-size: var(--g-typography-body-sm-font-size);
47
- color: var(--g-color-content-utility-error);
45
+ margin: 0;
46
+ font-size: var(--g-typography-body-sm-font-size);
47
+ color: var(--g-color-content-utility-error);
48
48
  }
@@ -1,6 +1,6 @@
1
- import { GrantCodesFormField } from "./form-field.component.js";
1
+ import { GrantCodesFormField } from './form-field.component.js';
2
2
 
3
- export * from "./form-field.component.js";
3
+ export * from './form-field.component.js';
4
4
  export default GrantCodesFormField;
5
5
 
6
- customElements.define("grantcodes-form-field", GrantCodesFormField);
6
+ customElements.define('grantcodes-form-field', GrantCodesFormField);
@@ -1,9 +1,9 @@
1
- import React from "react";
2
- import { createComponent } from "@lit/react";
3
- import { GrantCodesFormField } from "./form-field.js";
1
+ import { createComponent } from '@lit/react';
2
+ import React from 'react';
3
+ import { GrantCodesFormField } from './form-field.js';
4
4
 
5
5
  export const FormField = createComponent({
6
- tagName: "grantcodes-form-field",
7
- elementClass: GrantCodesFormField,
8
- react: React,
6
+ tagName: 'grantcodes-form-field',
7
+ elementClass: GrantCodesFormField,
8
+ react: React,
9
9
  });