@fpkit/acss 0.5.12 → 0.5.13

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 (264) hide show
  1. package/README.md +89 -0
  2. package/libs/{chunk-DV56L5YX.cjs → chunk-2LTJ7HHX.cjs} +4 -4
  3. package/libs/{chunk-EQ67LF46.js → chunk-2Y7W75TT.js} +3 -3
  4. package/libs/{chunk-KKLTUJFB.cjs → chunk-3MKLDCKQ.cjs} +5 -5
  5. package/libs/chunk-3MKLDCKQ.cjs.map +1 -0
  6. package/libs/{chunk-X3EVB7VS.cjs → chunk-5S4ORA4C.cjs} +3 -3
  7. package/libs/{chunk-O6QZBB6G.js → chunk-772NRB75.js} +5 -5
  8. package/libs/chunk-772NRB75.js.map +1 -0
  9. package/libs/{chunk-6BVXFW7U.cjs → chunk-AHDJGCG5.cjs} +3 -3
  10. package/libs/{chunk-E3XP6BEX.cjs → chunk-B7F5FS6D.cjs} +3 -3
  11. package/libs/chunk-D4YLRWAO.cjs +18 -0
  12. package/libs/chunk-D4YLRWAO.cjs.map +1 -0
  13. package/libs/chunk-ETFLFC2S.js +10 -0
  14. package/libs/chunk-ETFLFC2S.js.map +1 -0
  15. package/libs/chunk-GZ4QFPRY.js +9 -0
  16. package/libs/chunk-GZ4QFPRY.js.map +1 -0
  17. package/libs/{chunk-LHVJKDMA.cjs → chunk-J32EZPYD.cjs} +3 -3
  18. package/libs/chunk-JJ43O4Y5.js +8 -0
  19. package/libs/chunk-JJ43O4Y5.js.map +1 -0
  20. package/libs/chunk-KUKIVRC2.js +7 -0
  21. package/libs/chunk-KUKIVRC2.js.map +1 -0
  22. package/libs/chunk-L75OQKEI.cjs +13 -0
  23. package/libs/chunk-L75OQKEI.cjs.map +1 -0
  24. package/libs/{chunk-LL7HTLMS.cjs → chunk-M5RRNTVX.cjs} +3 -3
  25. package/libs/{chunk-LIQJ7ZZR.js → chunk-NGTJDDFO.js} +2 -2
  26. package/libs/chunk-OK5QEIMD.cjs +17 -0
  27. package/libs/chunk-OK5QEIMD.cjs.map +1 -0
  28. package/libs/chunk-P2DC76ZZ.cjs +18 -0
  29. package/libs/chunk-P2DC76ZZ.cjs.map +1 -0
  30. package/libs/chunk-PQ2K3BM6.cjs +17 -0
  31. package/libs/chunk-PQ2K3BM6.cjs.map +1 -0
  32. package/libs/{chunk-QCMV4VQZ.js → chunk-QLZWHAMK.js} +2 -2
  33. package/libs/{chunk-BIP2NY53.js → chunk-RIVUMPOG.js} +2 -2
  34. package/libs/{chunk-ICCKQ2GC.cjs → chunk-ROZI23GS.cjs} +4 -4
  35. package/libs/{chunk-NHYXGV3L.js → chunk-SMYRLO3E.js} +2 -2
  36. package/libs/{chunk-5ZM4XL44.js → chunk-TYRCEX2L.js} +2 -2
  37. package/libs/chunk-VUH3FXGJ.js +11 -0
  38. package/libs/chunk-VUH3FXGJ.js.map +1 -0
  39. package/libs/{chunk-PPOOBUOS.js → chunk-XBA562WW.js} +2 -2
  40. package/libs/{chunk-QVV34QEH.cjs → chunk-XTQKWY7W.cjs} +3 -3
  41. package/libs/{chunk-YWOYVRFT.js → chunk-ZANSFMTD.js} +3 -3
  42. package/libs/components/alert/alert.css +1 -1
  43. package/libs/components/alert/alert.css.map +1 -1
  44. package/libs/components/alert/alert.min.css +2 -2
  45. package/libs/components/badge/badge.css +1 -1
  46. package/libs/components/badge/badge.css.map +1 -1
  47. package/libs/components/badge/badge.min.css +2 -2
  48. package/libs/components/breadcrumbs/breadcrumb.cjs +9 -5
  49. package/libs/components/breadcrumbs/breadcrumb.d.cts +271 -32
  50. package/libs/components/breadcrumbs/breadcrumb.d.ts +271 -32
  51. package/libs/components/breadcrumbs/breadcrumb.js +3 -3
  52. package/libs/components/button.cjs +4 -4
  53. package/libs/components/button.d.cts +2 -2
  54. package/libs/components/button.d.ts +2 -2
  55. package/libs/components/button.js +2 -2
  56. package/libs/components/buttons/button.css +1 -1
  57. package/libs/components/buttons/button.css.map +1 -1
  58. package/libs/components/buttons/button.min.css +2 -2
  59. package/libs/components/card.cjs +7 -7
  60. package/libs/components/card.d.cts +277 -33
  61. package/libs/components/card.d.ts +277 -33
  62. package/libs/components/card.js +2 -2
  63. package/libs/components/cards/card.css +1 -1
  64. package/libs/components/cards/card.css.map +1 -1
  65. package/libs/components/cards/card.min.css +2 -2
  66. package/libs/components/details/details.css +1 -1
  67. package/libs/components/details/details.css.map +1 -1
  68. package/libs/components/details/details.min.css +2 -2
  69. package/libs/components/dialog/dialog.cjs +7 -7
  70. package/libs/components/dialog/dialog.css +1 -1
  71. package/libs/components/dialog/dialog.css.map +1 -1
  72. package/libs/components/dialog/dialog.d.cts +88 -34
  73. package/libs/components/dialog/dialog.d.ts +88 -34
  74. package/libs/components/dialog/dialog.js +5 -5
  75. package/libs/components/dialog/dialog.min.css +2 -2
  76. package/libs/components/form/fields.cjs +4 -4
  77. package/libs/components/form/fields.d.cts +2 -2
  78. package/libs/components/form/fields.d.ts +2 -2
  79. package/libs/components/form/fields.js +2 -2
  80. package/libs/components/form/textarea.cjs +4 -4
  81. package/libs/components/form/textarea.d.cts +2 -2
  82. package/libs/components/form/textarea.d.ts +2 -2
  83. package/libs/components/form/textarea.js +2 -2
  84. package/libs/components/heading/heading.cjs +3 -3
  85. package/libs/components/heading/heading.d.cts +3 -14
  86. package/libs/components/heading/heading.d.ts +3 -14
  87. package/libs/components/heading/heading.js +2 -2
  88. package/libs/components/icons/icon.cjs +4 -4
  89. package/libs/components/icons/icon.d.cts +148 -4
  90. package/libs/components/icons/icon.d.ts +148 -4
  91. package/libs/components/icons/icon.js +2 -2
  92. package/libs/components/images/img.css +1 -1
  93. package/libs/components/images/img.css.map +1 -1
  94. package/libs/components/images/img.min.css +2 -2
  95. package/libs/components/link/link.cjs +4 -4
  96. package/libs/components/link/link.d.cts +2 -2
  97. package/libs/components/link/link.d.ts +2 -2
  98. package/libs/components/link/link.js +2 -2
  99. package/libs/components/list/list.cjs +5 -5
  100. package/libs/components/list/list.d.cts +3 -3
  101. package/libs/components/list/list.d.ts +3 -3
  102. package/libs/components/list/list.js +2 -2
  103. package/libs/components/modal.cjs +4 -4
  104. package/libs/components/modal.js +3 -3
  105. package/libs/components/nav/nav.cjs +7 -7
  106. package/libs/components/nav/nav.d.cts +2 -2
  107. package/libs/components/nav/nav.d.ts +2 -2
  108. package/libs/components/nav/nav.js +3 -3
  109. package/libs/components/text/text.cjs +5 -5
  110. package/libs/components/text/text.d.cts +2 -2
  111. package/libs/components/text/text.d.ts +2 -2
  112. package/libs/components/text/text.js +2 -2
  113. package/libs/heading-3648c538.d.ts +250 -0
  114. package/libs/hooks.cjs +7 -0
  115. package/libs/hooks.d.cts +5 -0
  116. package/libs/hooks.d.ts +5 -0
  117. package/libs/hooks.js +3 -0
  118. package/libs/icons.cjs +3 -3
  119. package/libs/icons.d.cts +1 -1
  120. package/libs/icons.d.ts +1 -1
  121. package/libs/icons.js +2 -2
  122. package/libs/index.cjs +112 -91
  123. package/libs/index.cjs.map +1 -1
  124. package/libs/index.css +1 -1
  125. package/libs/index.css.map +1 -1
  126. package/libs/index.d.cts +515 -31
  127. package/libs/index.d.ts +515 -31
  128. package/libs/index.js +31 -19
  129. package/libs/index.js.map +1 -1
  130. package/libs/ui-645f95b5.d.ts +285 -0
  131. package/package.json +2 -83
  132. package/src/components/README-UI.mdx +416 -0
  133. package/src/components/alert/ACCESSIBILITY.md +319 -0
  134. package/src/components/alert/README.mdx +475 -19
  135. package/src/components/alert/alert.scss +113 -6
  136. package/src/components/alert/alert.stories.tsx +372 -0
  137. package/src/components/alert/alert.test.tsx +762 -0
  138. package/src/components/alert/alert.tsx +331 -66
  139. package/src/components/alert/views/alert-actions.tsx +13 -0
  140. package/src/components/alert/views/alert-content.tsx +17 -0
  141. package/src/components/alert/views/alert-icon.tsx +53 -0
  142. package/src/components/alert/views/alert-screen-reader-text.tsx +30 -0
  143. package/src/components/alert/views/alert-title.tsx +23 -0
  144. package/src/components/alert/views/alert-view.tsx +158 -0
  145. package/src/components/alert/views/index.ts +12 -0
  146. package/src/components/badge/badge.mdx +186 -49
  147. package/src/components/badge/badge.scss +20 -2
  148. package/src/components/badge/badge.stories.tsx +160 -14
  149. package/src/components/badge/badge.test.tsx +179 -0
  150. package/src/components/badge/badge.tsx +97 -4
  151. package/src/components/breadcrumbs/README.mdx +364 -45
  152. package/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap +152 -0
  153. package/src/components/breadcrumbs/breadcrumb.stories.tsx +7 -3
  154. package/src/components/breadcrumbs/breadcrumb.test.tsx +490 -0
  155. package/src/components/breadcrumbs/breadcrumb.tsx +427 -170
  156. package/src/components/buttons/button.scss +34 -31
  157. package/src/components/buttons/button.stories.tsx +35 -0
  158. package/src/components/cards/README.mdx +657 -0
  159. package/src/components/cards/card.scss +22 -0
  160. package/src/components/cards/card.stories.tsx +167 -5
  161. package/src/components/cards/card.test.tsx +360 -20
  162. package/src/components/cards/card.tsx +200 -79
  163. package/src/components/cards/card.types.ts +135 -0
  164. package/src/components/cards/card.utils.ts +79 -0
  165. package/src/components/details/ACCESSIBILITY-REVIEW-LIVE.md +1050 -0
  166. package/src/components/details/ACCESSIBILITY-REVIEW.md +502 -0
  167. package/src/components/details/README.mdx +437 -69
  168. package/src/components/details/details.scss +16 -7
  169. package/src/components/details/details.test.tsx +385 -0
  170. package/src/components/details/details.tsx +101 -69
  171. package/src/components/details/details.types.ts +76 -0
  172. package/src/components/dialog/README.mdx +513 -110
  173. package/src/components/dialog/dialog-modal.tsx +79 -56
  174. package/src/components/dialog/dialog.scss +53 -3
  175. package/src/components/dialog/dialog.stories.tsx +10 -7
  176. package/src/components/dialog/dialog.test.tsx +450 -0
  177. package/src/components/dialog/dialog.tsx +69 -59
  178. package/src/components/dialog/dialog.types.ts +133 -0
  179. package/src/components/dialog/views/dialog-footer.tsx +54 -11
  180. package/src/components/dialog/views/dialog-header.tsx +20 -15
  181. package/src/components/heading/heading.stories.tsx +44 -4
  182. package/src/components/heading/heading.tsx +89 -23
  183. package/src/components/icons/README.mdx +332 -0
  184. package/src/components/icons/icon.stories.tsx +74 -1
  185. package/src/components/icons/icon.tsx +89 -1
  186. package/src/components/icons/types.ts +47 -0
  187. package/src/components/images/README.mdx +340 -24
  188. package/src/components/images/img.scss +19 -3
  189. package/src/components/images/img.stories.tsx +424 -15
  190. package/src/components/images/img.test.tsx +354 -25
  191. package/src/components/images/img.tsx +186 -63
  192. package/src/components/images/img.types.ts +211 -0
  193. package/src/components/title/MIGRATION.md +199 -0
  194. package/src/components/title/README.md +326 -0
  195. package/src/components/title/README.mdx +452 -0
  196. package/src/components/title/title.stories.tsx +393 -0
  197. package/src/components/title/title.test.tsx +251 -0
  198. package/src/components/title/title.tsx +219 -0
  199. package/src/components/ui.stories.tsx +894 -0
  200. package/src/components/ui.test.tsx +559 -0
  201. package/src/components/ui.tsx +266 -15
  202. package/src/components/word-count/README.md +240 -0
  203. package/src/hooks.ts +1 -0
  204. package/src/index.ts +10 -2
  205. package/src/sass/_properties.scss +1 -0
  206. package/src/styles/alert/alert.css +94 -4
  207. package/src/styles/alert/alert.css.map +1 -1
  208. package/src/styles/badge/badge.css +20 -2
  209. package/src/styles/badge/badge.css.map +1 -1
  210. package/src/styles/buttons/button.css +31 -31
  211. package/src/styles/buttons/button.css.map +1 -1
  212. package/src/styles/cards/card.css +16 -0
  213. package/src/styles/cards/card.css.map +1 -1
  214. package/src/styles/details/details.css +19 -8
  215. package/src/styles/details/details.css.map +1 -1
  216. package/src/styles/dialog/dialog.css +43 -2
  217. package/src/styles/dialog/dialog.css.map +1 -1
  218. package/src/styles/images/img.css +15 -3
  219. package/src/styles/images/img.css.map +1 -1
  220. package/src/styles/index.css +240 -51
  221. package/src/styles/index.css.map +1 -1
  222. package/src/test/setup.d.ts +9 -0
  223. package/src/test/setup.ts +53 -1
  224. package/libs/chunk-6TE5QEVE.cjs +0 -13
  225. package/libs/chunk-6TE5QEVE.cjs.map +0 -1
  226. package/libs/chunk-7K76RW2A.cjs +0 -18
  227. package/libs/chunk-7K76RW2A.cjs.map +0 -1
  228. package/libs/chunk-BSPKFLO4.js +0 -8
  229. package/libs/chunk-BSPKFLO4.js.map +0 -1
  230. package/libs/chunk-BV5CLH44.cjs +0 -18
  231. package/libs/chunk-BV5CLH44.cjs.map +0 -1
  232. package/libs/chunk-DKGJHKGW.js +0 -9
  233. package/libs/chunk-DKGJHKGW.js.map +0 -1
  234. package/libs/chunk-ECLD37WN.cjs +0 -16
  235. package/libs/chunk-ECLD37WN.cjs.map +0 -1
  236. package/libs/chunk-HYBZBN4G.js +0 -8
  237. package/libs/chunk-HYBZBN4G.js.map +0 -1
  238. package/libs/chunk-KKLTUJFB.cjs.map +0 -1
  239. package/libs/chunk-M5QL5TAE.cjs +0 -14
  240. package/libs/chunk-M5QL5TAE.cjs.map +0 -1
  241. package/libs/chunk-NE6YXTMC.js +0 -7
  242. package/libs/chunk-NE6YXTMC.js.map +0 -1
  243. package/libs/chunk-O6QZBB6G.js.map +0 -1
  244. package/libs/chunk-SXVZSWX6.js +0 -11
  245. package/libs/chunk-SXVZSWX6.js.map +0 -1
  246. package/libs/ui-9a6f9f8d.d.ts +0 -24
  247. package/src/components/cards/README.md +0 -80
  248. package/src/components/dialog/hooks/useClickOutside.ts +0 -33
  249. /package/libs/{chunk-DV56L5YX.cjs.map → chunk-2LTJ7HHX.cjs.map} +0 -0
  250. /package/libs/{chunk-EQ67LF46.js.map → chunk-2Y7W75TT.js.map} +0 -0
  251. /package/libs/{chunk-X3EVB7VS.cjs.map → chunk-5S4ORA4C.cjs.map} +0 -0
  252. /package/libs/{chunk-6BVXFW7U.cjs.map → chunk-AHDJGCG5.cjs.map} +0 -0
  253. /package/libs/{chunk-E3XP6BEX.cjs.map → chunk-B7F5FS6D.cjs.map} +0 -0
  254. /package/libs/{chunk-LHVJKDMA.cjs.map → chunk-J32EZPYD.cjs.map} +0 -0
  255. /package/libs/{chunk-LL7HTLMS.cjs.map → chunk-M5RRNTVX.cjs.map} +0 -0
  256. /package/libs/{chunk-LIQJ7ZZR.js.map → chunk-NGTJDDFO.js.map} +0 -0
  257. /package/libs/{chunk-QCMV4VQZ.js.map → chunk-QLZWHAMK.js.map} +0 -0
  258. /package/libs/{chunk-BIP2NY53.js.map → chunk-RIVUMPOG.js.map} +0 -0
  259. /package/libs/{chunk-ICCKQ2GC.cjs.map → chunk-ROZI23GS.cjs.map} +0 -0
  260. /package/libs/{chunk-NHYXGV3L.js.map → chunk-SMYRLO3E.js.map} +0 -0
  261. /package/libs/{chunk-5ZM4XL44.js.map → chunk-TYRCEX2L.js.map} +0 -0
  262. /package/libs/{chunk-PPOOBUOS.js.map → chunk-XBA562WW.js.map} +0 -0
  263. /package/libs/{chunk-QVV34QEH.cjs.map → chunk-XTQKWY7W.cjs.map} +0 -0
  264. /package/libs/{chunk-YWOYVRFT.js.map → chunk-ZANSFMTD.js.map} +0 -0
