@fpkit/acss 0.5.13 → 0.6.1

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 (280) hide show
  1. package/libs/{chunk-PQ2K3BM6.cjs → chunk-2NRIP6RB.cjs} +3 -3
  2. package/libs/chunk-33PNJ4LO.cjs +15 -0
  3. package/libs/chunk-33PNJ4LO.cjs.map +1 -0
  4. package/libs/chunk-4BZKFPEC.cjs +17 -0
  5. package/libs/chunk-4BZKFPEC.cjs.map +1 -0
  6. package/libs/{chunk-772NRB75.js → chunk-5QD3DWFI.js} +2 -2
  7. package/libs/chunk-6SAHIYCZ.js +7 -0
  8. package/libs/chunk-6SAHIYCZ.js.map +1 -0
  9. package/libs/{chunk-3MKLDCKQ.cjs → chunk-6WTC4JXH.cjs} +3 -3
  10. package/libs/chunk-75QHTLFO.js +7 -0
  11. package/libs/chunk-75QHTLFO.js.map +1 -0
  12. package/libs/{chunk-ZANSFMTD.js → chunk-7XPFW7CB.js} +3 -3
  13. package/libs/chunk-BFK62VX5.js +5 -0
  14. package/libs/chunk-BFK62VX5.js.map +1 -0
  15. package/libs/{chunk-ROZI23GS.cjs → chunk-DKTHCQ5P.cjs} +4 -4
  16. package/libs/chunk-E2AJURUW.cjs +13 -0
  17. package/libs/chunk-E2AJURUW.cjs.map +1 -0
  18. package/libs/{chunk-L75OQKEI.cjs → chunk-ENTCUJ3A.cjs} +3 -3
  19. package/libs/chunk-ENTCUJ3A.cjs.map +1 -0
  20. package/libs/chunk-F5EYMVQM.js +10 -0
  21. package/libs/chunk-F5EYMVQM.js.map +1 -0
  22. package/libs/chunk-FVROL3V5.js +9 -0
  23. package/libs/chunk-FVROL3V5.js.map +1 -0
  24. package/libs/chunk-GT77BX4L.cjs +17 -0
  25. package/libs/chunk-GT77BX4L.cjs.map +1 -0
  26. package/libs/chunk-GUJSMQ3V.cjs +16 -0
  27. package/libs/chunk-GUJSMQ3V.cjs.map +1 -0
  28. package/libs/chunk-HHLNOC5T.js +7 -0
  29. package/libs/chunk-HHLNOC5T.js.map +1 -0
  30. package/libs/chunk-HRRHPLER.js +8 -0
  31. package/libs/chunk-HRRHPLER.js.map +1 -0
  32. package/libs/chunk-IEB64SWY.js +8 -0
  33. package/libs/chunk-IEB64SWY.js.map +1 -0
  34. package/libs/{chunk-NGTJDDFO.js → chunk-IQ76HGVP.js} +2 -2
  35. package/libs/chunk-IRLFZ3OL.js +9 -0
  36. package/libs/chunk-IRLFZ3OL.js.map +1 -0
  37. package/libs/{chunk-JJ43O4Y5.js → chunk-KK47SYZI.js} +2 -2
  38. package/libs/chunk-O3JIHC5M.cjs +15 -0
  39. package/libs/chunk-O3JIHC5M.cjs.map +1 -0
  40. package/libs/chunk-O5XAJ7BY.cjs +18 -0
  41. package/libs/chunk-O5XAJ7BY.cjs.map +1 -0
  42. package/libs/chunk-OVWLQYMK.js +10 -0
  43. package/libs/chunk-OVWLQYMK.js.map +1 -0
  44. package/libs/chunk-PNWIRCG3.cjs +7 -0
  45. package/libs/chunk-PNWIRCG3.cjs.map +1 -0
  46. package/libs/{chunk-D4YLRWAO.cjs → chunk-QVW6W76L.cjs} +6 -6
  47. package/libs/chunk-T4T6GWYQ.cjs +17 -0
  48. package/libs/chunk-T4T6GWYQ.cjs.map +1 -0
  49. package/libs/chunk-TON2YGMD.cjs +9 -0
  50. package/libs/chunk-TON2YGMD.cjs.map +1 -0
  51. package/libs/chunk-UEPAWMDF.js +8 -0
  52. package/libs/chunk-UEPAWMDF.js.map +1 -0
  53. package/libs/{chunk-LT5KZ2QW.cjs → chunk-US2I5GI7.cjs} +3 -3
  54. package/libs/{chunk-B7F5FS6D.cjs → chunk-W2UIN7EV.cjs} +3 -3
  55. package/libs/{chunk-P2DC76ZZ.cjs → chunk-W5TKWBFC.cjs} +3 -3
  56. package/libs/chunk-WXBFBWYF.cjs +16 -0
  57. package/libs/chunk-WXBFBWYF.cjs.map +1 -0
  58. package/libs/{chunk-VUH3FXGJ.js → chunk-X3JCTEPD.js} +5 -5
  59. package/libs/chunk-X5LGFCWG.js +9 -0
  60. package/libs/chunk-X5LGFCWG.js.map +1 -0
  61. package/libs/{chunk-5M57K4SW.js → chunk-Y2PFDELK.js} +2 -2
  62. package/libs/{chunk-ETFLFC2S.js → chunk-ZFJ4U45S.js} +2 -2
  63. package/libs/{component-props-a8a2f97e.d.ts → component-props-67d978a2.d.ts} +4 -4
  64. package/libs/components/alert/alert.css +1 -1
  65. package/libs/components/alert/alert.css.map +1 -1
  66. package/libs/components/alert/alert.min.css +2 -2
  67. package/libs/components/breadcrumbs/breadcrumb.cjs +6 -6
  68. package/libs/components/breadcrumbs/breadcrumb.d.cts +11 -11
  69. package/libs/components/breadcrumbs/breadcrumb.d.ts +11 -11
  70. package/libs/components/breadcrumbs/breadcrumb.js +3 -3
  71. package/libs/components/button.cjs +6 -4
  72. package/libs/components/button.d.cts +97 -4
  73. package/libs/components/button.d.ts +97 -4
  74. package/libs/components/button.js +4 -2
  75. package/libs/components/card.cjs +7 -7
  76. package/libs/components/card.d.cts +14 -14
  77. package/libs/components/card.d.ts +14 -14
  78. package/libs/components/card.js +2 -2
  79. package/libs/components/dialog/dialog.cjs +9 -7
  80. package/libs/components/dialog/dialog.d.cts +3 -3
  81. package/libs/components/dialog/dialog.d.ts +3 -3
  82. package/libs/components/dialog/dialog.js +7 -5
  83. package/libs/components/form/fields.cjs +4 -4
  84. package/libs/components/form/fields.d.cts +16 -7
  85. package/libs/components/form/fields.d.ts +16 -7
  86. package/libs/components/form/fields.js +2 -2
  87. package/libs/components/form/inputs.cjs +6 -4
  88. package/libs/components/form/inputs.d.cts +50 -2
  89. package/libs/components/form/inputs.d.ts +50 -2
  90. package/libs/components/form/inputs.js +4 -2
  91. package/libs/components/form/textarea.cjs +5 -4
  92. package/libs/components/form/textarea.d.cts +32 -23
  93. package/libs/components/form/textarea.d.ts +32 -23
  94. package/libs/components/form/textarea.js +3 -2
  95. package/libs/components/heading/heading.cjs +3 -3
  96. package/libs/components/heading/heading.d.cts +2 -2
  97. package/libs/components/heading/heading.d.ts +2 -2
  98. package/libs/components/heading/heading.js +2 -2
  99. package/libs/components/icons/icon.cjs +4 -4
  100. package/libs/components/icons/icon.d.cts +38 -38
  101. package/libs/components/icons/icon.d.ts +38 -38
  102. package/libs/components/icons/icon.js +2 -2
  103. package/libs/components/link/link.cjs +4 -4
  104. package/libs/components/link/link.css +1 -1
  105. package/libs/components/link/link.css.map +1 -1
  106. package/libs/components/link/link.d.cts +3 -19
  107. package/libs/components/link/link.d.ts +3 -19
  108. package/libs/components/link/link.js +2 -2
  109. package/libs/components/link/link.min.css +2 -2
  110. package/libs/components/list/list.cjs +5 -5
  111. package/libs/components/list/list.css +1 -0
  112. package/libs/components/list/list.css.map +1 -0
  113. package/libs/components/list/list.d.cts +120 -33
  114. package/libs/components/list/list.d.ts +120 -33
  115. package/libs/components/list/list.js +2 -2
  116. package/libs/components/list/list.min.css +3 -0
  117. package/libs/components/modal.cjs +6 -4
  118. package/libs/components/modal.d.cts +8 -8
  119. package/libs/components/modal.d.ts +8 -8
  120. package/libs/components/modal.js +5 -3
  121. package/libs/components/nav/nav.cjs +7 -7
  122. package/libs/components/nav/nav.css +1 -1
  123. package/libs/components/nav/nav.css.map +1 -1
  124. package/libs/components/nav/nav.d.cts +550 -34
  125. package/libs/components/nav/nav.d.ts +550 -34
  126. package/libs/components/nav/nav.js +3 -3
  127. package/libs/components/nav/nav.min.css +2 -2
  128. package/libs/components/popover/popover.d.cts +5 -5
  129. package/libs/components/popover/popover.d.ts +5 -5
  130. package/libs/components/tables/table.cjs +5 -5
  131. package/libs/components/tables/table.d.cts +8 -8
  132. package/libs/components/tables/table.d.ts +8 -8
  133. package/libs/components/tables/table.js +2 -2
  134. package/libs/components/tag/tag.css +1 -1
  135. package/libs/components/tag/tag.css.map +1 -1
  136. package/libs/components/tag/tag.min.css +2 -2
  137. package/libs/components/text/text.cjs +5 -5
  138. package/libs/components/text/text.d.cts +5 -5
  139. package/libs/components/text/text.d.ts +5 -5
  140. package/libs/components/text/text.js +2 -2
  141. package/libs/form.types-d25ebfac.d.ts +233 -0
  142. package/libs/{heading-3648c538.d.ts → heading-7446cb46.d.ts} +8 -8
  143. package/libs/hooks.cjs +9 -4
  144. package/libs/hooks.d.cts +137 -3
  145. package/libs/hooks.d.ts +137 -3
  146. package/libs/hooks.js +4 -3
  147. package/libs/icons.cjs +3 -3
  148. package/libs/icons.d.cts +2 -2
  149. package/libs/icons.d.ts +2 -2
  150. package/libs/icons.js +2 -2
  151. package/libs/index.cjs +53 -51
  152. package/libs/index.cjs.map +1 -1
  153. package/libs/index.css +1 -1
  154. package/libs/index.css.map +1 -1
  155. package/libs/index.d.cts +338 -49
  156. package/libs/index.d.ts +338 -49
  157. package/libs/index.js +24 -22
  158. package/libs/index.js.map +1 -1
  159. package/libs/link-5192f411.d.ts +323 -0
  160. package/libs/list.types-d26de310.d.ts +245 -0
  161. package/libs/{ui-645f95b5.d.ts → ui-d01b50d4.d.ts} +16 -12
  162. package/package.json +4 -6
  163. package/src/components/alert/alert.scss +1 -4
  164. package/src/components/breadcrumbs/breadcrumb.tsx +4 -1
  165. package/src/components/buttons/README.mdx +102 -1
  166. package/src/components/buttons/button.stories.tsx +106 -0
  167. package/src/components/buttons/button.tsx +82 -52
  168. package/src/components/dialog/dialog-a11y-review.md +653 -0
  169. package/src/components/form/README.mdx +725 -43
  170. package/src/components/form/WCAG-REVIEW.md +654 -0
  171. package/src/components/form/fields.tsx +10 -1
  172. package/src/components/form/form.stories.tsx +604 -23
  173. package/src/components/form/form.tsx +204 -63
  174. package/src/components/form/form.types.ts +378 -0
  175. package/src/components/form/input.stories.tsx +71 -3
  176. package/src/components/form/inputs.tsx +159 -67
  177. package/src/components/form/select.tsx +122 -66
  178. package/src/components/form/textarea.tsx +120 -73
  179. package/src/components/fp.tsx +86 -11
  180. package/src/components/link/README.mdx +923 -0
  181. package/src/components/link/link.scss +79 -26
  182. package/src/components/link/link.stories.tsx +383 -30
  183. package/src/components/link/link.test.tsx +677 -0
  184. package/src/components/link/link.tsx +163 -57
  185. package/src/components/link/link.types.ts +261 -0
  186. package/src/components/list/README.mdx +764 -0
  187. package/src/components/list/list.scss +285 -0
  188. package/src/components/list/list.stories.tsx +514 -27
  189. package/src/components/list/list.test.tsx +554 -0
  190. package/src/components/list/list.tsx +153 -51
  191. package/src/components/list/list.types.ts +255 -0
  192. package/src/components/nav/ACCESSIBILITY.md +649 -0
  193. package/src/components/nav/README.mdx +782 -0
  194. package/src/components/nav/nav.scss +37 -4
  195. package/src/components/nav/nav.stories.tsx +44 -6
  196. package/src/components/nav/nav.tsx +302 -51
  197. package/src/components/nav/nav.types.ts +308 -0
  198. package/src/components/tag/README.mdx +426 -0
  199. package/src/components/tag/tag.scss +101 -27
  200. package/src/components/tag/tag.stories.tsx +384 -10
  201. package/src/components/tag/tag.test.tsx +210 -0
  202. package/src/components/tag/tag.tsx +106 -9
  203. package/src/components/tag/tag.types.ts +107 -0
  204. package/src/components/ui.tsx +8 -3
  205. package/src/hooks/use-disabled-state.test.tsx +536 -0
  206. package/src/hooks/use-disabled-state.ts +246 -0
  207. package/src/hooks/useDisabledState.md +393 -0
  208. package/src/hooks.ts +6 -0
  209. package/src/index.scss +2 -0
  210. package/src/index.ts +2 -1
  211. package/src/sass/_globals.scss +2 -7
  212. package/src/styles/alert/alert.css +1 -3
  213. package/src/styles/alert/alert.css.map +1 -1
  214. package/src/styles/index.css +461 -81
  215. package/src/styles/index.css.map +1 -1
  216. package/src/styles/link/link.css +45 -28
  217. package/src/styles/link/link.css.map +1 -1
  218. package/src/styles/list/list.css +214 -0
  219. package/src/styles/list/list.css.map +1 -0
  220. package/src/styles/nav/nav.css +32 -6
  221. package/src/styles/nav/nav.css.map +1 -1
  222. package/src/styles/tag/tag.css +113 -35
  223. package/src/styles/tag/tag.css.map +1 -1
  224. package/src/styles/utilities/_disabled.scss +58 -0
  225. package/src/types/shared.ts +43 -6
  226. package/src/utils/accessibility.ts +109 -0
  227. package/libs/chunk-2LTJ7HHX.cjs +0 -18
  228. package/libs/chunk-2LTJ7HHX.cjs.map +0 -1
  229. package/libs/chunk-2Y7W75TT.js +0 -9
  230. package/libs/chunk-2Y7W75TT.js.map +0 -1
  231. package/libs/chunk-5S4ORA4C.cjs +0 -15
  232. package/libs/chunk-5S4ORA4C.cjs.map +0 -1
  233. package/libs/chunk-AHDJGCG5.cjs +0 -15
  234. package/libs/chunk-AHDJGCG5.cjs.map +0 -1
  235. package/libs/chunk-BHRQBJRY.js +0 -8
  236. package/libs/chunk-BHRQBJRY.js.map +0 -1
  237. package/libs/chunk-GZ4QFPRY.js +0 -9
  238. package/libs/chunk-GZ4QFPRY.js.map +0 -1
  239. package/libs/chunk-IYUN2EW3.cjs +0 -15
  240. package/libs/chunk-IYUN2EW3.cjs.map +0 -1
  241. package/libs/chunk-J32EZPYD.cjs +0 -15
  242. package/libs/chunk-J32EZPYD.cjs.map +0 -1
  243. package/libs/chunk-KUKIVRC2.js +0 -7
  244. package/libs/chunk-KUKIVRC2.js.map +0 -1
  245. package/libs/chunk-L75OQKEI.cjs.map +0 -1
  246. package/libs/chunk-M5RRNTVX.cjs +0 -15
  247. package/libs/chunk-M5RRNTVX.cjs.map +0 -1
  248. package/libs/chunk-OK5QEIMD.cjs +0 -17
  249. package/libs/chunk-OK5QEIMD.cjs.map +0 -1
  250. package/libs/chunk-P7TTEYCD.js +0 -7
  251. package/libs/chunk-P7TTEYCD.js.map +0 -1
  252. package/libs/chunk-QLZWHAMK.js +0 -8
  253. package/libs/chunk-QLZWHAMK.js.map +0 -1
  254. package/libs/chunk-RIVUMPOG.js +0 -8
  255. package/libs/chunk-RIVUMPOG.js.map +0 -1
  256. package/libs/chunk-S7BABR7Z.cjs +0 -13
  257. package/libs/chunk-S7BABR7Z.cjs.map +0 -1
  258. package/libs/chunk-SMYRLO3E.js +0 -8
  259. package/libs/chunk-SMYRLO3E.js.map +0 -1
  260. package/libs/chunk-TYRCEX2L.js +0 -8
  261. package/libs/chunk-TYRCEX2L.js.map +0 -1
  262. package/libs/chunk-XBA562WW.js +0 -8
  263. package/libs/chunk-XBA562WW.js.map +0 -1
  264. package/libs/chunk-XTQKWY7W.cjs +0 -32
  265. package/libs/chunk-XTQKWY7W.cjs.map +0 -1
  266. package/libs/inputs-f3a216db.d.ts +0 -45
  267. /package/libs/{chunk-PQ2K3BM6.cjs.map → chunk-2NRIP6RB.cjs.map} +0 -0
  268. /package/libs/{chunk-772NRB75.js.map → chunk-5QD3DWFI.js.map} +0 -0
  269. /package/libs/{chunk-3MKLDCKQ.cjs.map → chunk-6WTC4JXH.cjs.map} +0 -0
  270. /package/libs/{chunk-ZANSFMTD.js.map → chunk-7XPFW7CB.js.map} +0 -0
  271. /package/libs/{chunk-ROZI23GS.cjs.map → chunk-DKTHCQ5P.cjs.map} +0 -0
  272. /package/libs/{chunk-NGTJDDFO.js.map → chunk-IQ76HGVP.js.map} +0 -0
  273. /package/libs/{chunk-JJ43O4Y5.js.map → chunk-KK47SYZI.js.map} +0 -0
  274. /package/libs/{chunk-D4YLRWAO.cjs.map → chunk-QVW6W76L.cjs.map} +0 -0
  275. /package/libs/{chunk-LT5KZ2QW.cjs.map → chunk-US2I5GI7.cjs.map} +0 -0
  276. /package/libs/{chunk-B7F5FS6D.cjs.map → chunk-W2UIN7EV.cjs.map} +0 -0
  277. /package/libs/{chunk-P2DC76ZZ.cjs.map → chunk-W5TKWBFC.cjs.map} +0 -0
  278. /package/libs/{chunk-VUH3FXGJ.js.map → chunk-X3JCTEPD.js.map} +0 -0
  279. /package/libs/{chunk-5M57K4SW.js.map → chunk-Y2PFDELK.js.map} +0 -0
  280. /package/libs/{chunk-ETFLFC2S.js.map → chunk-ZFJ4U45S.js.map} +0 -0
