@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,393 @@
1
+ import { StoryObj, Meta } from "@storybook/react-vite";
2
+ import { within, expect } from "storybook/test";
3
+
4
+ import Title from "./title";
5
+
6
+ const meta: Meta<typeof Title> = {
7
+ title: "FP.REACT Components/Title",
8
+ component: Title,
9
+ tags: ["version:2.0.0", "autodocs"],
10
+ parameters: {
11
+ actions: { argTypesRegex: "^on.*" },
12
+ docs: {
13
+ description: {
14
+ component: `
15
+ A semantic heading component for document structure and hierarchy.
16
+
17
+ The Title component renders semantic HTML headings (h1-h6) with proper accessibility support,
18
+ ensuring WCAG 2.1 AA compliance by maintaining semantic document structure for screen readers.
19
+
20
+ ## Key Features
21
+
22
+ - **Semantic HTML**: Renders actual heading elements (h1-h6) for proper document outline
23
+ - **Accessibility**: Full ARIA support and proper heading hierarchy
24
+ - **Flexible Styling**: Supports fpkit's UI system, custom classes, and inline styles
25
+ - **Type Safety**: Fully typed with TypeScript
26
+ - **Performance**: Memoized to prevent unnecessary re-renders
27
+
28
+ ## Migration from Heading
29
+
30
+ If you're migrating from the deprecated \`Heading\` component:
31
+
32
+ \`\`\`tsx
33
+ // Before (deprecated):
34
+ <Heading type="h2">Section Title</Heading>
35
+
36
+ // After:
37
+ <Title level="h2">Section Title</Title>
38
+ \`\`\`
39
+
40
+ **Note**: Default level changed from \`h3\` to \`h2\`.
41
+
42
+ 📖 [View Full Documentation](?path=/docs/fp-react-components-title-readme--docs)
43
+ `,
44
+ },
45
+ },
46
+ },
47
+ argTypes: {
48
+ level: {
49
+ control: { type: "select" },
50
+ options: ["h1", "h2", "h3", "h4", "h5", "h6"],
51
+ description: "The semantic heading level to render",
52
+ table: {
53
+ type: { summary: "h1 | h2 | h3 | h4 | h5 | h6" },
54
+ defaultValue: { summary: "h2" },
55
+ },
56
+ },
57
+ children: {
58
+ control: "text",
59
+ description: "The content to display in the heading",
60
+ },
61
+ id: {
62
+ control: "text",
63
+ description: "Unique identifier for the heading (useful for anchor links)",
64
+ },
65
+ ui: {
66
+ control: "text",
67
+ description: "Data attribute for UI framework styling hooks",
68
+ },
69
+ className: {
70
+ control: "text",
71
+ description: "CSS class names to apply",
72
+ },
73
+ },
74
+ args: {
75
+ children: "Default Title",
76
+ },
77
+ } satisfies Meta<typeof Title>;
78
+
79
+ export default meta;
80
+ type Story = StoryObj<typeof Title>;
81
+
82
+ /**
83
+ * Default Title component using h2 (the default heading level).
84
+ */
85
+ export const Default: Story = {
86
+ args: {},
87
+ play: async ({ canvasElement }) => {
88
+ const canvas = within(canvasElement);
89
+ expect(canvas.getByText(/default title/i)).toBeInTheDocument();
90
+ expect(canvas.getByRole("heading", { level: 2 })).toBeInTheDocument();
91
+ },
92
+ };
93
+
94
+ /**
95
+ * Page title using h1. Typically used once per page for the main title.
96
+ */
97
+ export const PageTitle: Story = {
98
+ args: {
99
+ level: "h1",
100
+ children: "Main Page Title",
101
+ },
102
+ parameters: {
103
+ docs: {
104
+ description: {
105
+ story: "Use h1 for the main page title. Each page should have exactly one h1 for proper document structure and SEO.",
106
+ },
107
+ },
108
+ },
109
+ play: async ({ canvasElement }) => {
110
+ const canvas = within(canvasElement);
111
+ expect(canvas.getByRole("heading", { level: 1 })).toHaveTextContent("Main Page Title");
112
+ },
113
+ };
114
+
115
+ /**
116
+ * Section heading using h2. Used for major sections on the page.
117
+ */
118
+ export const SectionHeading: Story = {
119
+ args: {
120
+ level: "h2",
121
+ children: "Section Heading",
122
+ },
123
+ parameters: {
124
+ docs: {
125
+ description: {
126
+ story: "h2 is the default level and commonly used for major sections.",
127
+ },
128
+ },
129
+ },
130
+ play: async ({ canvasElement }) => {
131
+ const canvas = within(canvasElement);
132
+ expect(canvas.getByRole("heading", { level: 2 })).toBeInTheDocument();
133
+ },
134
+ };
135
+
136
+ /**
137
+ * Subsection heading using h3.
138
+ */
139
+ export const SubsectionHeading: Story = {
140
+ args: {
141
+ level: "h3",
142
+ children: "Subsection Heading",
143
+ },
144
+ play: async ({ canvasElement }) => {
145
+ const canvas = within(canvasElement);
146
+ expect(canvas.getByRole("heading", { level: 3 })).toBeInTheDocument();
147
+ },
148
+ };
149
+
150
+ /**
151
+ * H4 heading for nested content.
152
+ */
153
+ export const LevelFour: Story = {
154
+ args: {
155
+ level: "h4",
156
+ children: "Level Four Heading",
157
+ },
158
+ play: async ({ canvasElement }) => {
159
+ const canvas = within(canvasElement);
160
+ expect(canvas.getByRole("heading", { level: 4 })).toBeInTheDocument();
161
+ },
162
+ };
163
+
164
+ /**
165
+ * H5 heading for deeply nested content.
166
+ */
167
+ export const LevelFive: Story = {
168
+ args: {
169
+ level: "h5",
170
+ children: "Level Five Heading",
171
+ },
172
+ play: async ({ canvasElement }) => {
173
+ const canvas = within(canvasElement);
174
+ expect(canvas.getByRole("heading", { level: 5 })).toBeInTheDocument();
175
+ },
176
+ };
177
+
178
+ /**
179
+ * H6 heading - the deepest heading level.
180
+ */
181
+ export const LevelSix: Story = {
182
+ args: {
183
+ level: "h6",
184
+ children: "Level Six Heading",
185
+ },
186
+ play: async ({ canvasElement }) => {
187
+ const canvas = within(canvasElement);
188
+ expect(canvas.getByRole("heading", { level: 6 })).toBeInTheDocument();
189
+ },
190
+ };
191
+
192
+ /**
193
+ * Example of proper heading hierarchy for accessibility.
194
+ */
195
+ export const ProperHierarchy: Story = {
196
+ render: () => (
197
+ <div>
198
+ <Title level="h1">Page Title</Title>
199
+ <p>Introduction paragraph...</p>
200
+
201
+ <Title level="h2">First Major Section</Title>
202
+ <p>Section content...</p>
203
+
204
+ <Title level="h3">Subsection 1.1</Title>
205
+ <p>Subsection content...</p>
206
+
207
+ <Title level="h3">Subsection 1.2</Title>
208
+ <p>Subsection content...</p>
209
+
210
+ <Title level="h2">Second Major Section</Title>
211
+ <p>Section content...</p>
212
+
213
+ <Title level="h3">Subsection 2.1</Title>
214
+ <p>Subsection content...</p>
215
+
216
+ <Title level="h4">Sub-subsection 2.1.1</Title>
217
+ <p>Deep nested content...</p>
218
+ </div>
219
+ ),
220
+ parameters: {
221
+ docs: {
222
+ description: {
223
+ story: `
224
+ **✅ GOOD: Proper heading hierarchy**
225
+
226
+ This example demonstrates correct heading hierarchy:
227
+ - One h1 for the page title
228
+ - h2 for major sections
229
+ - h3 for subsections
230
+ - h4 for nested subsections
231
+ - No skipped levels
232
+
233
+ This structure helps screen reader users navigate the page effectively and improves SEO.
234
+ `,
235
+ },
236
+ },
237
+ },
238
+ play: async ({ canvasElement }) => {
239
+ const canvas = within(canvasElement);
240
+ expect(canvas.getAllByRole("heading", { level: 1 })).toHaveLength(1);
241
+ expect(canvas.getAllByRole("heading", { level: 2 })).toHaveLength(2);
242
+ expect(canvas.getAllByRole("heading", { level: 3 })).toHaveLength(3);
243
+ },
244
+ };
245
+
246
+ /**
247
+ * Example showing improper heading hierarchy (accessibility violation).
248
+ */
249
+ export const ImproperHierarchy: Story = {
250
+ render: () => (
251
+ <div>
252
+ <Title level="h1">Page Title</Title>
253
+ <p>Introduction...</p>
254
+
255
+ {/* ❌ BAD: Skipping from h1 to h4 */}
256
+ <Title level="h4" styles={{ color: "red" }}>
257
+ Skipped h2 and h3 (Accessibility Issue!)
258
+ </Title>
259
+ <p>This violates WCAG guidelines...</p>
260
+ </div>
261
+ ),
262
+ parameters: {
263
+ docs: {
264
+ description: {
265
+ story: `
266
+ **❌ BAD: Improper heading hierarchy**
267
+
268
+ This example shows an accessibility violation - skipping from h1 to h4 without h2 or h3 in between.
269
+
270
+ **Issues:**
271
+ - Confuses screen reader users
272
+ - Violates WCAG 2.4.6 (Headings and Labels)
273
+ - Poor document structure
274
+
275
+ **Solution:** Always maintain sequential heading levels.
276
+ `,
277
+ },
278
+ },
279
+ },
280
+ };
281
+
282
+ /**
283
+ * Title with custom ID for anchor linking.
284
+ */
285
+ export const WithAnchorLink: Story = {
286
+ args: {
287
+ level: "h2",
288
+ id: "getting-started",
289
+ children: "Getting Started",
290
+ },
291
+ parameters: {
292
+ docs: {
293
+ description: {
294
+ story: "Use the `id` prop to create anchor links. Users can link directly to this section with `#getting-started`.",
295
+ },
296
+ },
297
+ },
298
+ play: async ({ canvasElement }) => {
299
+ const canvas = within(canvasElement);
300
+ const heading = canvas.getByRole("heading", { level: 2 });
301
+ expect(heading).toHaveAttribute("id", "getting-started");
302
+ },
303
+ };
304
+
305
+ /**
306
+ * Title with custom styling using fpkit's UI data attribute.
307
+ */
308
+ export const WithUIAttribute: Story = {
309
+ args: {
310
+ level: "h2",
311
+ ui: "section-title",
312
+ children: "Styled Section Title",
313
+ },
314
+ parameters: {
315
+ docs: {
316
+ description: {
317
+ story: "Use the `ui` prop to apply fpkit's component-specific styles via data attributes.",
318
+ },
319
+ },
320
+ },
321
+ play: async ({ canvasElement }) => {
322
+ const canvas = within(canvasElement);
323
+ const heading = canvas.getByRole("heading", { level: 2 });
324
+ expect(heading).toHaveAttribute("data-ui", "section-title");
325
+ },
326
+ };
327
+
328
+ /**
329
+ * Title with custom CSS classes.
330
+ */
331
+ export const WithCustomClasses: Story = {
332
+ args: {
333
+ level: "h2",
334
+ className: "text-primary font-bold",
335
+ children: "Custom Styled Title",
336
+ },
337
+ parameters: {
338
+ docs: {
339
+ description: {
340
+ story: "Apply custom CSS classes using the `className` prop.",
341
+ },
342
+ },
343
+ },
344
+ };
345
+
346
+ /**
347
+ * Title with inline styles.
348
+ */
349
+ export const WithInlineStyles: Story = {
350
+ args: {
351
+ level: "h2",
352
+ styles: {
353
+ color: "#0066cc",
354
+ fontWeight: 700,
355
+ marginBottom: "1rem",
356
+ },
357
+ children: "Inline Styled Title",
358
+ },
359
+ parameters: {
360
+ docs: {
361
+ description: {
362
+ story: "Apply inline styles using the `styles` prop. Useful for dynamic styling or CSS-in-JS.",
363
+ },
364
+ },
365
+ },
366
+ };
367
+
368
+ /**
369
+ * Accessible title with ARIA label for additional context.
370
+ */
371
+ export const WithAriaLabel: Story = {
372
+ args: {
373
+ level: "h2",
374
+ "aria-label": "User dashboard statistics overview",
375
+ children: "Dashboard",
376
+ },
377
+ parameters: {
378
+ docs: {
379
+ description: {
380
+ story: `
381
+ Use \`aria-label\` when the visible text doesn't provide enough context for screen reader users.
382
+
383
+ **Example:** "Dashboard" becomes "User dashboard statistics overview" for assistive technologies.
384
+ `,
385
+ },
386
+ },
387
+ },
388
+ play: async ({ canvasElement }) => {
389
+ const canvas = within(canvasElement);
390
+ const heading = canvas.getByRole("heading", { level: 2 });
391
+ expect(heading).toHaveAttribute("aria-label", "User dashboard statistics overview");
392
+ },
393
+ };
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import React from "react";
4
+
5
+ import Title from "./title";
6
+
7
+ describe("Title Component", () => {
8
+ describe("Rendering", () => {
9
+ it("should render with default h2 level", () => {
10
+ render(<Title>Default Title</Title>);
11
+ const heading = screen.getByRole("heading", { level: 2 });
12
+ expect(heading).toBeInTheDocument();
13
+ expect(heading).toHaveTextContent("Default Title");
14
+ });
15
+
16
+ it("should render children content", () => {
17
+ render(<Title>Test Content</Title>);
18
+ expect(screen.getByText("Test Content")).toBeInTheDocument();
19
+ });
20
+
21
+ it("should render complex children with elements", () => {
22
+ render(
23
+ <Title>
24
+ <span data-testid="icon">📄</span>
25
+ <span>Document Title</span>
26
+ </Title>
27
+ );
28
+ expect(screen.getByTestId("icon")).toBeInTheDocument();
29
+ expect(screen.getByText("Document Title")).toBeInTheDocument();
30
+ });
31
+ });
32
+
33
+ describe("Heading Levels", () => {
34
+ it.each([
35
+ ["h1", 1],
36
+ ["h2", 2],
37
+ ["h3", 3],
38
+ ["h4", 4],
39
+ ["h5", 5],
40
+ ["h6", 6],
41
+ ] as const)("should render %s with level %i", (level, numericLevel) => {
42
+ render(<Title level={level}>Heading {level}</Title>);
43
+ const heading = screen.getByRole("heading", { level: numericLevel });
44
+ expect(heading).toBeInTheDocument();
45
+ expect(heading.tagName.toLowerCase()).toBe(level);
46
+ });
47
+ });
48
+
49
+ describe("Props and Attributes", () => {
50
+ it("should apply id attribute", () => {
51
+ render(<Title id="test-heading">Title with ID</Title>);
52
+ const heading = screen.getByRole("heading", { level: 2 });
53
+ expect(heading).toHaveAttribute("id", "test-heading");
54
+ });
55
+
56
+ it("should apply data-ui attribute", () => {
57
+ render(<Title ui="section-title">Styled Title</Title>);
58
+ const heading = screen.getByRole("heading", { level: 2 });
59
+ expect(heading).toHaveAttribute("data-ui", "section-title");
60
+ });
61
+
62
+ it("should apply className", () => {
63
+ render(<Title className="custom-class">Classed Title</Title>);
64
+ const heading = screen.getByRole("heading", { level: 2 });
65
+ expect(heading).toHaveClass("custom-class");
66
+ });
67
+
68
+ it("should apply multiple class names", () => {
69
+ render(<Title className="class-one class-two">Title</Title>);
70
+ const heading = screen.getByRole("heading", { level: 2 });
71
+ expect(heading).toHaveClass("class-one");
72
+ expect(heading).toHaveClass("class-two");
73
+ });
74
+
75
+ it("should apply inline styles", () => {
76
+ const styles = { color: "red", fontSize: "24px" };
77
+ render(<Title styles={styles}>Styled Title</Title>);
78
+ const heading = screen.getByRole("heading", { level: 2 });
79
+ // Browsers convert color values, so check for rgb format
80
+ expect(heading).toHaveStyle({ color: "rgb(255, 0, 0)", fontSize: "24px" });
81
+ });
82
+ });
83
+
84
+ describe("Accessibility", () => {
85
+ it("should have proper heading role", () => {
86
+ render(<Title level="h1">Accessible Title</Title>);
87
+ const heading = screen.getByRole("heading", { level: 1 });
88
+ expect(heading).toBeInTheDocument();
89
+ });
90
+
91
+ it("should support aria-label", () => {
92
+ render(
93
+ <Title aria-label="Dashboard overview">Dashboard</Title>
94
+ );
95
+ const heading = screen.getByRole("heading", { level: 2 });
96
+ expect(heading).toHaveAttribute("aria-label", "Dashboard overview");
97
+ });
98
+
99
+ it("should support aria-labelledby", () => {
100
+ render(
101
+ <>
102
+ <div id="label-element">Section Label</div>
103
+ <Title aria-labelledby="label-element">Title</Title>
104
+ </>
105
+ );
106
+ const heading = screen.getByRole("heading", { level: 2 });
107
+ expect(heading).toHaveAttribute("aria-labelledby", "label-element");
108
+ });
109
+
110
+ it("should support aria-describedby", () => {
111
+ render(
112
+ <>
113
+ <p id="description">This is a description</p>
114
+ <Title aria-describedby="description">Title</Title>
115
+ </>
116
+ );
117
+ const heading = screen.getByRole("heading", { level: 2 });
118
+ expect(heading).toHaveAttribute("aria-describedby", "description");
119
+ });
120
+
121
+ it("should be findable by accessible name", () => {
122
+ render(<Title>Findable Heading</Title>);
123
+ expect(
124
+ screen.getByRole("heading", { name: "Findable Heading" })
125
+ ).toBeInTheDocument();
126
+ });
127
+ });
128
+
129
+ describe("Ref Forwarding", () => {
130
+ it("should forward ref to the heading element", () => {
131
+ const ref = React.createRef<HTMLHeadingElement>();
132
+ render(<Title ref={ref}>Title with Ref</Title>);
133
+
134
+ expect(ref.current).toBeInstanceOf(HTMLHeadingElement);
135
+ expect(ref.current?.tagName.toLowerCase()).toBe("h2");
136
+ expect(ref.current?.textContent).toBe("Title with Ref");
137
+ });
138
+
139
+ it("should allow focus via ref", () => {
140
+ const ref = React.createRef<HTMLHeadingElement>();
141
+ render(<Title ref={ref} tabIndex={-1}>Focusable Title</Title>);
142
+
143
+ ref.current?.focus();
144
+ expect(ref.current).toHaveFocus();
145
+ });
146
+ });
147
+
148
+ describe("Component API", () => {
149
+ it("should have displayName set", () => {
150
+ expect(Title.displayName).toBe("Title");
151
+ });
152
+
153
+ it("should accept all native heading attributes", () => {
154
+ render(
155
+ <Title
156
+ data-testid="custom-heading"
157
+ tabIndex={0}
158
+ title="Tooltip text"
159
+ >
160
+ Title
161
+ </Title>
162
+ );
163
+
164
+ const heading = screen.getByTestId("custom-heading");
165
+ expect(heading).toHaveAttribute("tabIndex", "0");
166
+ expect(heading).toHaveAttribute("title", "Tooltip text");
167
+ });
168
+ });
169
+
170
+ describe("Edge Cases", () => {
171
+ it("should handle empty string children", () => {
172
+ render(<Title>{""}</Title>);
173
+ const heading = screen.getByRole("heading", { level: 2 });
174
+ expect(heading).toBeInTheDocument();
175
+ expect(heading).toHaveTextContent("");
176
+ });
177
+
178
+ it("should handle null children gracefully", () => {
179
+ render(<Title>{null}</Title>);
180
+ const heading = screen.getByRole("heading", { level: 2 });
181
+ expect(heading).toBeInTheDocument();
182
+ });
183
+
184
+ it("should handle multiple text nodes", () => {
185
+ render(
186
+ <Title>
187
+ Text one
188
+ {" "}
189
+ Text two
190
+ </Title>
191
+ );
192
+ const heading = screen.getByRole("heading", { level: 2 });
193
+ expect(heading).toHaveTextContent("Text one Text two");
194
+ });
195
+
196
+ it("should handle conditional rendering", () => {
197
+ const showIcon = true;
198
+ render(
199
+ <Title>
200
+ {showIcon && <span data-testid="icon">📄</span>}
201
+ Document
202
+ </Title>
203
+ );
204
+ expect(screen.getByTestId("icon")).toBeInTheDocument();
205
+ });
206
+ });
207
+
208
+ describe("Integration with UI Component", () => {
209
+ it("should pass through all UI component props", () => {
210
+ render(
211
+ <Title
212
+ level="h3"
213
+ id="integration-test"
214
+ className="test-class"
215
+ styles={{ margin: "10px" }}
216
+ data-custom="value"
217
+ >
218
+ Integration Test
219
+ </Title>
220
+ );
221
+
222
+ const heading = screen.getByRole("heading", { level: 3 });
223
+ expect(heading).toHaveAttribute("id", "integration-test");
224
+ expect(heading).toHaveClass("test-class");
225
+ expect(heading).toHaveStyle({ margin: "10px" });
226
+ expect(heading).toHaveAttribute("data-custom", "value");
227
+ });
228
+ });
229
+
230
+ describe("Memoization", () => {
231
+ it("should be a memoized component", () => {
232
+ // Title is wrapped with React.memo, so it should have the memo properties
233
+ expect(Title.$$typeof).toBeDefined();
234
+ });
235
+ });
236
+ });
237
+
238
+ describe("Title Component - Backwards Compatibility", () => {
239
+ describe("Migration from Heading", () => {
240
+ it("should work with level prop (new API)", () => {
241
+ render(<Title level="h2">New API</Title>);
242
+ expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent("New API");
243
+ });
244
+
245
+ it("should use h2 as default (changed from h3)", () => {
246
+ // This tests the new default behavior
247
+ render(<Title>Default is now h2</Title>);
248
+ expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument();
249
+ });
250
+ });
251
+ });