@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
@@ -68,18 +68,86 @@ export const DefaultRequired: Story = {
68
68
  },
69
69
  } as Story;
70
70
 
71
+ /**
72
+ * Disabled input using WCAG-compliant aria-disabled pattern.
73
+ *
74
+ * Key accessibility features implemented by the optimized useDisabledState hook:
75
+ * - Uses aria-disabled instead of native disabled attribute
76
+ * - Remains keyboard focusable (in tab order)
77
+ * - Prevents all interactions (typing, onChange events)
78
+ * - Screen readers can discover and announce disabled state
79
+ * - Automatic className merging (.is-disabled + custom classes)
80
+ */
71
81
  export const InputDisabled: Story = {
72
82
  parameters: {
73
83
  docs: {
74
84
  description: {
75
- story:
76
- 'Displays a disabled input `aria-disabled="true"` on any input type',
85
+ story: `
86
+ Displays a disabled input with \`aria-disabled="true"\`.
87
+
88
+ **Why aria-disabled instead of disabled?**
89
+ - Keeps input in tab order for screen reader users
90
+ - Allows focus for screen reader announcement
91
+ - Better contrast control for WCAG AA compliance
92
+ - Can show tooltips/help text even when disabled
93
+
94
+ Try tabbing to the input - it receives focus!
95
+ Try typing - interactions are prevented by the hook.
96
+ `,
77
97
  },
78
98
  },
79
99
  },
80
100
  args: {
81
101
  type: "text",
82
- isDisabled: true,
102
+ disabled: true,
103
+ placeholder: "This input is disabled",
104
+ },
105
+ play: async ({ canvasElement, step }) => {
106
+ const canvas = within(canvasElement);
107
+ const input = canvas.getByRole("textbox");
108
+
109
+ await step("Disabled input has aria-disabled attribute", async () => {
110
+ expect(input).toHaveAttribute("aria-disabled", "true");
111
+ });
112
+
113
+ await step("Disabled input remains focusable", async () => {
114
+ await userEvent.tab();
115
+ expect(input).toHaveFocus();
116
+ });
117
+
118
+ await step("Disabled input has .is-disabled class", async () => {
119
+ expect(input).toHaveClass("is-disabled");
120
+ });
121
+
122
+ await step("Disabled input prevents typing interactions", async () => {
123
+ const initialValue = input.getAttribute("value") || "";
124
+ await userEvent.type(input, "test");
125
+ // Value should remain unchanged due to disabled state
126
+ expect(input).toHaveValue(initialValue);
127
+ });
128
+ },
129
+ } as Story;
130
+
131
+ /**
132
+ * Disabled input with custom classes.
133
+ *
134
+ * Demonstrates the hook's automatic className merging feature.
135
+ * The .is-disabled class is automatically combined with custom classes.
136
+ */
137
+ export const DisabledWithCustomClass: Story = {
138
+ args: {
139
+ type: "text",
140
+ disabled: true,
141
+ classes: "custom-input highlight-disabled",
142
+ placeholder: "Disabled with custom classes",
143
+ },
144
+ parameters: {
145
+ docs: {
146
+ description: {
147
+ story:
148
+ "Shows how the optimized hook automatically merges `.is-disabled` with custom classes (`custom-input highlight-disabled`).",
149
+ },
150
+ },
83
151
  },
84
152
  } as Story;
85
153
 
@@ -1,78 +1,170 @@
1
1
  import React from "react";
2
2
  import FP from "../fp";
3
+ import type { InputProps } from "./form.types";
4
+ import { useDisabledState } from "../../hooks/use-disabled-state";
5
+ import { resolveDisabledState } from "../../utils/accessibility";
3
6
 
