@grantcodes/ui 2.15.6 → 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 +67 -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,148 +1,202 @@
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 "./tabs.js";
5
- import "./tab.js";
6
-
7
- describe("Tabs Component", () => {
8
- let element;
9
-
10
- afterEach(() => {
11
- cleanup(element);
12
- });
13
-
14
- it("should render with default properties", async () => {
15
- element = await fixture("grantcodes-tabs");
16
- assert.ok(element, "Element should be created");
17
- assert.ok(element.shadowRoot, "Element should have shadow root");
18
- });
19
-
20
- it("should have label property", async () => {
21
- element = await fixture("grantcodes-tabs", {
22
- label: "Navigation tabs",
23
- });
24
-
25
- assert.strictEqual(element.label, "Navigation tabs", "Label should be set");
26
- });
27
-
28
- it("should render tabs wrapper", async () => {
29
- element = await fixture("grantcodes-tabs");
30
- const tabs = element.shadowRoot.querySelector(".tabs");
31
- assert.ok(tabs, "Tabs wrapper should exist");
32
- });
33
-
34
- it("should render tablist with role", async () => {
35
- element = await fixture("grantcodes-tabs");
36
- const tablist = element.shadowRoot.querySelector('[role="tablist"]');
37
- assert.ok(tablist, "Tablist should exist with role");
38
- assert.ok(
39
- tablist.classList.contains("tabs__tablist"),
40
- "Should have tablist class",
41
- );
42
- });
43
-
44
- it("should render panels container", async () => {
45
- element = await fixture("grantcodes-tabs");
46
- const panels = element.shadowRoot.querySelector(".tabs__panels");
47
- assert.ok(panels, "Panels container should exist");
48
- });
49
-
50
- it("should have slot for tab panels", async () => {
51
- element = await fixture("grantcodes-tabs");
52
- const slot = element.shadowRoot.querySelector(".tabs__panels slot");
53
- assert.ok(slot, "Slot should exist for tab panels");
54
- });
55
-
56
- it("should generate unique ID", async () => {
57
- const element1 = await fixture("grantcodes-tabs");
58
- const element2 = await fixture("grantcodes-tabs");
59
-
60
- assert.notStrictEqual(element1.id, element2.id, "IDs should be unique");
61
-
62
- cleanup(element1);
63
- cleanup(element2);
64
- });
65
-
66
- it("should initialize tabs array", async () => {
67
- element = await fixture("grantcodes-tabs");
68
- assert.ok(Array.isArray(element.tabs), "Tabs should be an array");
69
- });
70
-
71
- it("should initialize tabButtons array", async () => {
72
- element = await fixture("grantcodes-tabs");
73
- assert.ok(
74
- Array.isArray(element.tabButtons),
75
- "Tab buttons should be an array",
76
- );
77
- });
78
-
79
- it("should have focusedTabIndex property", async () => {
80
- element = await fixture("grantcodes-tabs");
81
- // After firstUpdated, if there are tabs, _focusedTabIndex will be set to 0
82
- // If there are no tabs, it should remain -1
83
- // Since we're not adding tabs, it should remain -1, but firstUpdated may have run
84
- // Check that the property exists and is a number
85
- assert.ok(
86
- typeof element._focusedTabIndex === "number",
87
- "Should have focusedTabIndex property",
88
- );
89
- });
90
-
91
- it("should support keyboard navigation", async () => {
92
- element = await fixture("grantcodes-tabs");
93
- // The handleTabKeyDown method handles ArrowRight and ArrowLeft
94
- assert.ok(
95
- element.handleTabKeyDown,
96
- "Should have keyboard navigation handler",
97
- );
98
- });
99
-
100
- it("should set aria-label on tablist when label is provided", async () => {
101
- element = await fixture("grantcodes-tabs", {
102
- label: "Main navigation",
103
- });
104
-
105
- const tablist = element.shadowRoot.querySelector('[role="tablist"]');
106
- assert.strictEqual(
107
- tablist.getAttribute("aria-label"),
108
- "Main navigation",
109
- "Tablist should have aria-label",
110
- );
111
- });
112
-
113
- it("should manage active tab state", async () => {
114
- element = await fixture("grantcodes-tabs");
115
-
116
- // Create and add tab elements
117
- const tab1 = document.createElement("grantcodes-tab");
118
- tab1.label = "Tab 1";
119
- const tab2 = document.createElement("grantcodes-tab");
120
- tab2.label = "Tab 2";
121
-
122
- element.appendChild(tab1);
123
- element.appendChild(tab2);
124
-
125
- await element.updateComplete;
126
-
127
- // The tabs are initialized in firstUpdated
128
- // Check that activeTab getter works
129
- assert.ok(element.activeTab !== undefined, "Should have activeTab getter");
130
- });
131
-
132
- it("should render tab buttons for each tab", async () => {
133
- element = await fixture("grantcodes-tabs");
134
-
135
- const tab1 = document.createElement("grantcodes-tab");
136
- tab1.label = "First";
137
- const tab2 = document.createElement("grantcodes-tab");
138
- tab2.label = "Second";
139
-
140
- element.appendChild(tab1);
141
- element.appendChild(tab2);
142
-
143
- await element.updateComplete;
144
-
145
- // Tab buttons are rendered via renderTabButtons method
146
- // In a real browser, these would be visible in the shadow DOM
147
- });
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 './tabs.js';
5
+ import './tab.js';
6
+
7
+ describe('Tabs Component', () => {
8
+ let element;
9
+
10
+ afterEach(() => {
11
+ cleanup(element);
12
+ });
13
+
14
+ it('should render with default properties', async () => {
15
+ element = await fixture('grantcodes-tabs');
16
+ assert.ok(element, 'Element should be created');
17
+ assert.ok(element.shadowRoot, 'Element should have shadow root');
18
+ });
19
+
20
+ it('should have label property', async () => {
21
+ element = await fixture('grantcodes-tabs', {
22
+ label: 'Navigation tabs',
23
+ });
24
+
25
+ assert.strictEqual(element.label, 'Navigation tabs', 'Label should be set');
26
+ });
27
+
28
+ it('should render tabs wrapper', async () => {
29
+ element = await fixture('grantcodes-tabs');
30
+ const tabs = element.shadowRoot.querySelector('.tabs');
31
+ assert.ok(tabs, 'Tabs wrapper should exist');
32
+ });
33
+
34
+ it('should render tablist with role', async () => {
35
+ element = await fixture('grantcodes-tabs');
36
+ const tablist = element.shadowRoot.querySelector('[role="tablist"]');
37
+ assert.ok(tablist, 'Tablist should exist with role');
38
+ assert.ok(tablist.classList.contains('tabs__tablist'), 'Should have tablist class');
39
+ });
40
+
41
+ it('should render panels container', async () => {
42
+ element = await fixture('grantcodes-tabs');
43
+ const panels = element.shadowRoot.querySelector('.tabs__panels');
44
+ assert.ok(panels, 'Panels container should exist');
45
+ });
46
+
47
+ it('should have slot for tab panels', async () => {
48
+ element = await fixture('grantcodes-tabs');
49
+ const slot = element.shadowRoot.querySelector('.tabs__panels slot');
50
+ assert.ok(slot, 'Slot should exist for tab panels');
51
+ });
52
+
53
+ it('should generate unique ID', async () => {
54
+ const element1 = await fixture('grantcodes-tabs');
55
+ const element2 = await fixture('grantcodes-tabs');
56
+
57
+ assert.notStrictEqual(element1.id, element2.id, 'IDs should be unique');
58
+
59
+ cleanup(element1);
60
+ cleanup(element2);
61
+ });
62
+
63
+ it('should initialize tabs array', async () => {
64
+ element = await fixture('grantcodes-tabs');
65
+ assert.ok(Array.isArray(element.tabs), 'Tabs should be an array');
66
+ });
67
+
68
+ it('should initialize tabButtons array', async () => {
69
+ element = await fixture('grantcodes-tabs');
70
+ assert.ok(Array.isArray(element.tabButtons), 'Tab buttons should be an array');
71
+ });
72
+
73
+ it('should have focusedTabIndex property', async () => {
74
+ element = await fixture('grantcodes-tabs');
75
+ // After firstUpdated, if there are tabs, _focusedTabIndex will be set to 0
76
+ // If there are no tabs, it should remain -1
77
+ // Since we're not adding tabs, it should remain -1, but firstUpdated may have run
78
+ // Check that the property exists and is a number
79
+ assert.ok(typeof element._focusedTabIndex === 'number', 'Should have focusedTabIndex property');
80
+ });
81
+
82
+ it('should support keyboard navigation', async () => {
83
+ element = await fixture('grantcodes-tabs');
84
+ // The handleTabKeyDown method handles ArrowRight and ArrowLeft
85
+ assert.ok(element.handleTabKeyDown, 'Should have keyboard navigation handler');
86
+ });
87
+
88
+ it('should set aria-label on tablist when label is provided', async () => {
89
+ element = await fixture('grantcodes-tabs', {
90
+ label: 'Main navigation',
91
+ });
92
+
93
+ const tablist = element.shadowRoot.querySelector('[role="tablist"]');
94
+ assert.strictEqual(
95
+ tablist.getAttribute('aria-label'),
96
+ 'Main navigation',
97
+ 'Tablist should have aria-label',
98
+ );
99
+ });
100
+
101
+ it('should manage active tab state', async () => {
102
+ element = await fixture('grantcodes-tabs');
103
+
104
+ // Create and add tab elements
105
+ const tab1 = document.createElement('grantcodes-tab');
106
+ tab1.label = 'Tab 1';
107
+ const tab2 = document.createElement('grantcodes-tab');
108
+ tab2.label = 'Tab 2';
109
+
110
+ element.appendChild(tab1);
111
+ element.appendChild(tab2);
112
+
113
+ await element.updateComplete;
114
+
115
+ // The tabs are initialized in firstUpdated
116
+ // Check that activeTab getter works
117
+ assert.ok(element.activeTab !== undefined, 'Should have activeTab getter');
118
+ });
119
+
120
+ it('should render tab buttons for each tab', async () => {
121
+ element = await fixture('grantcodes-tabs');
122
+
123
+ const tab1 = document.createElement('grantcodes-tab');
124
+ tab1.label = 'First';
125
+ const tab2 = document.createElement('grantcodes-tab');
126
+ tab2.label = 'Second';
127
+
128
+ element.appendChild(tab1);
129
+ element.appendChild(tab2);
130
+
131
+ await element.updateComplete;
132
+
133
+ // Tab buttons are rendered via renderTabButtons method
134
+ // In a real browser, these would be visible in the shadow DOM
135
+ });
136
+
137
+ const mountTabs = async (labels) => {
138
+ element = document.createElement('grantcodes-tabs');
139
+ element.innerHTML = labels
140
+ .map((label) => `<grantcodes-tab label="${label}"><p>${label}</p></grantcodes-tab>`)
141
+ .join('');
142
+ document.body.appendChild(element);
143
+ await element.updateComplete;
144
+ await element.updateComplete;
145
+ return {
146
+ buttons: Array.from(element.shadowRoot.querySelectorAll('grantcodes-tabs-button')),
147
+ panels: Array.from(element.querySelectorAll('grantcodes-tab')).map((tab) =>
148
+ tab.shadowRoot.querySelector('.tabs__panel'),
149
+ ),
150
+ };
151
+ };
152
+
153
+ it('should mark exactly one tab button active', async () => {
154
+ const { buttons } = await mountTabs(['One', 'Two', 'Three']);
155
+
156
+ assert.strictEqual(buttons.length, 3, 'One button per tab');
157
+ assert.strictEqual(
158
+ buttons.filter((button) => button.active).length,
159
+ 1,
160
+ 'Exactly one tab may be active',
161
+ );
162
+ assert.strictEqual(buttons[0].active, true, 'The first tab is active by default');
163
+
164
+ const highlighted = buttons.filter((button) =>
165
+ button.shadowRoot.querySelector('button').classList.contains('is-active'),
166
+ );
167
+ assert.strictEqual(highlighted.length, 1, 'Exactly one tab button carries the highlight');
168
+ });
169
+
170
+ it('should pair each tab button with its own panel', async () => {
171
+ const { buttons, panels } = await mountTabs(['One', 'Two']);
172
+
173
+ buttons.forEach((button, index) => {
174
+ const inner = button.shadowRoot.querySelector('button');
175
+ assert.strictEqual(
176
+ inner.getAttribute('aria-controls'),
177
+ panels[index].id,
178
+ `Button ${index} must control its own panel`,
179
+ );
180
+ assert.strictEqual(
181
+ panels[index].getAttribute('aria-labelledby'),
182
+ inner.id,
183
+ `Panel ${index} must be labelled by its own button`,
184
+ );
185
+ });
186
+ });
187
+
188
+ it('should switch panels when another tab button is clicked', async () => {
189
+ const { buttons, panels } = await mountTabs(['One', 'Two']);
190
+
191
+ buttons[1].click();
192
+ await element.updateComplete;
193
+
194
+ assert.deepStrictEqual(
195
+ panels.map((panel) => panel.classList.contains('is-active')),
196
+ [false, true],
197
+ 'Only the clicked panel stays visible',
198
+ );
199
+ assert.strictEqual(buttons[1].active, true, 'Clicked button is active');
200
+ assert.strictEqual(buttons[0].active, false, 'Other button is no longer active');
201
+ });
148
202
  });
@@ -1 +1 @@
1
- export * from "./testimonials.js";
1
+ export * from './testimonials.js';
@@ -1,61 +1,57 @@
1
- import { LitElement, html } from "lit";
2
- import testimonialsStyles from "./testimonials.css" with { type: "css" };
3
- import "../card/card.js";
4
- import "../person/person.js";
5
- import "../container/container.js";
1
+ import { html, LitElement } from 'lit';
2
+ import testimonialsStyles from './testimonials.css' with { type: 'css' };
3
+ import '../card/card.js';
4
+ import '../person/person.js';
5
+ import '../container/container.js';
6
6
 
7
7
  export class GrantCodesTestimonials extends LitElement {
8
- static styles = [testimonialsStyles];
8
+ static styles = [testimonialsStyles];
9
9
 
10
- static properties = {
11
- /**
12
- * Optional section heading.
13
- * @type {string}
14
- */
15
- title: { type: String },
16
- /**
17
- * Testimonial items as a JSON string array: `[{"quote":"...","name":"...","role":"...","company":"...","avatar":"..."}]`.
18
- * @type {string}
19
- */
20
- items: { type: String },
21
- /**
22
- * Display layout of the testimonials.
23
- * @type {'cards' | 'list'}
24
- */
25
- layout: { type: String, reflect: true },
26
- };
10
+ static properties = {
11
+ /**
12
+ * Optional section heading.
13
+ * @type {string}
14
+ */
15
+ title: { type: String },
16
+ /**
17
+ * Testimonial items as a JSON string array: `[{"quote":"...","name":"...","role":"...","company":"...","avatar":"..."}]`.
18
+ * @type {string}
19
+ */
20
+ items: { type: String },
21
+ /**
22
+ * Display layout of the testimonials.
23
+ * @type {'cards' | 'list'}
24
+ */
25
+ layout: { type: String, reflect: true },
26
+ };
27
27
 
28
- constructor() {
29
- super();
30
- this.title = "";
31
- this.items = "[]";
32
- this.layout = "cards";
33
- }
28
+ constructor() {
29
+ super();
30
+ this.title = '';
31
+ this.items = '[]';
32
+ this.layout = 'cards';
33
+ }
34
34
 
35
- get _items() {
36
- try {
37
- return JSON.parse(this.items);
38
- } catch {
39
- return [];
40
- }
41
- }
35
+ get _items() {
36
+ try {
37
+ return JSON.parse(this.items);
38
+ } catch {
39
+ return [];
40
+ }
41
+ }
42
42
 
43
- render() {
44
- const items = this._items;
45
- return html`
43
+ render() {
44
+ const items = this._items;
45
+ return html`
46
46
  <section class="testimonials">
47
47
  <grantcodes-container>
48
- ${
49
- this.title
50
- ? html`<h2 class="testimonials__title">${this.title}</h2>`
51
- : null
52
- }
48
+ ${this.title ? html`<h2 class="testimonials__title">${this.title}</h2>` : null}
53
49
  <ul
54
50
  class="testimonials__grid testimonials__grid--${this.layout}"
55
51
  role="list"
56
52
  >
57
53
  ${items.map(
58
- (item) => html`
54
+ (item) => html`
59
55
  <li class="testimonials__item">
60
56
  <grantcodes-card>
61
57
  <blockquote class="testimonials__quote" slot="header">
@@ -65,20 +61,20 @@ export class GrantCodesTestimonials extends LitElement {
65
61
  </blockquote>
66
62
  <footer class="testimonials__attribution" slot="footer">
67
63
  <grantcodes-person
68
- name=${item.name ?? ""}
69
- role=${item.role ?? ""}
70
- company=${item.company ?? ""}
71
- avatar=${item.avatar ?? ""}
64
+ name=${item.name ?? ''}
65
+ role=${item.role ?? ''}
66
+ company=${item.company ?? ''}
67
+ avatar=${item.avatar ?? ''}
72
68
  size="small"
73
69
  ></grantcodes-person>
74
70
  </footer>
75
71
  </grantcodes-card>
76
72
  </li>
77
73
  `,
78
- )}
74
+ )}
79
75
  </ul>
80
76
  </grantcodes-container>
81
77
  </section>
82
78
  `;
83
- }
79
+ }
84
80
  }
@@ -1,59 +1,59 @@
1
1
  *,
2
2
  *::before,
3
3
  *::after {
4
- box-sizing: border-box;
4
+ box-sizing: border-box;
5
5
  }
6
6
 
7
7
  :host {
8
- display: block;
8
+ display: block;
9
9
  }
10
10
 
11
11
  .testimonials {
12
- padding-block: var(--g-spacing-3xl);
12
+ padding-block: var(--g-spacing-3xl);
13
13
  }
14
14
 
15
15
  .testimonials__title {
16
- margin: 0 0 var(--g-spacing-2xl);
17
- font: var(--g-typography-h3-font);
18
- text-align: center;
19
- color: var(--g-color-content-default);
16
+ margin: 0 0 var(--g-spacing-2xl);
17
+ font: var(--g-typography-h3-font);
18
+ text-align: center;
19
+ color: var(--g-color-content-default);
20
20
  }
21
21
 
22
22
  .testimonials__grid--cards {
23
- display: grid;
24
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
25
- gap: var(--g-spacing-lg);
26
- padding: 0;
27
- margin: 0;
28
- list-style: none;
23
+ display: grid;
24
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
25
+ gap: var(--g-spacing-lg);
26
+ padding: 0;
27
+ margin: 0;
28
+ list-style: none;
29
29
  }
30
30
 
31
31
  .testimonials__grid--list {
32
- display: flex;
33
- flex-direction: column;
34
- gap: var(--g-spacing-xl);
35
- max-width: 65ch;
36
- margin: 0 auto;
37
- padding: 0;
38
- list-style: none;
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: var(--g-spacing-xl);
35
+ max-width: 65ch;
36
+ margin: 0 auto;
37
+ padding: 0;
38
+ list-style: none;
39
39
  }
40
40
 
41
41
  .testimonials__quote {
42
- margin: 0;
43
- display: flex;
44
- flex-direction: column;
45
- gap: var(--g-spacing-lg);
46
- height: 100%;
42
+ margin: 0;
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--g-spacing-lg);
46
+ height: 100%;
47
47
  }
48
48
 
49
49
  .testimonials__text {
50
- margin: 0;
51
- font: var(--g-typography-body-default);
52
- font-style: italic;
53
- color: var(--g-color-content-default);
54
- flex: 1;
50
+ margin: 0;
51
+ font: var(--g-typography-body-default);
52
+ font-style: italic;
53
+ color: var(--g-color-content-default);
54
+ flex: 1;
55
55
  }
56
56
 
57
57
  .testimonials__attribution {
58
- margin-top: auto;
58
+ margin-top: auto;
59
59
  }
@@ -1,6 +1,6 @@
1
- import { GrantCodesTestimonials } from "./testimonials.component.js";
1
+ import { GrantCodesTestimonials } from './testimonials.component.js';
2
2
 
3
- export * from "./testimonials.component.js";
3
+ export * from './testimonials.component.js';
4
4
  export default GrantCodesTestimonials;
5
5
 
6
- customElements.define("grantcodes-testimonials", GrantCodesTestimonials);
6
+ customElements.define('grantcodes-testimonials', GrantCodesTestimonials);
@@ -1,9 +1,9 @@
1
- import React from "react";
2
- import { createComponent } from "@lit/react";
3
- import { GrantCodesTestimonials } from "./testimonials.js";
1
+ import { createComponent } from '@lit/react';
2
+ import React from 'react';
3
+ import { GrantCodesTestimonials } from './testimonials.js';
4
4
 
5
5
  export const Testimonials = createComponent({
6
- tagName: "grantcodes-testimonials",
7
- elementClass: GrantCodesTestimonials,
8
- react: React,
6
+ tagName: 'grantcodes-testimonials',
7
+ elementClass: GrantCodesTestimonials,
8
+ react: React,
9
9
  });