@@ -0,0 +1,385 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import Details from "./details";
5
+ import React from "react";
6
+
7
+ describe("Details Component", () => {
8
+ describe("Rendering", () => {
9
+ it("should render with summary and children", () => {
10
+ render(
11
+ <Details summary="Test Summary">
12
+ <p>Test Content</p>
13
+ </Details>
14
+ );
15
+
16
+ expect(screen.getByText("Test Summary")).toBeInTheDocument();
17
+ expect(screen.getByText("Test Content")).toBeInTheDocument();
18
+ });
19
+
20
+ it("should render with icon", () => {
21
+ const TestIcon = () => <svg data-testid="test-icon" />;
22
+ render(
23
+ <Details summary="Test Summary" icon={<TestIcon />}>
24
+ <p>Content</p>
25
+ </Details>
26
+ );
27
+
28
+ expect(screen.getByTestId("test-icon")).toBeInTheDocument();
29
+ });
30
+
31
+ it("should apply custom classes", () => {
32
+ render(
33
+ <Details summary="Test" classes="custom-class">
34
+ <p>Content</p>
35
+ </Details>
36
+ );
37
+
38
+ const detailsElement = screen.getByText("Test").closest("details");
39
+ expect(detailsElement).toHaveClass("custom-class");
40
+ });
41
+
42
+ it("should apply custom styles", () => {
43
+ render(
44
+ <Details summary="Test" styles={{ backgroundColor: "red" }}>
45
+ <p>Content</p>
46
+ </Details>
47
+ );
48
+
49
+ const detailsElement = screen.getByText("Test").closest("details");
50
+ // Check that style attribute contains the property
51
+ const styleAttr = detailsElement?.getAttribute("style") || "";
52
+ expect(styleAttr).toContain("background-color");
53
+ });
54
+
55
+ it("should render with aria-label when provided", () => {
56
+ render(
57
+ <Details summary="Test" ariaLabel="Custom label">
58
+ <p>Content</p>
59
+ </Details>
60
+ );
61
+
62
+ const detailsElement = screen.getByText("Test").closest("details");
63
+ expect(detailsElement).toHaveAttribute("aria-label", "Custom label");
64
+ });
65
+ });
66
+
67
+ describe("Open/Close State", () => {
68
+ it("should be closed by default", () => {
69
+ render(
70
+ <Details summary="Test">
71
+ <p>Content</p>
72
+ </Details>
73
+ );
74
+
75
+ const detailsElement = screen.getByText("Test").closest("details");
76
+ expect(detailsElement).not.toHaveAttribute("open");
77
+ });
78
+
79
+ it("should be open when open prop is true", () => {
80
+ render(
81
+ <Details summary="Test" open>
82
+ <p>Content</p>
83
+ </Details>
84
+ );
85
+
86
+ const detailsElement = screen.getByText("Test").closest("details");
87
+ expect(detailsElement).toHaveAttribute("open");
88
+ });
89
+
90
+ it("should toggle open state on summary click", async () => {
91
+ const user = userEvent.setup();
92
+ render(
93
+ <Details summary="Test">
94
+ <p>Content</p>
95
+ </Details>
96
+ );
97
+
98
+ const summaryElement = screen.getByText("Test");
99
+ const detailsElement = summaryElement.closest("details")!;
100
+
101
+ // Initially closed
102
+ expect(detailsElement).not.toHaveAttribute("open");
103
+
104
+ // Click to open
105
+ await user.click(summaryElement);
106
+ expect(detailsElement).toHaveAttribute("open");
107
+
108
+ // Click to close
109
+ await user.click(summaryElement);
110
+ expect(detailsElement).not.toHaveAttribute("open");
111
+ });
112
+ });
113
+
114
+ describe("Keyboard Interaction", () => {
115
+ it("should be keyboard accessible with focusable summary", () => {
116
+ render(
117
+ <Details summary="Test">
118
+ <p>Content</p>
119
+ </Details>
120
+ );
121
+
122
+ const summaryElement = screen.getByText("Test");
123
+ summaryElement.focus();
124
+
125
+ // Summary element should be focusable for keyboard navigation
126
+ expect(summaryElement).toHaveFocus();
127
+ expect(summaryElement.tagName).toBe("SUMMARY");
128
+ });
129
+
130
+ it("should support keyboard interaction through native browser behavior", () => {
131
+ // Note: Keyboard interaction (Space, Enter) on <summary> is handled
132
+ // natively by the browser and is part of the HTML5 spec.
133
+ // These interactions are tested in Storybook interaction tests
134
+ // where the browser's native behavior is fully available.
135
+ render(
136
+ <Details summary="Test">
137
+ <p>Content</p>
138
+ </Details>
139
+ );
140
+
141
+ const summaryElement = screen.getByText("Test");
142
+ const detailsElement = summaryElement.closest("details");
143
+
144
+ // Verify semantic HTML structure that enables keyboard support
145
+ expect(summaryElement.tagName).toBe("SUMMARY");
146
+ expect(detailsElement?.tagName).toBe("DETAILS");
147
+ });
148
+
149
+ it("should call onToggle callback when interaction occurs", async () => {
150
+ const user = userEvent.setup();
151
+ const handleToggle = vi.fn();
152
+
153
+ render(
154
+ <Details summary="Test" onToggle={handleToggle}>
155
+ <p>Content</p>
156
+ </Details>
157
+ );
158
+
159
+ const summaryElement = screen.getByText("Test");
160
+
161
+ // Click interaction
162
+ await user.click(summaryElement);
163
+
164
+ // Callback should be invoked
165
+ expect(handleToggle).toHaveBeenCalled();
166
+ });
167
+ });
168
+
169
+ describe("Event Handlers", () => {
170
+ it("should call onToggle when toggled", async () => {
171
+ const user = userEvent.setup();
172
+ const handleToggle = vi.fn();
173
+
174
+ render(
175
+ <Details summary="Test" onToggle={handleToggle}>
176
+ <p>Content</p>
177
+ </Details>
178
+ );
179
+
180
+ const summaryElement = screen.getByText("Test");
181
+ await user.click(summaryElement);
182
+
183
+ expect(handleToggle).toHaveBeenCalledTimes(1);
184
+ expect(handleToggle).toHaveBeenCalledWith(
185
+ expect.objectContaining({
186
+ type: "toggle",
187
+ })
188
+ );
189
+ });
190
+
191
+ it("should call onPointerDown on summary pointer down", async () => {
192
+ const user = userEvent.setup();
193
+ const handlePointerDown = vi.fn();
194
+
195
+ render(
196
+ <Details summary="Test" onPointerDown={handlePointerDown}>
197
+ <p>Content</p>
198
+ </Details>
199
+ );
200
+
201
+ const summaryElement = screen.getByText("Test");
202
+ await user.pointer({ target: summaryElement, keys: "[MouseLeft>]" });
203
+
204
+ expect(handlePointerDown).toHaveBeenCalled();
205
+ });
206
+
207
+ it("should not call onToggle multiple times incorrectly", async () => {
208
+ const user = userEvent.setup();
209
+ const handleToggle = vi.fn();
210
+
211
+ render(
212
+ <Details summary="Test" onToggle={handleToggle}>
213
+ <p>Content</p>
214
+ </Details>
215
+ );
216
+
217
+ const summaryElement = screen.getByText("Test");
218
+ await user.click(summaryElement);
219
+
220
+ // Should only be called once per toggle
221
+ expect(handleToggle).toHaveBeenCalledTimes(1);
222
+ });
223
+ });
224
+
225
+ describe("Accordion Behavior", () => {
226
+ it("should support name attribute for accordion groups", () => {
227
+ render(
228
+ <>
229
+ <Details summary="First" name="accordion-group">
230
+ <p>Content 1</p>
231
+ </Details>
232
+ <Details summary="Second" name="accordion-group">
233
+ <p>Content 2</p>
234
+ </Details>
235
+ </>
236
+ );
237
+
238
+ const firstDetails = screen.getByText("First").closest("details");
239
+ const secondDetails = screen.getByText("Second").closest("details");
240
+
241
+ expect(firstDetails).toHaveAttribute("name", "accordion-group");
242
+ expect(secondDetails).toHaveAttribute("name", "accordion-group");
243
+ });
244
+
245
+ it("should configure accordion group with name attribute", () => {
246
+ // Note: The exclusive open/close behavior of accordion groups
247
+ // is handled natively by browsers and tested in Storybook.
248
+ // Here we verify the name attribute is correctly applied.
249
+ render(
250
+ <>
251
+ <Details summary="First" name="test-accordion">
252
+ <p>Content 1</p>
253
+ </Details>
254
+ <Details summary="Second" name="test-accordion">
255
+ <p>Content 2</p>
256
+ </Details>
257
+ </>
258
+ );
259
+
260
+ const firstDetails = screen.getByText("First").closest("details");
261
+ const secondDetails = screen.getByText("Second").closest("details");
262
+
263
+ // Both should have the same name for accordion grouping
264
+ expect(firstDetails).toHaveAttribute("name", "test-accordion");
265
+ expect(secondDetails).toHaveAttribute("name", "test-accordion");
266
+ });
267
+ });
268
+
269
+ describe("Accessibility", () => {
270
+ it("should use semantic details element", () => {
271
+ render(
272
+ <Details summary="Test">
273
+ <p>Content</p>
274
+ </Details>
275
+ );
276
+
277
+ const detailsElement = screen.getByText("Test").closest("details");
278
+ expect(detailsElement?.tagName).toBe("DETAILS");
279
+ });
280
+
281
+ it("should use semantic summary element", () => {
282
+ render(
283
+ <Details summary="Test">
284
+ <p>Content</p>
285
+ </Details>
286
+ );
287
+
288
+ const summaryElement = screen.getByText("Test");
289
+ expect(summaryElement.tagName).toBe("SUMMARY");
290
+ });
291
+
292
+ it("should wrap content in section element", () => {
293
+ render(
294
+ <Details summary="Test">
295
+ <p data-testid="content">Content</p>
296
+ </Details>
297
+ );
298
+
299
+ const contentElement = screen.getByTestId("content");
300
+ const sectionElement = contentElement.closest("section");
301
+ expect(sectionElement).toBeInTheDocument();
302
+ });
303
+
304
+ it("should support ref forwarding", () => {
305
+ const ref = React.createRef<HTMLDetailsElement>();
306
+ render(
307
+ <Details summary="Test" ref={ref}>
308
+ <p>Content</p>
309
+ </Details>
310
+ );
311
+
312
+ expect(ref.current).toBeInstanceOf(HTMLDetailsElement);
313
+ });
314
+ });
315
+
316
+ describe("Complex Content", () => {
317
+ it("should render React nodes as summary", () => {
318
+ render(
319
+ <Details
320
+ summary={
321
+ <>
322
+ <span>Icon</span>
323
+ <span>Text</span>
324
+ </>
325
+ }
326
+ >
327
+ <p>Content</p>
328
+ </Details>
329
+ );
330
+
331
+ expect(screen.getByText("Icon")).toBeInTheDocument();
332
+ expect(screen.getByText("Text")).toBeInTheDocument();
333
+ });
334
+
335
+ it("should render complex children", () => {
336
+ render(
337
+ <Details summary="Test">
338
+ <div>
339
+ <h3>Heading</h3>
340
+ <p>Paragraph</p>
341
+ <ul>
342
+ <li>Item 1</li>
343
+ <li>Item 2</li>
344
+ </ul>
345
+ </div>
346
+ </Details>
347
+ );
348
+
349
+ expect(screen.getByText("Heading")).toBeInTheDocument();
350
+ expect(screen.getByText("Paragraph")).toBeInTheDocument();
351
+ expect(screen.getByText("Item 1")).toBeInTheDocument();
352
+ expect(screen.getByText("Item 2")).toBeInTheDocument();
353
+ });
354
+ });
355
+
356
+ describe("Edge Cases", () => {
357
+ it("should handle undefined optional props", () => {
358
+ render(
359
+ <Details summary="Test">
360
+ <p>Content</p>
361
+ </Details>
362
+ );
363
+
364
+ const detailsElement = screen.getByText("Test").closest("details");
365
+ expect(detailsElement).toBeInTheDocument();
366
+ });
367
+
368
+ it("should handle empty children", () => {
369
+ render(<Details summary="Test">{null}</Details>);
370
+
371
+ expect(screen.getByText("Test")).toBeInTheDocument();
372
+ });
373
+
374
+ it("should pass through additional HTML attributes", () => {
375
+ render(
376
+ <Details summary="Test" data-testid="custom-details" id="details-id">
377
+ <p>Content</p>
378
+ </Details>
379
+ );
380
+
381
+ const detailsElement = screen.getByTestId("custom-details");
382
+ expect(detailsElement).toHaveAttribute("id", "details-id");
383
+ });
384
+ });
385
+ });
@@ -1,78 +1,110 @@
1
1
  import UI from "#components/ui";