@@ -1,49 +1,630 @@
1
+ import React from "react";
1
2
  import { StoryObj, Meta } from "@storybook/react-vite";
2
- import { within, expect } from "storybook/test";
3
+ import { within, expect, userEvent, fn } from "storybook/test";
4
+ import { useState } from "react";
3
5
 
4
6
  import Form from "./form";
5
7
  import "./form.scss";
6
8
 
7
- const meta: Meta<typeof Form> = {
8
- title: "FP.REACT Forms/Examples",
9
- tags: ["beta"],
10
- component: Form,
9
+ // Type assertion to resolve React type compatibility issues
10
+ const FormComponent = Form as unknown as typeof Form;
11
+
12
+ const meta: Meta<typeof FormComponent> = {
13
+ title: "FP.REACT Forms/Form",
14
+ tags: ["rc", "autodocs"],
15
+ component: FormComponent,
11
16
  parameters: {
12
17
  docs: {
13
18
  description: {
14
- component: "Form description here...",
19
+ component: `
20
+ An accessible HTML form wrapper with validation support and compound component pattern.
21
+ Provides proper ARIA attributes, form submission handling, and validation state management.
22
+
23
+ ## Features
24
+ - ✅ WCAG 2.1 AA compliant with proper ARIA attributes
25
+ - ✅ Compound component pattern (Form.Field, Form.Input, etc.)
26
+ - ✅ Form submission and validation state management
27
+ - ✅ Keyboard navigation support
28
+ - ✅ Controlled and uncontrolled form patterns
29
+ `,
15
30
  },
16
31
  },
17
32
  },
18
33
  args: {
19
- children: "Link",
20
- name: "my-form",
34
+ name: "contact-form",
35
+ "aria-label": "Contact form",
21
36
  },
22
- } as Story;
37
+ } as Meta;
23
38
 