4
- export type InputProps = {
5
- /**
6
- * The type of the input.
7
- */
8
- type?: "text" | "password" | "email" | "number" | "tel" | "url" | "search";
7
+ export type { InputProps } from "./form.types";
9
8
 
10
- /**
11
- * Set the element as disabled
12
- */
13
- isDisabled?: boolean;
14
- } & React.ComponentProps<typeof FP>;
9
+ /**
10
+ * Input component - Accessible text input with validation support
11
+ *
12
+ * A flexible input component that supports various input types, validation states,
13
+ * and proper ARIA attributes for accessibility. Integrates seamlessly with the
14
+ * Field component for complete form control composition.
15
+ *
16
+ * @component
17
+ * @example
18
+ * // Basic text input
19
+ * <Input
20
+ * id="username"
21
+ * name="username"
22
+ * placeholder="Enter username"
23
+ * required
24
+ * />
25
+ *
26
+ * @example
27
+ * // Input with error state
28
+ * <Input
29
+ * id="email"
30
+ * type="email"
31
+ * validationState="invalid"
32
+ * errorMessage="Please enter a valid email"
33
+ * aria-describedby="email-error"
34
+ * />
35
+ *
36
+ * @example
37
+ * // Controlled input with validation
38
+ * <Input
39
+ * id="password"
40
+ * type="password"
41
+ * value={password}
42
+ * onChange={(e) => setPassword(e.target.value)}
43
+ * minLength={8}
44
+ * required
45
+ * />
46
+ *
47
+ * @param {InputProps} props - Component props
48
+ * @returns {JSX.Element} Input element with proper accessibility attributes
49
+ *
50
+ * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/error-identification.html|WCAG 3.3.1 Error Identification}
51
+ * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html|WCAG 4.1.2 Name, Role, Value}
52
+ */
53
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
54
+ (
55
+ {
56
+ type = "text",
57
+ name,
58
+ value,
59
+ defaultValue,
60
+ placeholder,
61
+ id,
62
+ styles,
63
+ classes,
64
+ isDisabled, // Legacy support
65
+ disabled,
66
+ readOnly,
67
+ required = false,
68
+ validationState = "none",
69
+ errorMessage,
70
+ hintText,
71
+ onChange,
72
+ onBlur,
73
+ onFocus,
74
+ onKeyDown,
75
+ onEnter,
76
+ maxLength,
77
+ minLength,
78
+ pattern,
79
+ autoComplete,
80
+ autoFocus = false,
81
+ inputMode,
82
+ ...props
83
+ },
84
+ ref
85
+ ) => {
86
+ // Support both `disabled` and `isDisabled` props (legacy compatibility)
87
+ const isInputDisabled = resolveDisabledState(disabled, isDisabled);
15
88
 
16
- export const Input = ({
17
- type = "text",
18
- name,
19
- value,
20
- placeholder,
21
- id,
22
- styles,
23
- classes,
24
- isDisabled,
25
- disabled,
26
- readonly,
27
- required,
28
- ref,
29
- onChange,
30
- onBlur,
31
- onPointerDown,
32
- ...props
33
- }: InputProps): JSX.Element => {
34
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35
- if (onChange && !disabled) {
36
- onChange?.(e);
37
- }
38
- };
89
+ // Use the disabled state hook with enhanced API for automatic className merging
90
+ const { disabledProps, handlers } = useDisabledState<HTMLInputElement>(
91
+ isInputDisabled,
92
+ {
93
+ handlers: {
94
+ onChange,
95
+ onBlur,
96
+ onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {
97
+ // Handle Enter key press for accessibility
98
+ if (e.key === "Enter" && onEnter) {
99
+ onEnter(e);
100
+ }
101
+ // Always call consumer's onKeyDown if provided
102
+ if (onKeyDown) {
103
+ onKeyDown(e);
104
+ }
105
+ },
106
+ // Note: onFocus is NOT wrapped to allow focus on disabled inputs
107
+ // This is handled automatically by useDisabledState
108
+ },
109
+ // Automatic className merging - hook combines disabled class with user classes
110
+ className: classes,
111
+ }
112
+ );
39
113
 
40
- const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
41
- if (onBlur && !disabled) {
42
- onBlur?.(e);
43
- }
44
- };
114
+ // Determine aria-invalid based on validation state
115
+ const isInvalid = validationState === "invalid";
45
116
 
46
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
47
- if (onPointerDown && !disabled) {
48
- e.preventDefault();
49
- onPointerDown?.(e);
117
+ // Generate describedby IDs for error and hint text
118
+ const describedByIds: string[] = [];
119
+ if (errorMessage && id) {
120
+ describedByIds.push(`${id}-error`);
121
+ }
122
+ if (hintText && id) {
123
+ describedByIds.push(`${id}-hint`);
50
124
  }
51
- };
125
+ const ariaDescribedBy =
126
+ describedByIds.length > 0 ? describedByIds.join(" ") : undefined;
127
+
128
+ // Generate accessible placeholder if not provided
129
+ const accessiblePlaceholder =
130
+ placeholder || (required ? `* ${type} input` : `${type} input`);
131
+
132
+ return (
133
+ <FP
134
+ as="input"
135
+ ref={ref}
136
+ id={id}
137
+ type={type}
138
+ name={name}
139
+ value={value}
140
+ defaultValue={defaultValue}
141
+ placeholder={accessiblePlaceholder}
142
+ className={disabledProps.className}
143
+ styles={styles}
144
+ readOnly={readOnly}
145
+ required={required}
146
+ maxLength={maxLength}
147
+ minLength={minLength}
148
+ pattern={pattern}
149
+ autoComplete={autoComplete}
150
+ autoFocus={autoFocus}
151
+ inputMode={inputMode}
152
+ // Event handlers (wrapped by useDisabledState)
153
+ {...handlers}
154
+ onFocus={onFocus}
155
+ // ARIA attributes
156
+ aria-disabled={disabledProps["aria-disabled"]}
157
+ aria-readonly={readOnly}
158
+ aria-required={required}
159
+ aria-invalid={isInvalid}
160
+ aria-describedby={ariaDescribedBy}
161
+ // Data attributes for styling
162
+ data-validation={validationState}
163
+ {...props}
164
+ />
165
+ );
166
+ }
167
+ );
52
168
 