2
- import React from "react";
2
+ import React, { useCallback } from "react";
3
+ import type { DetailsProps } from "./details.types";
3
4
 
4
- type DetailsProps = {
5
- /**
6
- * The summary text shown for the details.
7
- * Required.
8
- */
9
- summary: React.ReactNode;
10
-
11
- /**
12
- * The aria-label element for accessibility.
13
- */
14
- ariaLabel: string;
15
- } & React.ComponentProps<"details"> &
16
- Partial<React.ComponentProps<typeof UI>>;
17
-
18
- /**3
19
- * Details component props interface.
5
+ /**
6
+ * Details - A progressive disclosure component using native HTML `<details>` element.
7
+ *
8
+ * This component wraps the native `<details>` and `<summary>` elements to provide
9
+ * an accessible, semantic way to show and hide content. It supports accordion behavior
10
+ * through the `name` attribute and includes proper keyboard navigation out of the box.
11
+ *
12
+ * ## Key Features:
13
+ * - **Semantic HTML**: Uses native `<details>` for built-in accessibility
14
+ * - **Keyboard Support**: Space/Enter to toggle, fully accessible by default
15
+ * - **Accordion Mode**: Group multiple details with `name` for exclusive expansion
16
+ * - **Customizable**: Supports icons, custom styles, and event handlers
17
+ *
18
+ * ## Accessibility:
19
+ * - ✅ WCAG 2.1 AA compliant using semantic HTML
20
+ * - Native keyboard support (Space, Enter)
21
+ * - ✅ Screen reader compatible (announced as "disclosure" or "expandable")
22
+ * - ✅ Focus indicators automatically applied via CSS
23
+ * - ✅ `aria-expanded` managed automatically by browser
20
24
  *
21
- * @param {React.CSSProperties} [styles] - CSS styles object.
22
- * @param {string} [classes] - Classnames string.
23
- * @param {boolean} [open] - Whether the details is open.
24
- * @param {(e: React.PointerEvent<HTMLDetailsElement>) => void} [onToggle] - onToggle callback.
25
- * @param {(e: React.PointerEvent<HTMLDetailsElement>) => void} [onPointerDown] - onPointerDown callback.
26
- * @param {ReactNode} children - The content inside the details.
27
- * @param {string} [ariaLabel] - aria-label for accessibility.
28
- * @param {React.Ref<any>} [ref] - Ref object.
29
- * @param {Object} props - Other props.
25
+ * @example
26
+ * ```tsx
27
+ * // Basic usage
28
+ * <Details summary="Click to expand">
29
+ * <p>Hidden content here</p>
30
+ * </Details>
31
+ * ```
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * // With icon and custom styling
36
+ * <Details
37
+ * summary="Shipping Information"
38
+ * icon={<ChevronDownIcon />}
39
+ * classes="custom-details"
40
+ * onToggle={(e) => console.log('Open:', e.currentTarget.open)}
41
+ * >
42
+ * <p>Ships within 2-3 business days</p>
43
+ * </Details>
44
+ * ```
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * // Accordion mode - only one open at a time
49
+ * <Details name="faq" summary="Question 1">Answer 1</Details>
50
+ * <Details name="faq" summary="Question 2">Answer 2</Details>
51
+ * <Details name="faq" summary="Question 3">Answer 3</Details>
52
+ * ```
30
53
  */