24
39
  export default meta;
25
40
  type Story = StoryObj<typeof Form>;
26
41
 
27
- export const FormComponent: Story = {
42
+ // Mock submit handler for stories
43
+ const handleSubmit = fn();
44
+
45
+ /**
46
+ * Basic form example with required fields
47
+ */
48
+ export const BasicForm: Story = {
28
49
  args: {
50
+ onSubmit: handleSubmit,
29
51
  children: (
30
52
  <>
31
- <Form.Field label="Name" labelFor="name" id="name-field">
32
- <Form.Input id="name" name="name" />
33
- </Form.Field>
34
- <Form.Field label="Email" labelFor="email">
35
- <Form.Input id="email" name="email" type="email" />
36
- </Form.Field>
37
- <Form.Field label="Message" labelFor="message">
38
- <Form.Textarea id="message" name="message" />
39
- </Form.Field>
40
- <button type="submit">Submit Form</button>
53
+ <FormComponent.Field label="Name" labelFor="name" required>
54
+ <FormComponent.Input id="name" name="name" required />
55
+ </FormComponent.Field>
56
+ <FormComponent.Field label="Email" labelFor="email" required>
57
+ <FormComponent.Input id="email" name="email" type="email" required />
58
+ </FormComponent.Field>
59
+ <FormComponent.Field label="Message" labelFor="message">
60
+ <FormComponent.Textarea id="message" name="message" rows={4} />
61
+ </FormComponent.Field>
62
+ <button type="submit">Submit</button>
41
63
  </>
42
64
  ),
43
65
  },
44
- play: async ({ canvasElement }) => {
66
+ play: async ({ canvasElement, step }) => {
45
67
  const canvas = within(canvasElement);
46
- const form = canvas.getByRole("form");
47
- await expect(form).toBeInTheDocument();
68
+
69
+ await step("Form renders correctly", async () => {
70
+ const form = canvas.getByRole("form");
71
+ expect(form).toBeInTheDocument();
72
+ });
73
+
74
+ await step("Required fields are marked", async () => {
75
+ expect(
76
+ canvas.getByText("*", { selector: ".field-required" })
77
+ ).toBeInTheDocument();
78
+ });
79
+
80
+ await step("All inputs are accessible", async () => {
81
+ expect(canvas.getByLabelText(/name/i)).toBeInTheDocument();
82
+ expect(canvas.getByLabelText(/email/i)).toBeInTheDocument();
83
+ expect(canvas.getByLabelText(/message/i)).toBeInTheDocument();
84
+ });
85
+
86
+ await step("Submit button is present", async () => {
87
+ expect(
88
+ canvas.getByRole("button", { name: /submit/i })
89
+ ).toBeInTheDocument();
90
+ });
48
91
  },
49
92
  } as Story;
93
+
94
+ /**
95
+ * Form with validation states and error messages
96
+ */
97
+ export const WithValidation: Story = {
98
+ render: function ValidationExample() {
99
+ const [email, setEmail] = useState("");
100
+ const [emailError, setEmailError] = useState("");
101
+
102
+ const validateEmail = (value: string) => {
103
+ if (!value) {
104
+ setEmailError("Email is required");
105
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
106
+ setEmailError("Please enter a valid email address");
107
+ } else {
108
+ setEmailError("");
109
+ }
110
+ };
111
+
112
+ return (
113
+ <FormComponent
114
+ aria-label="Registration form"
115
+ onSubmit={(e) => {
116
+ e.preventDefault();
117
+ handleSubmit(e);
118
+ }}
119
+ >
120
+ <FormComponent.Field
121
+ label="Email"
122
+ labelFor="email-validation"
123
+ required
124
+ errorMessage={emailError}
125
+ >
126
+ <FormComponent.Input
127
+ id="email-validation"
128
+ name="email"
129
+ type="email"
130
+ value={email}
131
+ onChange={(e) => setEmail(e.target.value)}
132
+ onBlur={(e) => validateEmail(e.target.value)}
133
+ validationState={emailError ? "invalid" : email ? "valid" : "none"}
134
+ required
135
+ />
136
+ </FormComponent.Field>
137
+ <button type="submit">Register</button>
138
+ </FormComponent>
139
+ );
140
+ },
141
+ play: async ({ canvasElement, step }) => {
142
+ const canvas = within(canvasElement);
143
+ const user = userEvent.setup();
144
+
145
+ await step("Enter invalid email", async () => {
146
+ const emailInput = canvas.getByLabelText(/email/i);
147
+ await user.type(emailInput, "invalid-email");
148
+ await user.tab();
149
+ });
150
+
151
+ await step("Error message is displayed", async () => {
152
+ expect(
153
+ canvas.getByText(/please enter a valid email/i)
154
+ ).toBeInTheDocument();
155
+ });
156
+
157
+ await step("Input has aria-invalid", async () => {
158
+ const emailInput = canvas.getByLabelText(/email/i);
159
+ expect(emailInput).toHaveAttribute("aria-invalid", "true");
160
+ });
161
+ },
162
+ };
163
+
164
+ /**
165
+ * Form with hint text to guide users
166
+ */
167
+ export const WithHintText: Story = {
168
+ args: {
169
+ "aria-label": "Account creation form",
170
+ children: (
171
+ <>
172
+ <FormComponent.Field
173
+ label="Username"
174
+ labelFor="username"
175
+ required
176
+ hintText="Must be 3-20 characters, letters and numbers only"
177
+ >
178
+ <FormComponent.Input
179
+ id="username"
180
+ name="username"
181
+ minLength={3}
182
+ maxLength={20}
183
+ pattern="[a-zA-Z0-9]+"
184
+ required
185
+ />
186
+ </FormComponent.Field>
187
+ <FormComponent.Field
188
+ label="Password"
189
+ labelFor="password"
190
+ required
191
+ hintText="Minimum 8 characters, include uppercase, lowercase, and number"
192
+ >
193
+ <FormComponent.Input
194
+ id="password"
195
+ name="password"
196
+ type="password"
197
+ minLength={8}
198
+ required
199
+ />
200
+ </FormComponent.Field>
201
+ <button type="submit">Create Account</button>
202
+ </>
203
+ ),
204
+ },
205
+ play: async ({ canvasElement, step }) => {
206
+ const canvas = within(canvasElement);
207
+
208
+ await step("Hint text is visible", async () => {
209
+ expect(canvas.getByText(/must be 3-20 characters/i)).toBeInTheDocument();
210
+ expect(canvas.getByText(/minimum 8 characters/i)).toBeInTheDocument();
211
+ });
212
+
213
+ await step("Hint text is associated with inputs", async () => {
214
+ const usernameInput = canvas.getByLabelText(/username/i);
215
+ expect(usernameInput).toHaveAttribute(
216
+ "aria-describedby",
217
+ "username-hint"
218
+ );
219
+ });
220
+ },
221
+ };
222
+
223
+ /**
224
+ * Form with select dropdown
225
+ */
226
+ export const WithSelect: Story = {
227
+ args: {
228
+ "aria-label": "Profile form",
229
+ children: (
230
+ <>
231
+ <FormComponent.Field label="Full Name" labelFor="fullname" required>
232
+ <FormComponent.Input id="fullname" name="fullname" required />
233
+ </FormComponent.Field>
234
+ <FormComponent.Field label="Country" labelFor="country" required>
235
+ <FormComponent.Select id="country" name="country" required>
236
+ <FormComponent.Select.Option value="">
237
+ Select a country
238
+ </FormComponent.Select.Option>
239
+ <FormComponent.Select.Option value="us">
240
+ United States
241
+ </FormComponent.Select.Option>
242
+ <FormComponent.Select.Option value="ca">
243
+ Canada
244
+ </FormComponent.Select.Option>
245
+ <FormComponent.Select.Option value="uk">
246
+ United Kingdom
247
+ </FormComponent.Select.Option>
248
+ <FormComponent.Select.Option value="au">
249
+ Australia
250
+ </FormComponent.Select.Option>
251
+ </FormComponent.Select>
252
+ </FormComponent.Field>
253
+ <button type="submit">Save Profile</button>
254
+ </>
255
+ ),
256
+ },
257
+ play: async ({ canvasElement, step }) => {
258
+ const canvas = within(canvasElement);
259
+ const user = userEvent.setup();
260
+
261
+ await step("Select is accessible", async () => {
262
+ const select = canvas.getByLabelText(/country/i);
263
+ expect(select).toBeInTheDocument();
264
+ });
265
+
266
+ await step("Select can be changed", async () => {
267
+ const select = canvas.getByLabelText(/country/i);
268
+ await user.selectOptions(select, "us");
269
+ expect(select).toHaveValue("us");
270
+ });
271
+ },
272
+ };
273
+
274
+ /**
275
+ * Form with optional fields
276
+ */
277
+ export const WithOptionalFields: Story = {
278
+ args: {
279
+ "aria-label": "Contact preferences",
280
+ children: (
281
+ <>
282
+ <FormComponent.Field label="Email" labelFor="email-req" required>
283
+ <FormComponent.Input
284
+ id="email-req"
285
+ name="email"
286
+ type="email"
287
+ required
288
+ />
289
+ </FormComponent.Field>
290
+ <FormComponent.Field label="Phone" labelFor="phone-opt" optional>
291
+ <FormComponent.Input id="phone-opt" name="phone" type="tel" />
292
+ </FormComponent.Field>
293
+ <FormComponent.Field label="Address" labelFor="address-opt" optional>
294
+ <FormComponent.Textarea id="address-opt" name="address" rows={3} />
295
+ </FormComponent.Field>
296
+ <button type="submit">Save Preferences</button>
297
+ </>
298
+ ),
299
+ },
300
+ play: async ({ canvasElement, step }) => {
301
+ const canvas = within(canvasElement);
302
+
303
+ await step("Required field is marked with asterisk", async () => {
304
+ expect(
305
+ canvas.getByText("*", { selector: ".field-required" })
306
+ ).toBeInTheDocument();
307
+ });
308
+
309
+ await step("Optional fields are marked", async () => {
310
+ const optionalMarkers = canvas.getAllByText("(optional)");
311
+ expect(optionalMarkers).toHaveLength(2);
312
+ });
313
+ },
314
+ };
315
+
316
+ /**
317
+ * Form submission loading state
318
+ */
319
+ export const LoadingState: Story = {
320
+ render: function LoadingStateExample() {
321
+ const [isSubmitting, setIsSubmitting] = useState(false);
322
+
323
+ const handleFormSubmit = (e: React.FormEvent) => {
324
+ e.preventDefault();
325
+ setIsSubmitting(true);
326
+ setTimeout(() => setIsSubmitting(false), 2000);
327
+ };
328
+
329
+ return (
330
+ <FormComponent
331
+ aria-label="Submission form"
332
+ onSubmit={handleFormSubmit}
333
+ status={isSubmitting ? "submitting" : "idle"}
334
+ >
335
+ <FormComponent.Field label="Name" labelFor="loading-name" required>
336
+ <FormComponent.Input
337
+ id="loading-name"
338
+ name="name"
339
+ disabled={isSubmitting}
340
+ required
341
+ />
342
+ </FormComponent.Field>
343
+ <FormComponent.Field label="Email" labelFor="loading-email" required>
344
+ <FormComponent.Input
345
+ id="loading-email"
346
+ name="email"
347
+ type="email"
348
+ disabled={isSubmitting}
349
+ required
350
+ />
351
+ </FormComponent.Field>
352
+ <button type="submit" disabled={isSubmitting}>
353
+ {isSubmitting ? "Submitting..." : "Submit"}
354
+ </button>
355
+ </FormComponent>
356
+ );
357
+ },
358
+ play: async ({ canvasElement, step }) => {
359
+ const canvas = within(canvasElement);
360
+ const user = userEvent.setup();
361
+
362
+ await step("Submit the form", async () => {
363
+ const submitButton = canvas.getByRole("button");
364
+ await user.click(submitButton);
365
+ });
366
+
367
+ await step("Form shows loading state", async () => {
368
+ const form = canvas.getByRole("form");
369
+ expect(form).toHaveAttribute("aria-busy", "true");
370
+ expect(form).toHaveAttribute("data-status", "submitting");
371
+ });
372
+
373
+ await step("Submit button shows loading text", async () => {
374
+ expect(canvas.getByText(/submitting/i)).toBeInTheDocument();
375
+ });
376
+ },
377
+ };
378
+
379
+ /**
380
+ * Complete registration form example
381
+ */
382
+ export const RegistrationForm: Story = {
383
+ args: {
384
+ name: "registration",
385
+ "aria-labelledby": "registration-heading",
386
+ children: (
387
+ <>
388
+ <h2 id="registration-heading">Create Your Account</h2>
389
+ <FormComponent.Field
390
+ label="Email Address"
391
+ labelFor="reg-email"
392
+ required
393
+ hintText="We'll never share your email"
394
+ >
395
+ <FormComponent.Input
396
+ id="reg-email"
397
+ name="email"
398
+ type="email"
399
+ autoComplete="email"
400
+ required
401
+ />
402
+ </FormComponent.Field>
403
+ <FormComponent.Field
404
+ label="Password"
405
+ labelFor="reg-password"
406
+ required
407
+ hintText="At least 8 characters"
408
+ >
409
+ <FormComponent.Input
410
+ id="reg-password"
411
+ name="password"
412
+ type="password"
413
+ autoComplete="new-password"
414
+ minLength={8}
415
+ required
416
+ />
417
+ </FormComponent.Field>
418
+ <FormComponent.Field label="Country" labelFor="reg-country" required>
419
+ <FormComponent.Select id="reg-country" name="country" required>
420
+ <FormComponent.Select.Option value="">
421
+ Choose your country
422
+ </FormComponent.Select.Option>
423
+ <FormComponent.Select.Option value="us">
424
+ United States
425
+ </FormComponent.Select.Option>
426
+ <FormComponent.Select.Option value="ca">
427
+ Canada
428
+ </FormComponent.Select.Option>
429
+ <FormComponent.Select.Option value="uk">
430
+ United Kingdom
431
+ </FormComponent.Select.Option>
432
+ </FormComponent.Select>
433
+ </FormComponent.Field>
434
+ <FormComponent.Field label="Bio" labelFor="reg-bio" optional>
435
+ <FormComponent.Textarea
436
+ id="reg-bio"
437
+ name="bio"
438
+ placeholder="Tell us about yourself"
439
+ maxLength={500}
440
+ rows={4}
441
+ />
442
+ </FormComponent.Field>
443
+ <button type="submit" style={{ marginTop: "1rem" }}>
444
+ Create Account
445
+ </button>
446
+ </>
447
+ ),
448
+ },
449
+ play: async ({ canvasElement, step }) => {
450
+ const canvas = within(canvasElement);
451
+
452
+ await step("All form fields render", async () => {
453
+ expect(canvas.getByLabelText(/email address/i)).toBeInTheDocument();
454
+ expect(canvas.getByLabelText(/password/i)).toBeInTheDocument();
455
+ expect(canvas.getByLabelText(/country/i)).toBeInTheDocument();
456
+ expect(canvas.getByLabelText(/bio/i)).toBeInTheDocument();
457
+ });
458
+
459
+ await step("Form is properly labeled", async () => {
460
+ const form = canvas.getByRole("form");
461
+ expect(form).toHaveAttribute("aria-labelledby", "registration-heading");
462
+ });
463
+ },
464
+ };
465
+
466
+ /**
467
+ * Form with onEnter accessibility handlers
468
+ * Demonstrates the onEnter prop for keyboard-driven workflows
469
+ */
470
+ export const WithOnEnterHandler: Story = {
471
+ render: function OnEnterHandlerExample() {
472
+ const [searchQuery, setSearchQuery] = useState("");
473
+ const [comments, setComments] = useState("");
474
+ const [category, setCategory] = useState("");
475
+ const [messages, setMessages] = useState<string[]>([]);
476
+
477
+ const handleSearch = () => {
478
+ setMessages((prev) => [...prev, `🔍 Searching for: "${searchQuery}"`]);
479
+ };
480
+
481
+ const handleCommentSubmit = () => {
482
+ if (comments.trim()) {
483
+ setMessages((prev) => [
484
+ ...prev,
485
+ `💬 Comment submitted: "${comments.trim()}"`,
486
+ ]);
487
+ setComments("");
488
+ }
489
+ };
490
+
491
+ const handleCategorySelect = () => {
492
+ setMessages((prev) => [...prev, `📁 Category selected: "${category}"`]);
493
+ };
494
+
495
+ return (
496
+ <div>
497
+ <FormComponent aria-label="Keyboard accessibility demo">
498
+ <FormComponent.Field
499
+ label="Search"
500
+ labelFor="search-input"
501
+ hintText="Press Enter to search"
502
+ >
503
+ <FormComponent.Input
504
+ id="search-input"
505
+ name="search"
506
+ type="search"
507
+ placeholder="Type and press Enter..."
508
+ value={searchQuery}
509
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
510
+ setSearchQuery(e.target.value)
511
+ }
512
+ onEnter={handleSearch}
513
+ />
514
+ </FormComponent.Field>
515
+
516
+ <FormComponent.Field
517
+ label="Comments"
518
+ labelFor="comments-textarea"
519
+ hintText="Press Enter to submit (Shift+Enter for new line)"
520
+ >
521
+ <FormComponent.Textarea
522
+ id="comments-textarea"
523
+ name="comments"
524
+ placeholder="Type your comment..."
525
+ value={comments}
526
+ onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
527
+ setComments(e.target.value)
528
+ }
529
+ onEnter={handleCommentSubmit}
530
+ rows={4}
531
+ />
532
+ </FormComponent.Field>
533
+
534
+ <FormComponent.Field
535
+ label="Category"
536
+ labelFor="category-select"
537
+ hintText="Press Enter after selecting"
538
+ >
539
+ <FormComponent.Select
540
+ id="category-select"
541
+ name="category"
542
+ value={category}
543
+ onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
544
+ setCategory(e.target.value)
545
+ }
546
+ onEnter={handleCategorySelect}
547
+ >
548
+ <FormComponent.Select.Option value="">
549
+ Select category
550
+ </FormComponent.Select.Option>
551
+ <FormComponent.Select.Option value="bug">
552
+ Bug Report
553
+ </FormComponent.Select.Option>
554
+ <FormComponent.Select.Option value="feature">
555
+ Feature Request
556
+ </FormComponent.Select.Option>
557
+ <FormComponent.Select.Option value="question">
558
+ Question
559
+ </FormComponent.Select.Option>
560
+ </FormComponent.Select>
561
+ </FormComponent.Field>
562
+ </FormComponent>
563
+
564
+ {messages.length > 0 && (
565
+ <div
566
+ style={{
567
+ marginTop: "1rem",
568
+ padding: "1rem",
569
+ backgroundColor: "#f0f0f0",
570
+ borderRadius: "0.25rem",
571
+ }}
572
+ >
573
+ <h3 style={{ marginTop: 0 }}>Action Log:</h3>
574
+ <ul style={{ margin: 0, paddingLeft: "1.5rem" }}>
575
+ {messages.map((msg, idx) => (
576
+ <li key={idx}>{msg}</li>
577
+ ))}
578
+ </ul>
579
+ </div>
580
+ )}
581
+ </div>
582
+ );
583
+ },
584
+ play: async ({ canvasElement, step }) => {
585
+ const canvas = within(canvasElement);
586
+ const user = userEvent.setup();
587
+
588
+ await step("Input onEnter: Type and press Enter", async () => {
589
+ const searchInput = canvas.getByLabelText(/search/i);
590
+ await user.type(searchInput, "accessibility test");
591
+ await user.type(searchInput, "{Enter}");
592
+
593
+ // Verify action was logged
594
+ expect(
595
+ canvas.getByText(/Searching for: "accessibility test"/i)
596
+ ).toBeInTheDocument();
597
+ });
598
+
599
+ await step("Textarea onEnter: Enter without Shift", async () => {
600
+ const textarea = canvas.getByLabelText(/comments/i);
601
+ await user.type(textarea, "This is a test comment");
602
+ await user.type(textarea, "{Enter}");
603
+
604
+ // Verify comment was submitted
605
+ expect(
606
+ canvas.getByText(/Comment submitted: "This is a test comment"/i)
607
+ ).toBeInTheDocument();
608
+ });
609
+
610
+ await step(
611
+ "Textarea Shift+Enter: Adds newline without triggering onEnter",
612
+ async () => {
613
+ const textarea = canvas.getByLabelText(/comments/i);
614
+ await user.type(textarea, "Line 1{Shift>}{Enter}{/Shift}Line 2");
615
+
616
+ // Verify textarea contains newline
617
+ expect(textarea).toHaveValue("Line 1\nLine 2");
618
+ }
619
+ );
620
+
621
+ await step("Select onEnter: Select and press Enter", async () => {
622
+ const select = canvas.getByLabelText(/category/i);
623
+ await user.selectOptions(select, "bug");
624
+ await user.type(select, "{Enter}");
625
+
626
+ // Verify category selection was logged
627
+ expect(canvas.getByText(/Category selected: "bug"/i)).toBeInTheDocument();
628
+ });
629
+ },
630
+ };