53
- return (
54
- <FP
55
- as="input"
56
- id={id}
57
- type={type}
58
- placeholder={placeholder || `${required ? "*" : ""} ${type} input `}
59
- className={classes}
60
- styles={styles}
61
- onChange={handleChange}
62
- onBlur={handleBlur}
63
- onKeyDown={handleKeyDown}
64
- value={value}
65
- name={name}
66
- ref={ref}
67
- aria-disabled={isDisabled}
68
- tabIndex={isDisabled ? -1 : undefined}
69
- aria-readonly={readonly}
70
- aria-required={required}
71
- required={required}
72
- readOnly={readonly}
73
- {...props}
74
- />
75
- );
76
- };
77
169
  Input.displayName = "Input";
78
170
  export default Input;
@@ -1,7 +1,9 @@
1
1
  import UI from '../ui'
2
2
  import React from 'react'
3
+ import { useDisabledState } from '../../hooks/use-disabled-state'
3
4
 
4
- export type SelectProps = React.ComponentProps<typeof UI>
5
+ export type { SelectProps } from './form.types'
6
+ import type { SelectProps } from './form.types'
5
7
 
6
8
  export type SelectOptionsProps = {
7
9
  /**
@@ -13,7 +15,7 @@ export type SelectOptionsProps = {
13
15
  * Value for the select option. Can be a number or string.
14
16
  */
15
17
  selectValue: string
16
- } & SelectProps
18
+ }
17
19
 
18
20
  /**
19
21
  * Option component for select.
@@ -23,80 +25,134 @@ export type SelectOptionsProps = {
23
25
  */
24
26
  export const Option = ({ selectValue, selectLabel }: SelectOptionsProps) => {
25
27
  return (
26
- <option role="option" value={selectValue}>
28
+ <option value={selectValue}>
27
29
  {selectLabel || selectValue}
28
30
  </option>
29
31
  )
30
32
  }
31
33
 
32
34
  /**
33
- * Select component props.
34
- * @param {string} [id] - Unique id for the select.
35
- * @param {string} [name] - Name for the select input.
36
- * @param {React.CSSProperties} [styles] - Inline styles.
37
- * @param {string} [classes] - CSS classes.
38
- * @param {boolean} [disabled] - Whether select is disabled.
39
- * @param {React.ReactNode} [children] - Child elements.
40
- * @param {boolean} [required] - Whether select is required.
41
- * @param {string | number | string[] | undefined} [selected] - Selected option value(s).
42
- * @param {React.FocusEventHandler<HTMLSelectElement>} [onBlur] - Blur event handler.
43
- * @param {React.ChangeEventHandler<HTMLSelectElement>} [onChange] - Change event handler.
44
- * @param {(e: React.ChangeEvent<HTMLSelectElement>) => void} [onSelectionChange] - Selection change handler.
45
- * @param {(e: React.PointerEvent<HTMLSelectElement>) => void} [onPointerDown] - Pointer down handler.
46
- * @param {React.Ref<HTMLSelectElement>} [ref] - Ref for the select element.
35
+ * Select component - Accessible dropdown selection input with validation support
36
+ *
37
+ * A flexible select component that supports validation states, proper ARIA attributes
38
+ * for accessibility, and an onEnter handler for keyboard interactions. Enables keyboard-only
39
+ * users to trigger actions after making a selection.
40
+ *
41
+ * @component
42
+ * @example
43
+ * // Basic select
44
+ * <Select id="country" name="country" required>
45
+ * <option value="us">United States</option>
46
+ * <option value="uk">United Kingdom</option>
47
+ * </Select>
48
+ *
49
+ * @example
50
+ * // Select with Enter key handler for quick submission
51
+ * <Select
52
+ * id="status"
53
+ * name="status"
54
+ * onEnter={(e) => handleSubmit()}
55
+ * onSelectionChange={(e) => setStatus(e.target.value)}
56
+ * >
57
+ * <option value="active">Active</option>
58
+ * <option value="inactive">Inactive</option>
59
+ * </Select>
60
+ *
61
+ * @param {SelectProps} props - Component props
62
+ * @returns {JSX.Element} Select element with proper accessibility attributes
63
+ *
64
+ * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html|WCAG 2.1.1 Keyboard}
65
+ * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html|WCAG 4.1.2 Name, Role, Value}
47
66
  */