31
- export const Details = ({
32
- summary,
33
- icon,
34
- styles,
35
- classes,
36
- ariaLabel,
37
- name,
38
- open,
39
- onPointerDown,
40
- onToggle,
41
- children,
42
- ref,
43
- ...props
44
- }: DetailsProps) => {
45
- const defaultStyles: React.CSSProperties = { ...styles };
54
+ export const Details = React.forwardRef<HTMLDetailsElement, DetailsProps>(
55
+ (
56
+ {
57
+ summary,
58
+ icon,
59
+ styles,
60
+ classes,
61
+ ariaLabel,
62
+ name,
63
+ open,
64
+ onPointerDown,
65
+ onToggle,
66
+ children,
67
+ ...props
68
+ },
69
+ ref
70
+ ) => {
71
+ // Memoize callbacks to prevent unnecessary re-renders of child components
72
+ const handlePointerDown = useCallback(
73
+ (e: React.PointerEvent<HTMLElement>) => {
74
+ onPointerDown?.(e as React.PointerEvent<HTMLDetailsElement>);
75
+ },
76
+ [onPointerDown]
77
+ );
46
78
 
47
- const onPointerDownCallback = (e: React.PointerEvent<HTMLDetailsElement>) => {
48
- if (onPointerDown) onPointerDown?.(e);
49
- if (onPointerDown) onPointerDown?.(e);
50
- };
79
+ const handleToggle = useCallback(
80
+ (e: React.SyntheticEvent<HTMLDetailsElement>) => {
81
+ onToggle?.(e);
82
+ },
83
+ [onToggle]
84
+ );
51
85
 
52
- const onToggleCallback = (e: React.PointerEvent<HTMLDetailsElement>) => {
53
- if (onToggle) onPointerDown?.(e);
54
- };
55
- return (
56
- <UI
57
- as="details"
58
- style={defaultStyles}
59
- className={classes}
60
- onToggle={onToggleCallback}
61
- ref={ref}
62
- open={open}
63
- aria-label={ariaLabel || "Details dropdown"}
64
- // aria-roledescription="detail accordion"
65
- name={name}
66
- {...props}
67
- >
68
- <UI as="summary" onPointerDown={onPointerDownCallback}>
69
- {icon}
70
- {summary}
86
+ return (
87
+ <UI
88
+ as="details"
89
+ styles={styles}
90
+ classes={classes}
91
+ onToggle={handleToggle}
92
+ ref={ref}
93
+ open={open}
94
+ aria-label={ariaLabel}
95
+ name={name}
96
+ {...props}
97
+ >
98
+ <UI as="summary" onPointerDown={handlePointerDown}>
99
+ {icon}
100
+ {summary}
101
+ </UI>
102
+ <UI as="section">{children}</UI>
71
103
  </UI>
72
- <UI as="section">{children}</UI>
73
- </UI>
74
- );
75
- };
104
+ );
105
+ }
106
+ );
76
107
 