48
- export const Select = ({
49
- id,
50
- name,
51
- styles,
52
- classes,
53
- disabled,
54
- children,
55
- required,
56
- selected,
57
- onBlur,
58
- onSelectionChange,
59
- onPointerDown,
60
- ref,
61
- ...props
62
- }: SelectProps) => {
63
- const handleOnChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
64
- if (onSelectionChange && !disabled) onSelectionChange?.(e)
65
- }
67
+ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
68
+ (
69
+ {
70
+ id,
71
+ name,
72
+ styles,
73
+ classes,
74
+ disabled,
75
+ children,
76
+ required,
77
+ selected,
78
+ validationState = 'none',
79
+ errorMessage,
80
+ hintText,
81
+ onBlur,
82
+ onSelectionChange,
83
+ onPointerDown,
84
+ onKeyDown,
85
+ onEnter,
86
+ ...props
87
+ },
88
+ ref
89
+ ) => {
90
+ // Use the disabled state hook with enhanced API for automatic className merging
91
+ const { disabledProps, handlers } = useDisabledState<HTMLSelectElement>(
92
+ disabled,
93
+ {
94
+ handlers: {
95
+ onChange: onSelectionChange,
96
+ onPointerDown,
97
+ onBlur,
98
+ onKeyDown: (e: React.KeyboardEvent<HTMLSelectElement>) => {
99
+ // Handle Enter key press for accessibility
100
+ // Enables keyboard-only users to trigger actions after selection
101
+ if (e.key === 'Enter' && onEnter) {
102
+ onEnter(e)
103
+ }
104
+ // Always call consumer's onKeyDown if provided
105
+ if (onKeyDown) {
106
+ onKeyDown(e)
107
+ }
108
+ },
109
+ },
110
+ // Automatic className merging - hook combines disabled class with user classes
111
+ className: classes,
112
+ }
113
+ )
66
114
 
67
- const handlePointerDown = (e: React.PointerEvent<HTMLSelectElement>) => {
68
- if (onPointerDown && !disabled) onPointerDown?.(e)
69
- }
115
+ // Determine aria-invalid based on validation state
116
+ const isInvalid = validationState === 'invalid'
70
117
 
71
- const handleOnBlur = (e: React.FocusEvent<HTMLSelectElement>) => {
72
- if (onBlur && !disabled) onBlur?.(e)
118
+ // Generate describedby IDs for error and hint text
119
+ const describedByIds: string[] = []
120
+ if (errorMessage && id) {
121
+ describedByIds.push(`${id}-error`)
122
+ }
123
+ if (hintText && id) {
124
+ describedByIds.push(`${id}-hint`)
125
+ }
126
+ const ariaDescribedBy =
127
+ describedByIds.length > 0 ? describedByIds.join(' ') : undefined
128
+
129
+ return (
130
+ <UI
131
+ as="select"
132
+ id={id}
133
+ ref={ref}
134
+ name={name}
135
+ className={disabledProps.className}
136
+ defaultValue={selected}
137
+ {...handlers}
138
+ required={required}
139
+ aria-required={required}
140
+ aria-disabled={disabledProps['aria-disabled']}
141
+ aria-invalid={isInvalid}
142
+ aria-describedby={ariaDescribedBy}
143
+ style={styles}
144
+ {...props}
145
+ >
146
+ {children || <option value="" />}
147
+ </UI>
148
+ )
73
149
  }
150
+ )
74
151
 
75
- return (
76
- <UI
77
- as="select"
78
- id={id}
79
- ref={ref}
80
- name={name}
81
- className={classes}
82
- selected={selected}
83
- onChange={handleOnChange}
84
- onPointerDown={handlePointerDown}
85
- onBlur={handleOnBlur}
86
- required={required}
87
- aria-required={required} // Accessibility
88
- disabled={disabled}
89
- aria-disabled={disabled ? true : false}
90
- style={styles}
91
- {...props} // Accessibility
92
- >
93
- <option value="" />
94
- </UI>
95
- )
96
- }
152
+ Select.displayName = 'Select'
97
153
 
98
- export default Select
99
- Select.displayName = 'Select' // Remove this line
100
- Select.Option = Option // Remove this line
154
+ // Type assertion to allow adding static property to ForwardRefExoticComponent
155
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
+ ;(Select as any).Option = Option
101
157
 
102
- // export const MemoizedSelect = React.memo(Select)
158
+ export default Select