77
- export default Details;
78
108
  Details.displayName = "Details";
109
+
110
+ export default Details;
@@ -0,0 +1,76 @@
1
+ import React from "react";
2
+ import UI from "#components/ui";
3
+
4
+ /**
5
+ * Props for the Details component.
6
+ *
7
+ * Combines native HTML details element props with custom styling and interaction handlers.
8
+ * The Details component uses the native `<details>` element for progressive disclosure,
9
+ * providing built-in keyboard support and semantic HTML.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <Details
14
+ * summary="Click to expand"
15
+ * icon={<ChevronIcon />}
16
+ * onToggle={(e) => console.log('Toggled:', e.currentTarget.open)}
17
+ * >
18
+ * <p>Hidden content revealed when opened</p>
19
+ * </Details>
20
+ * ```
21
+ */
22
+ export type DetailsProps = {
23
+ /**
24
+ * The summary text or element shown in the clickable header.
25
+ * This is always visible and acts as the toggle control.
26
+ *
27
+ * @required
28
+ * @example
29
+ * ```tsx
30
+ * summary="Shipping Information"
31
+ * // or
32
+ * summary={<><Icon /> Shipping Information</>}
33
+ * ```
34
+ */
35
+ summary: React.ReactNode;
36
+
37
+ /**
38
+ * Optional icon displayed before the summary text.
39
+ * Commonly used for chevron/arrow indicators.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * icon={<ChevronDownIcon />}
44
+ * ```
45
+ */
46
+ icon?: React.ReactNode;
47
+
48
+ /**
49
+ * Accessible label for screen readers.
50
+ * If not provided, the native `<details>` semantic will be used.
51
+ *
52
+ * Note: Native `<details>` elements are already semantic and announced properly
53
+ * by screen readers. Only provide this if you need to override the default behavior.
54
+ *
55
+ * @optional
56
+ * @example
57
+ * ```tsx
58
+ * ariaLabel="Product details section"
59
+ * ```
60
+ */
61
+ ariaLabel?: string;
62
+
63
+ /**
64
+ * Groups multiple details elements into an accordion where only one can be open.
65
+ * Multiple details elements with the same `name` will behave as a mutually exclusive group.
66
+ *
67
+ * @optional
68
+ * @example
69
+ * ```tsx
70
+ * <Details name="faq-accordion" summary="Question 1">...</Details>
71
+ * <Details name="faq-accordion" summary="Question 2">...</Details>
72
+ * ```
73
+ */
74
+ name?: string;
75
+ } & React.ComponentProps<"details"> &
76
+ Partial<React.ComponentProps<typeof UI>>;