@fpkit/acss 0.5.13 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/libs/{chunk-PQ2K3BM6.cjs → chunk-2NRIP6RB.cjs} +3 -3
- package/libs/chunk-33PNJ4LO.cjs +15 -0
- package/libs/chunk-33PNJ4LO.cjs.map +1 -0
- package/libs/chunk-4BZKFPEC.cjs +17 -0
- package/libs/chunk-4BZKFPEC.cjs.map +1 -0
- package/libs/{chunk-772NRB75.js → chunk-5QD3DWFI.js} +2 -2
- package/libs/chunk-6SAHIYCZ.js +7 -0
- package/libs/chunk-6SAHIYCZ.js.map +1 -0
- package/libs/{chunk-3MKLDCKQ.cjs → chunk-6WTC4JXH.cjs} +3 -3
- package/libs/chunk-75QHTLFO.js +7 -0
- package/libs/chunk-75QHTLFO.js.map +1 -0
- package/libs/{chunk-ZANSFMTD.js → chunk-7XPFW7CB.js} +3 -3
- package/libs/chunk-BFK62VX5.js +5 -0
- package/libs/chunk-BFK62VX5.js.map +1 -0
- package/libs/{chunk-ROZI23GS.cjs → chunk-DKTHCQ5P.cjs} +4 -4
- package/libs/chunk-E2AJURUW.cjs +13 -0
- package/libs/chunk-E2AJURUW.cjs.map +1 -0
- package/libs/{chunk-L75OQKEI.cjs → chunk-ENTCUJ3A.cjs} +3 -3
- package/libs/chunk-ENTCUJ3A.cjs.map +1 -0
- package/libs/chunk-F5EYMVQM.js +10 -0
- package/libs/chunk-F5EYMVQM.js.map +1 -0
- package/libs/chunk-FVROL3V5.js +9 -0
- package/libs/chunk-FVROL3V5.js.map +1 -0
- package/libs/chunk-GT77BX4L.cjs +17 -0
- package/libs/chunk-GT77BX4L.cjs.map +1 -0
- package/libs/chunk-GUJSMQ3V.cjs +16 -0
- package/libs/chunk-GUJSMQ3V.cjs.map +1 -0
- package/libs/chunk-HHLNOC5T.js +7 -0
- package/libs/chunk-HHLNOC5T.js.map +1 -0
- package/libs/chunk-HRRHPLER.js +8 -0
- package/libs/chunk-HRRHPLER.js.map +1 -0
- package/libs/chunk-IEB64SWY.js +8 -0
- package/libs/chunk-IEB64SWY.js.map +1 -0
- package/libs/{chunk-NGTJDDFO.js → chunk-IQ76HGVP.js} +2 -2
- package/libs/chunk-IRLFZ3OL.js +9 -0
- package/libs/chunk-IRLFZ3OL.js.map +1 -0
- package/libs/{chunk-JJ43O4Y5.js → chunk-KK47SYZI.js} +2 -2
- package/libs/chunk-O3JIHC5M.cjs +15 -0
- package/libs/chunk-O3JIHC5M.cjs.map +1 -0
- package/libs/chunk-O5XAJ7BY.cjs +18 -0
- package/libs/chunk-O5XAJ7BY.cjs.map +1 -0
- package/libs/chunk-OVWLQYMK.js +10 -0
- package/libs/chunk-OVWLQYMK.js.map +1 -0
- package/libs/chunk-PNWIRCG3.cjs +7 -0
- package/libs/chunk-PNWIRCG3.cjs.map +1 -0
- package/libs/{chunk-D4YLRWAO.cjs → chunk-QVW6W76L.cjs} +6 -6
- package/libs/chunk-T4T6GWYQ.cjs +17 -0
- package/libs/chunk-T4T6GWYQ.cjs.map +1 -0
- package/libs/chunk-TON2YGMD.cjs +9 -0
- package/libs/chunk-TON2YGMD.cjs.map +1 -0
- package/libs/chunk-UEPAWMDF.js +8 -0
- package/libs/chunk-UEPAWMDF.js.map +1 -0
- package/libs/{chunk-LT5KZ2QW.cjs → chunk-US2I5GI7.cjs} +3 -3
- package/libs/{chunk-B7F5FS6D.cjs → chunk-W2UIN7EV.cjs} +3 -3
- package/libs/{chunk-P2DC76ZZ.cjs → chunk-W5TKWBFC.cjs} +3 -3
- package/libs/chunk-WXBFBWYF.cjs +16 -0
- package/libs/chunk-WXBFBWYF.cjs.map +1 -0
- package/libs/{chunk-VUH3FXGJ.js → chunk-X3JCTEPD.js} +5 -5
- package/libs/chunk-X5LGFCWG.js +9 -0
- package/libs/chunk-X5LGFCWG.js.map +1 -0
- package/libs/{chunk-5M57K4SW.js → chunk-Y2PFDELK.js} +2 -2
- package/libs/{chunk-ETFLFC2S.js → chunk-ZFJ4U45S.js} +2 -2
- package/libs/{component-props-a8a2f97e.d.ts → component-props-67d978a2.d.ts} +4 -4
- package/libs/components/alert/alert.css +1 -1
- package/libs/components/alert/alert.css.map +1 -1
- package/libs/components/alert/alert.min.css +2 -2
- package/libs/components/breadcrumbs/breadcrumb.cjs +6 -6
- package/libs/components/breadcrumbs/breadcrumb.d.cts +11 -11
- package/libs/components/breadcrumbs/breadcrumb.d.ts +11 -11
- package/libs/components/breadcrumbs/breadcrumb.js +3 -3
- package/libs/components/button.cjs +6 -4
- package/libs/components/button.d.cts +97 -4
- package/libs/components/button.d.ts +97 -4
- package/libs/components/button.js +4 -2
- package/libs/components/card.cjs +7 -7
- package/libs/components/card.d.cts +14 -14
- package/libs/components/card.d.ts +14 -14
- package/libs/components/card.js +2 -2
- package/libs/components/dialog/dialog.cjs +9 -7
- package/libs/components/dialog/dialog.d.cts +3 -3
- package/libs/components/dialog/dialog.d.ts +3 -3
- package/libs/components/dialog/dialog.js +7 -5
- package/libs/components/form/fields.cjs +4 -4
- package/libs/components/form/fields.d.cts +16 -7
- package/libs/components/form/fields.d.ts +16 -7
- package/libs/components/form/fields.js +2 -2
- package/libs/components/form/inputs.cjs +6 -4
- package/libs/components/form/inputs.d.cts +50 -2
- package/libs/components/form/inputs.d.ts +50 -2
- package/libs/components/form/inputs.js +4 -2
- package/libs/components/form/textarea.cjs +5 -4
- package/libs/components/form/textarea.d.cts +32 -23
- package/libs/components/form/textarea.d.ts +32 -23
- package/libs/components/form/textarea.js +3 -2
- package/libs/components/heading/heading.cjs +3 -3
- package/libs/components/heading/heading.d.cts +2 -2
- package/libs/components/heading/heading.d.ts +2 -2
- package/libs/components/heading/heading.js +2 -2
- package/libs/components/icons/icon.cjs +4 -4
- package/libs/components/icons/icon.d.cts +38 -38
- package/libs/components/icons/icon.d.ts +38 -38
- package/libs/components/icons/icon.js +2 -2
- package/libs/components/link/link.cjs +4 -4
- package/libs/components/link/link.css +1 -1
- package/libs/components/link/link.css.map +1 -1
- package/libs/components/link/link.d.cts +3 -19
- package/libs/components/link/link.d.ts +3 -19
- package/libs/components/link/link.js +2 -2
- package/libs/components/link/link.min.css +2 -2
- package/libs/components/list/list.cjs +5 -5
- package/libs/components/list/list.css +1 -0
- package/libs/components/list/list.css.map +1 -0
- package/libs/components/list/list.d.cts +120 -33
- package/libs/components/list/list.d.ts +120 -33
- package/libs/components/list/list.js +2 -2
- package/libs/components/list/list.min.css +3 -0
- package/libs/components/modal.cjs +6 -4
- package/libs/components/modal.d.cts +8 -8
- package/libs/components/modal.d.ts +8 -8
- package/libs/components/modal.js +5 -3
- package/libs/components/nav/nav.cjs +7 -7
- package/libs/components/nav/nav.css +1 -1
- package/libs/components/nav/nav.css.map +1 -1
- package/libs/components/nav/nav.d.cts +550 -34
- package/libs/components/nav/nav.d.ts +550 -34
- package/libs/components/nav/nav.js +3 -3
- package/libs/components/nav/nav.min.css +2 -2
- package/libs/components/popover/popover.d.cts +5 -5
- package/libs/components/popover/popover.d.ts +5 -5
- package/libs/components/tables/table.cjs +5 -5
- package/libs/components/tables/table.d.cts +8 -8
- package/libs/components/tables/table.d.ts +8 -8
- package/libs/components/tables/table.js +2 -2
- package/libs/components/tag/tag.css +1 -1
- package/libs/components/tag/tag.css.map +1 -1
- package/libs/components/tag/tag.min.css +2 -2
- package/libs/components/text/text.cjs +5 -5
- package/libs/components/text/text.d.cts +5 -5
- package/libs/components/text/text.d.ts +5 -5
- package/libs/components/text/text.js +2 -2
- package/libs/form.types-d25ebfac.d.ts +233 -0
- package/libs/{heading-3648c538.d.ts → heading-7446cb46.d.ts} +8 -8
- package/libs/hooks.cjs +9 -4
- package/libs/hooks.d.cts +137 -3
- package/libs/hooks.d.ts +137 -3
- package/libs/hooks.js +4 -3
- package/libs/icons.cjs +3 -3
- package/libs/icons.d.cts +2 -2
- package/libs/icons.d.ts +2 -2
- package/libs/icons.js +2 -2
- package/libs/index.cjs +53 -51
- package/libs/index.cjs.map +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +338 -49
- package/libs/index.d.ts +338 -49
- package/libs/index.js +24 -22
- package/libs/index.js.map +1 -1
- package/libs/link-5192f411.d.ts +323 -0
- package/libs/list.types-d26de310.d.ts +245 -0
- package/libs/{ui-645f95b5.d.ts → ui-d01b50d4.d.ts} +16 -12
- package/package.json +4 -6
- package/src/components/alert/alert.scss +1 -4
- package/src/components/breadcrumbs/breadcrumb.tsx +4 -1
- package/src/components/buttons/README.mdx +102 -1
- package/src/components/buttons/button.stories.tsx +106 -0
- package/src/components/buttons/button.tsx +82 -52
- package/src/components/dialog/dialog-a11y-review.md +653 -0
- package/src/components/form/README.mdx +725 -43
- package/src/components/form/WCAG-REVIEW.md +654 -0
- package/src/components/form/fields.tsx +10 -1
- package/src/components/form/form.stories.tsx +604 -23
- package/src/components/form/form.tsx +204 -63
- package/src/components/form/form.types.ts +378 -0
- package/src/components/form/input.stories.tsx +71 -3
- package/src/components/form/inputs.tsx +159 -67
- package/src/components/form/select.tsx +122 -66
- package/src/components/form/textarea.tsx +120 -73
- package/src/components/fp.tsx +86 -11
- package/src/components/link/README.mdx +923 -0
- package/src/components/link/link.scss +79 -26
- package/src/components/link/link.stories.tsx +383 -30
- package/src/components/link/link.test.tsx +677 -0
- package/src/components/link/link.tsx +163 -57
- package/src/components/link/link.types.ts +261 -0
- package/src/components/list/README.mdx +764 -0
- package/src/components/list/list.scss +285 -0
- package/src/components/list/list.stories.tsx +514 -27
- package/src/components/list/list.test.tsx +554 -0
- package/src/components/list/list.tsx +153 -51
- package/src/components/list/list.types.ts +255 -0
- package/src/components/nav/ACCESSIBILITY.md +649 -0
- package/src/components/nav/README.mdx +782 -0
- package/src/components/nav/nav.scss +32 -1
- package/src/components/nav/nav.stories.tsx +44 -6
- package/src/components/nav/nav.tsx +302 -51
- package/src/components/nav/nav.types.ts +308 -0
- package/src/components/tag/README.mdx +426 -0
- package/src/components/tag/tag.scss +101 -27
- package/src/components/tag/tag.stories.tsx +384 -10
- package/src/components/tag/tag.test.tsx +210 -0
- package/src/components/tag/tag.tsx +106 -9
- package/src/components/tag/tag.types.ts +107 -0
- package/src/components/ui.tsx +8 -3
- package/src/hooks/use-disabled-state.test.tsx +536 -0
- package/src/hooks/use-disabled-state.ts +246 -0
- package/src/hooks/useDisabledState.md +393 -0
- package/src/hooks.ts +6 -0
- package/src/index.scss +2 -0
- package/src/index.ts +2 -1
- package/src/sass/_globals.scss +2 -7
- package/src/styles/alert/alert.css +1 -3
- package/src/styles/alert/alert.css.map +1 -1
- package/src/styles/index.css +450 -76
- package/src/styles/index.css.map +1 -1
- package/src/styles/link/link.css +45 -28
- package/src/styles/link/link.css.map +1 -1
- package/src/styles/list/list.css +214 -0
- package/src/styles/list/list.css.map +1 -0
- package/src/styles/nav/nav.css +21 -1
- package/src/styles/nav/nav.css.map +1 -1
- package/src/styles/tag/tag.css +113 -35
- package/src/styles/tag/tag.css.map +1 -1
- package/src/styles/utilities/_disabled.scss +58 -0
- package/src/types/shared.ts +43 -6
- package/src/utils/accessibility.ts +109 -0
- package/libs/chunk-2LTJ7HHX.cjs +0 -18
- package/libs/chunk-2LTJ7HHX.cjs.map +0 -1
- package/libs/chunk-2Y7W75TT.js +0 -9
- package/libs/chunk-2Y7W75TT.js.map +0 -1
- package/libs/chunk-5S4ORA4C.cjs +0 -15
- package/libs/chunk-5S4ORA4C.cjs.map +0 -1
- package/libs/chunk-AHDJGCG5.cjs +0 -15
- package/libs/chunk-AHDJGCG5.cjs.map +0 -1
- package/libs/chunk-BHRQBJRY.js +0 -8
- package/libs/chunk-BHRQBJRY.js.map +0 -1
- package/libs/chunk-GZ4QFPRY.js +0 -9
- package/libs/chunk-GZ4QFPRY.js.map +0 -1
- package/libs/chunk-IYUN2EW3.cjs +0 -15
- package/libs/chunk-IYUN2EW3.cjs.map +0 -1
- package/libs/chunk-J32EZPYD.cjs +0 -15
- package/libs/chunk-J32EZPYD.cjs.map +0 -1
- package/libs/chunk-KUKIVRC2.js +0 -7
- package/libs/chunk-KUKIVRC2.js.map +0 -1
- package/libs/chunk-L75OQKEI.cjs.map +0 -1
- package/libs/chunk-M5RRNTVX.cjs +0 -15
- package/libs/chunk-M5RRNTVX.cjs.map +0 -1
- package/libs/chunk-OK5QEIMD.cjs +0 -17
- package/libs/chunk-OK5QEIMD.cjs.map +0 -1
- package/libs/chunk-P7TTEYCD.js +0 -7
- package/libs/chunk-P7TTEYCD.js.map +0 -1
- package/libs/chunk-QLZWHAMK.js +0 -8
- package/libs/chunk-QLZWHAMK.js.map +0 -1
- package/libs/chunk-RIVUMPOG.js +0 -8
- package/libs/chunk-RIVUMPOG.js.map +0 -1
- package/libs/chunk-S7BABR7Z.cjs +0 -13
- package/libs/chunk-S7BABR7Z.cjs.map +0 -1
- package/libs/chunk-SMYRLO3E.js +0 -8
- package/libs/chunk-SMYRLO3E.js.map +0 -1
- package/libs/chunk-TYRCEX2L.js +0 -8
- package/libs/chunk-TYRCEX2L.js.map +0 -1
- package/libs/chunk-XBA562WW.js +0 -8
- package/libs/chunk-XBA562WW.js.map +0 -1
- package/libs/chunk-XTQKWY7W.cjs +0 -32
- package/libs/chunk-XTQKWY7W.cjs.map +0 -1
- package/libs/inputs-f3a216db.d.ts +0 -45
- /package/libs/{chunk-PQ2K3BM6.cjs.map → chunk-2NRIP6RB.cjs.map} +0 -0
- /package/libs/{chunk-772NRB75.js.map → chunk-5QD3DWFI.js.map} +0 -0
- /package/libs/{chunk-3MKLDCKQ.cjs.map → chunk-6WTC4JXH.cjs.map} +0 -0
- /package/libs/{chunk-ZANSFMTD.js.map → chunk-7XPFW7CB.js.map} +0 -0
- /package/libs/{chunk-ROZI23GS.cjs.map → chunk-DKTHCQ5P.cjs.map} +0 -0
- /package/libs/{chunk-NGTJDDFO.js.map → chunk-IQ76HGVP.js.map} +0 -0
- /package/libs/{chunk-JJ43O4Y5.js.map → chunk-KK47SYZI.js.map} +0 -0
- /package/libs/{chunk-D4YLRWAO.cjs.map → chunk-QVW6W76L.cjs.map} +0 -0
- /package/libs/{chunk-LT5KZ2QW.cjs.map → chunk-US2I5GI7.cjs.map} +0 -0
- /package/libs/{chunk-B7F5FS6D.cjs.map → chunk-W2UIN7EV.cjs.map} +0 -0
- /package/libs/{chunk-P2DC76ZZ.cjs.map → chunk-W5TKWBFC.cjs.map} +0 -0
- /package/libs/{chunk-VUH3FXGJ.js.map → chunk-X3JCTEPD.js.map} +0 -0
- /package/libs/{chunk-5M57K4SW.js.map → chunk-Y2PFDELK.js.map} +0 -0
- /package/libs/{chunk-ETFLFC2S.js.map → chunk-ZFJ4U45S.js.map} +0 -0
|
@@ -1,70 +1,752 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="FP.REACT Forms/Form/Readme" />
|
|
4
|
+
|
|
1
5
|
# Form Components
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
A comprehensive set of accessible React form components built with TypeScript, designed for building robust, WCAG 2.1 AA compliant forms with proper validation, error handling, and status management.
|
|
8
|
+
|
|
9
|
+
> 💡 **Interactive Examples**: See the [Form stories](./?path=/docs/fp-react-forms-form--docs) for live, interactive examples with automated accessibility testing.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- ✅ **WCAG 2.1 AA Compliant** - Full accessibility support with proper ARIA attributes
|
|
14
|
+
- ✅ **Compound Component Pattern** - Intuitive API with Form.Field, Form.Input, etc.
|
|
15
|
+
- ✅ **TypeScript First** - Full type safety with comprehensive interfaces
|
|
16
|
+
- ✅ **Status Management** - Built-in loading states and submission tracking
|
|
17
|
+
- ✅ **Validation Support** - Client-side and server-side validation patterns
|
|
18
|
+
- ✅ **Flexible** - Supports both controlled and uncontrolled form patterns
|
|
19
|
+
- ✅ **Keyboard Navigation** - Full keyboard accessibility including Enter key handlers
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Components Overview
|
|
24
|
+
|
|
25
|
+
### Core Components
|
|
26
|
+
|
|
27
|
+
| Component | Purpose | Key Props |
|
|
28
|
+
|-----------|---------|-----------|
|
|
29
|
+
| **Form** | Form wrapper with submission handling | `onSubmit`, `status`, `noValidate` |
|
|
30
|
+
| **Form.Field** | Label + input wrapper for accessibility | `label`, `labelFor`, `required`, `optional` |
|
|
31
|
+
| **Form.Input** | Text input with validation | `type`, `validationState`, `onEnter` |
|
|
32
|
+
| **Form.Textarea** | Multi-line text input | `rows`, `cols`, `onEnter` |
|
|
33
|
+
| **Form.Select** | Dropdown select with options | `onSelectionChange`, `onEnter` |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Installation & Import
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import Form from '@fpkit/acss';
|
|
41
|
+
// Or import specific components
|
|
42
|
+
import { Form, Input, Field } from '@fpkit/acss';
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Basic Usage
|
|
48
|
+
|
|
49
|
+
### Simple Contact Form
|
|
50
|
+
|
|
51
|
+
A basic form with required fields, proper label associations, and submission handling.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import Form from '@fpkit/acss';
|
|
55
|
+
|
|
56
|
+
function ContactForm() {
|
|
57
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
// Handle form submission
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Form onSubmit={handleSubmit} aria-label="Contact form">
|
|
64
|
+
<Form.Field label="Name" labelFor="name" required>
|
|
65
|
+
<Form.Input id="name" name="name" required />
|
|
66
|
+
</Form.Field>
|
|
67
|
+
|
|
68
|
+
<Form.Field label="Email" labelFor="email" required>
|
|
69
|
+
<Form.Input id="email" name="email" type="email" required />
|
|
70
|
+
</Form.Field>
|
|
71
|
+
|
|
72
|
+
<Form.Field label="Message" labelFor="message">
|
|
73
|
+
<Form.Textarea id="message" name="message" rows={4} />
|
|
74
|
+
</Form.Field>
|
|
75
|
+
|
|
76
|
+
<button type="submit">Submit</button>
|
|
77
|
+
</Form>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Advanced Patterns
|
|
85
|
+
|
|
86
|
+
### Form with Status Management
|
|
87
|
+
|
|
88
|
+
Use the `status` prop to manage form submission states. This example shows how the form automatically sets `aria-busy` and disables fields during submission.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { useState } from 'react';
|
|
92
|
+
import Form, { FormStatus } from '@fpkit/acss';
|
|
4
93
|
|
|
5
|
-
|
|
94
|
+
function RegistrationForm() {
|
|
95
|
+
const [status, setStatus] = useState<FormStatus>('idle');
|
|
6
96
|
|
|
7
|
-
|
|
97
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
98
|
+
e.preventDefault();
|
|
8
99
|
|
|
9
|
-
|
|
10
|
-
- `Input`: A component for rendering input fields of different types (text, email, password, etc.).
|
|
11
|
-
- `Textarea`: A component for rendering a textarea input field.
|
|
12
|
-
- `Select`: A component for rendering a dropdown select input field with options.
|
|
13
|
-
- `Field`: A component for rendering a label and an input field together.
|
|
100
|
+
setStatus('submitting');
|
|
14
101
|
|
|
15
|
-
|
|
102
|
+
try {
|
|
103
|
+
const formData = new FormData(e.currentTarget);
|
|
104
|
+
await fetch('/api/register', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
body: formData,
|
|
107
|
+
});
|
|
108
|
+
setStatus('success');
|
|
109
|
+
} catch (error) {
|
|
110
|
+
setStatus('error');
|
|
111
|
+
}
|
|
112
|
+
};
|
|
16
113
|
|
|
17
|
-
|
|
114
|
+
const isSubmitting = status === 'submitting';
|
|
18
115
|
|
|
19
|
-
|
|
116
|
+
return (
|
|
117
|
+
<Form
|
|
118
|
+
status={status}
|
|
119
|
+
onSubmit={handleSubmit}
|
|
120
|
+
aria-label="Registration form"
|
|
121
|
+
>
|
|
122
|
+
<Form.Field label="Username" labelFor="username" required>
|
|
123
|
+
<Form.Input
|
|
124
|
+
id="username"
|
|
125
|
+
name="username"
|
|
126
|
+
disabled={isSubmitting}
|
|
127
|
+
required
|
|
128
|
+
/>
|
|
129
|
+
</Form.Field>
|
|
20
130
|
|
|
21
|
-
|
|
22
|
-
|
|
131
|
+
<Form.Field label="Password" labelFor="password" required>
|
|
132
|
+
<Form.Input
|
|
133
|
+
id="password"
|
|
134
|
+
name="password"
|
|
135
|
+
type="password"
|
|
136
|
+
minLength={8}
|
|
137
|
+
disabled={isSubmitting}
|
|
138
|
+
required
|
|
139
|
+
/>
|
|
140
|
+
</Form.Field>
|
|
141
|
+
|
|
142
|
+
<button type="submit" disabled={isSubmitting}>
|
|
143
|
+
{isSubmitting ? 'Submitting...' : 'Create Account'}
|
|
144
|
+
</button>
|
|
145
|
+
|
|
146
|
+
{status === 'success' && <p>Account created successfully!</p>}
|
|
147
|
+
{status === 'error' && <p>Error creating account. Please try again.</p>}
|
|
148
|
+
</Form>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
23
151
|
```
|
|
24
152
|
|
|
25
|
-
|
|
153
|
+
### Form with Validation
|
|
154
|
+
|
|
155
|
+
Implement client-side validation with error messages. Notice how the input uses `validationState` to show visual feedback and `aria-invalid` for screen readers.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { useState } from 'react';
|
|
159
|
+
import Form from '@fpkit/acss';
|
|
160
|
+
|
|
161
|
+
function ValidatedForm() {
|
|
162
|
+
const [email, setEmail] = useState('');
|
|
163
|
+
const [emailError, setEmailError] = useState('');
|
|
164
|
+
|
|
165
|
+
const validateEmail = (value: string) => {
|
|
166
|
+
if (!value) {
|
|
167
|
+
setEmailError('Email is required');
|
|
168
|
+
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
169
|
+
setEmailError('Please enter a valid email address');
|
|
170
|
+
} else {
|
|
171
|
+
setEmailError('');
|
|
172
|
+
}
|
|
173
|
+
};
|
|
26
174
|
|
|
27
|
-
|
|
175
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
if (!emailError) {
|
|
178
|
+
// Submit form
|
|
179
|
+
}
|
|
180
|
+
};
|
|
28
181
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
182
|
+
return (
|
|
183
|
+
<Form onSubmit={handleSubmit}>
|
|
184
|
+
<Form.Field
|
|
185
|
+
label="Email"
|
|
186
|
+
labelFor="email"
|
|
187
|
+
required
|
|
188
|
+
errorMessage={emailError}
|
|
189
|
+
>
|
|
190
|
+
<Form.Input
|
|
191
|
+
id="email"
|
|
192
|
+
name="email"
|
|
193
|
+
type="email"
|
|
194
|
+
value={email}
|
|
195
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
196
|
+
onBlur={(e) => validateEmail(e.target.value)}
|
|
197
|
+
validationState={emailError ? 'invalid' : email ? 'valid' : 'none'}
|
|
198
|
+
required
|
|
199
|
+
/>
|
|
200
|
+
</Form.Field>
|
|
201
|
+
|
|
202
|
+
<button type="submit">Submit</button>
|
|
203
|
+
</Form>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
45
206
|
```
|
|
46
207
|
|
|
47
|
-
|
|
208
|
+
### Keyboard-Driven Workflows
|
|
209
|
+
|
|
210
|
+
Use the `onEnter` prop for keyboard-friendly interactions. This example demonstrates `onEnter` on inputs, textareas, and selects.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
import { useState } from 'react';
|
|
214
|
+
import Form from '@fpkit/acss';
|
|
215
|
+
|
|
216
|
+
function SearchForm() {
|
|
217
|
+
const [query, setQuery] = useState('');
|
|
218
|
+
|
|
219
|
+
const handleSearch = () => {
|
|
220
|
+
console.log('Searching for:', query);
|
|
221
|
+
// Perform search
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<Form aria-label="Search form">
|
|
226
|
+
<Form.Field
|
|
227
|
+
label="Search"
|
|
228
|
+
labelFor="search"
|
|
229
|
+
hintText="Press Enter to search"
|
|
230
|
+
>
|
|
231
|
+
<Form.Input
|
|
232
|
+
id="search"
|
|
233
|
+
name="search"
|
|
234
|
+
type="search"
|
|
235
|
+
value={query}
|
|
236
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
237
|
+
onEnter={handleSearch}
|
|
238
|
+
/>
|
|
239
|
+
</Form.Field>
|
|
240
|
+
</Form>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Uncontrolled Form with Native Submission
|
|
246
|
+
|
|
247
|
+
For server-side form handling:
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
function ServerSideForm() {
|
|
251
|
+
return (
|
|
252
|
+
<Form action="/api/contact" formMethod="post">
|
|
253
|
+
<Form.Field label="Name" labelFor="name" required>
|
|
254
|
+
<Form.Input id="name" name="name" required />
|
|
255
|
+
</Form.Field>
|
|
256
|
+
|
|
257
|
+
<Form.Field label="Email" labelFor="email" required>
|
|
258
|
+
<Form.Input id="email" name="email" type="email" required />
|
|
259
|
+
</Form.Field>
|
|
260
|
+
|
|
261
|
+
<button type="submit">Send</button>
|
|
262
|
+
</Form>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Disabled State
|
|
270
|
+
|
|
271
|
+
Form components support an accessible disabled state using the `aria-disabled` pattern, which provides better accessibility than the native HTML `disabled` attribute.
|
|
272
|
+
|
|
273
|
+
### Why aria-disabled?
|
|
274
|
+
|
|
275
|
+
The `aria-disabled` pattern offers key advantages:
|
|
276
|
+
|
|
277
|
+
- **Keyboard Accessibility**: Disabled elements remain in the tab order, allowing keyboard users to discover them
|
|
278
|
+
- **Screen Reader Discovery**: Screen readers can announce the disabled state and read associated tooltips or help text
|
|
279
|
+
- **WCAG Compliance**: Meets WCAG 2.1.1 (Keyboard) and 4.1.2 (Name, Role, Value) requirements
|
|
280
|
+
- **Interactive Help**: Enables tooltips or contextual help on disabled elements to explain why they're disabled
|
|
281
|
+
|
|
282
|
+
### Basic Usage
|
|
283
|
+
|
|
284
|
+
Use the `disabled` prop to disable form inputs, textareas, and selects:
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
import Form from '@fpkit/acss';
|
|
288
|
+
|
|
289
|
+
function DisabledInputExample() {
|
|
290
|
+
return (
|
|
291
|
+
<Form>
|
|
292
|
+
<Form.Field label="Email" labelFor="email">
|
|
293
|
+
<Form.Input
|
|
294
|
+
id="email"
|
|
295
|
+
name="email"
|
|
296
|
+
type="email"
|
|
297
|
+
disabled={true}
|
|
298
|
+
value="locked@example.com"
|
|
299
|
+
/>
|
|
300
|
+
</Form.Field>
|
|
301
|
+
|
|
302
|
+
<Form.Field label="Comments" labelFor="comments">
|
|
303
|
+
<Form.Textarea
|
|
304
|
+
id="comments"
|
|
305
|
+
name="comments"
|
|
306
|
+
disabled={true}
|
|
307
|
+
/>
|
|
308
|
+
</Form.Field>
|
|
309
|
+
|
|
310
|
+
<Form.Field label="Country" labelFor="country">
|
|
311
|
+
<Form.Select id="country" name="country" disabled={true}>
|
|
312
|
+
<option value="us">United States</option>
|
|
313
|
+
<option value="ca">Canada</option>
|
|
314
|
+
</Form.Select>
|
|
315
|
+
</Form.Field>
|
|
316
|
+
</Form>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Migration from isDisabled
|
|
322
|
+
|
|
323
|
+
For backward compatibility, the deprecated `isDisabled` prop is still supported but will be removed in a future version.
|
|
324
|
+
|
|
325
|
+
**Before (deprecated):**
|
|
326
|
+
```tsx
|
|
327
|
+
<Form.Input isDisabled={true} />
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**After (recommended):**
|
|
331
|
+
```tsx
|
|
332
|
+
<Form.Input disabled={true} />
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Both props work identically, but `disabled` follows standard HTML conventions and should be used for all new code.
|
|
336
|
+
|
|
337
|
+
### Disabled State During Form Submission
|
|
338
|
+
|
|
339
|
+
A common pattern is disabling all form fields while a form is submitting:
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { useState } from 'react';
|
|
343
|
+
import Form, { FormStatus } from '@fpkit/acss';
|
|
344
|
+
|
|
345
|
+
function SubmitDisabledForm() {
|
|
346
|
+
const [status, setStatus] = useState<FormStatus>('idle');
|
|
347
|
+
|
|
348
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
setStatus('submitting');
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const formData = new FormData(e.currentTarget);
|
|
354
|
+
await fetch('/api/save', {
|
|
355
|
+
method: 'POST',
|
|
356
|
+
body: formData,
|
|
357
|
+
});
|
|
358
|
+
setStatus('success');
|
|
359
|
+
} catch (error) {
|
|
360
|
+
setStatus('error');
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const isSubmitting = status === 'submitting';
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<Form status={status} onSubmit={handleSubmit}>
|
|
368
|
+
<Form.Field label="Username" labelFor="username" required>
|
|
369
|
+
<Form.Input
|
|
370
|
+
id="username"
|
|
371
|
+
name="username"
|
|
372
|
+
disabled={isSubmitting}
|
|
373
|
+
required
|
|
374
|
+
/>
|
|
375
|
+
</Form.Field>
|
|
376
|
+
|
|
377
|
+
<Form.Field label="Bio" labelFor="bio">
|
|
378
|
+
<Form.Textarea
|
|
379
|
+
id="bio"
|
|
380
|
+
name="bio"
|
|
381
|
+
disabled={isSubmitting}
|
|
382
|
+
/>
|
|
383
|
+
</Form.Field>
|
|
384
|
+
|
|
385
|
+
<button type="submit" disabled={isSubmitting}>
|
|
386
|
+
{isSubmitting ? 'Saving...' : 'Save Profile'}
|
|
387
|
+
</button>
|
|
388
|
+
</Form>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Behavior
|
|
394
|
+
|
|
395
|
+
When a form control is disabled:
|
|
396
|
+
|
|
397
|
+
- **Keyboard Navigation**: Element remains focusable via Tab key (maintains tab order)
|
|
398
|
+
- **Interaction Prevention**: All interaction events are prevented (click, change, keydown, etc.)
|
|
399
|
+
- **Focus Events**: Focus events still work, allowing screen readers to discover and announce the element
|
|
400
|
+
- **Visual Styling**: `.is-disabled` class and `aria-disabled="true"` attribute are applied
|
|
401
|
+
- **Screen Readers**: Announce "disabled" state when focused
|
|
402
|
+
|
|
403
|
+
### Styling
|
|
404
|
+
|
|
405
|
+
Disabled elements can be styled using CSS custom properties:
|
|
406
|
+
|
|
407
|
+
```css
|
|
408
|
+
:root {
|
|
409
|
+
--disabled-opacity: 0.6; /* Visual opacity for disabled state */
|
|
410
|
+
--disabled-cursor: not-allowed; /* Cursor style */
|
|
411
|
+
--disabled-color: hsl(0 0% 40%); /* Text color (3:1 contrast minimum) */
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Override these properties for custom styling:
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
<Form.Input
|
|
419
|
+
disabled={true}
|
|
420
|
+
styles={{
|
|
421
|
+
'--disabled-opacity': '0.5',
|
|
422
|
+
'--disabled-color': '#666666',
|
|
423
|
+
}}
|
|
424
|
+
/>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Selectors Available:**
|
|
428
|
+
- `.is-disabled` - Class added to disabled elements
|
|
429
|
+
- `[aria-disabled="true"]` - Attribute selector
|
|
430
|
+
|
|
431
|
+
### WCAG Compliance
|
|
48
432
|
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
433
|
+
The `aria-disabled` pattern ensures compliance with:
|
|
434
|
+
|
|
435
|
+
- **WCAG 2.1.1 (Keyboard)**: Elements remain in keyboard tab order for discovery
|
|
436
|
+
- **WCAG 4.1.2 (Name, Role, Value)**: `aria-disabled` announces state to screen readers
|
|
437
|
+
- **WCAG 1.4.3 (Contrast Minimum)**: Disabled text maintains 3:1 contrast ratio
|
|
438
|
+
- **WCAG 2.4.7 (Focus Visible)**: Focus indicators preserved on disabled elements
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Component API
|
|
443
|
+
|
|
444
|
+
### Form
|
|
445
|
+
|
|
446
|
+
The main form wrapper component.
|
|
447
|
+
|
|
448
|
+
#### Props
|
|
449
|
+
|
|
450
|
+
| Prop | Type | Default | Description |
|
|
451
|
+
|------|------|---------|-------------|
|
|
452
|
+
| `onSubmit` | `(event: FormEvent) => void` | - | Form submission handler (prevents default) |
|
|
453
|
+
| `status` | `'idle' \| 'submitting' \| 'success' \| 'error'` | `'idle'` | Current form status |
|
|
454
|
+
| `action` | `string` | - | Form submission URL |
|
|
455
|
+
| `formMethod` | `'get' \| 'post'` | `'post'` | HTTP method |
|
|
456
|
+
| `noValidate` | `boolean` | `false` | Disable HTML5 validation |
|
|
457
|
+
| `target` | `string` | - | Form submission target |
|
|
458
|
+
| `id` | `string` | - | Unique form identifier |
|
|
459
|
+
| `name` | `string` | - | Form name attribute |
|
|
460
|
+
| `classes` | `string` | - | CSS class names |
|
|
461
|
+
| `styles` | `CSSProperties` | - | Inline styles |
|
|
462
|
+
|
|
463
|
+
#### Accessibility
|
|
464
|
+
|
|
465
|
+
- **role**: Automatically set to `"form"`
|
|
466
|
+
- **aria-busy**: Set to `true` when `status="submitting"`
|
|
467
|
+
- **data-status**: Reflects current status for CSS styling
|
|
468
|
+
|
|
469
|
+
### Form.Field
|
|
470
|
+
|
|
471
|
+
Wrapper component that associates labels with inputs for accessibility.
|
|
472
|
+
|
|
473
|
+
#### Props
|
|
474
|
+
|
|
475
|
+
| Prop | Type | Default | Description |
|
|
476
|
+
|------|------|---------|-------------|
|
|
477
|
+
| `label` | `ReactNode` | - | **Required.** Label text or element |
|
|
478
|
+
| `labelFor` | `string` | - | ID of associated input (for `htmlFor`) |
|
|
479
|
+
| `required` | `boolean` | `false` | Show required indicator (*) |
|
|
480
|
+
| `optional` | `boolean` | `false` | Show optional indicator |
|
|
481
|
+
| `errorMessage` | `string` | - | Error message to display |
|
|
482
|
+
| `hintText` | `string` | - | Helper text below input |
|
|
483
|
+
|
|
484
|
+
### Form.Input
|
|
485
|
+
|
|
486
|
+
Text input component with validation support.
|
|
487
|
+
|
|
488
|
+
#### Props
|
|
489
|
+
|
|
490
|
+
| Prop | Type | Default | Description |
|
|
491
|
+
|------|------|---------|-------------|
|
|
492
|
+
| `type` | `string` | `'text'` | Input type (text, email, password, etc.) |
|
|
493
|
+
| `validationState` | `'none' \| 'valid' \| 'invalid'` | `'none'` | Validation state |
|
|
494
|
+
| `errorMessage` | `string` | - | Error message for `aria-describedby` |
|
|
495
|
+
| `hintText` | `string` | - | Hint text for `aria-describedby` |
|
|
496
|
+
| `onEnter` | `(event: KeyboardEvent) => void` | - | Handler for Enter key press |
|
|
497
|
+
| `disabled` | `boolean` | `false` | Disable input using `aria-disabled` pattern (remains focusable) |
|
|
498
|
+
| `isDisabled` | `boolean` | `false` | **Deprecated.** Use `disabled` instead |
|
|
499
|
+
| `readOnly` | `boolean` | `false` | Make input read-only |
|
|
500
|
+
| `required` | `boolean` | `false` | Mark input as required |
|
|
501
|
+
|
|
502
|
+
### Form.Textarea
|
|
503
|
+
|
|
504
|
+
Multi-line text input component.
|
|
505
|
+
|
|
506
|
+
#### Props
|
|
507
|
+
|
|
508
|
+
| Prop | Type | Default | Description |
|
|
509
|
+
|------|------|---------|-------------|
|
|
510
|
+
| `rows` | `number` | `5` | Number of visible rows |
|
|
511
|
+
| `cols` | `number` | `25` | Number of visible columns |
|
|
512
|
+
| `onEnter` | `(event: KeyboardEvent) => void` | - | Handler for Enter (without Shift) |
|
|
513
|
+
| `disabled` | `boolean` | `false` | Disable textarea using `aria-disabled` pattern (remains focusable) |
|
|
514
|
+
| `isDisabled` | `boolean` | `false` | **Deprecated.** Use `disabled` instead |
|
|
515
|
+
|
|
516
|
+
**Note**: Shift+Enter adds a new line without triggering `onEnter`.
|
|
517
|
+
|
|
518
|
+
### Form.Select
|
|
519
|
+
|
|
520
|
+
Dropdown select component with keyboard support.
|
|
521
|
+
|
|
522
|
+
#### Props
|
|
523
|
+
|
|
524
|
+
| Prop | Type | Default | Description |
|
|
525
|
+
|------|------|---------|-------------|
|
|
526
|
+
| `onSelectionChange` | `(event: ChangeEvent) => void` | - | Selection change handler |
|
|
527
|
+
| `onEnter` | `(event: KeyboardEvent) => void` | - | Handler for Enter key press |
|
|
528
|
+
| `required` | `boolean` | `false` | Mark select as required |
|
|
529
|
+
| `disabled` | `boolean` | `false` | Disable select using `aria-disabled` pattern (remains focusable) |
|
|
530
|
+
| `isDisabled` | `boolean` | `false` | **Deprecated.** Use `disabled` instead |
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Accessibility Features
|
|
535
|
+
|
|
536
|
+
### WCAG 2.1 AA Compliance
|
|
537
|
+
|
|
538
|
+
All form components meet WCAG 2.1 Level AA standards:
|
|
539
|
+
|
|
540
|
+
1. **WCAG 3.3.1 Error Identification** ✅
|
|
541
|
+
- Error messages clearly associated with inputs via `aria-describedby`
|
|
542
|
+
- Validation states communicated via `aria-invalid`
|
|
543
|
+
|
|
544
|
+
2. **WCAG 3.3.2 Labels or Instructions** ✅
|
|
545
|
+
- All inputs have associated labels via `<label htmlFor>`
|
|
546
|
+
- Required fields marked with visual and programmatic indicators
|
|
547
|
+
|
|
548
|
+
3. **WCAG 4.1.2 Name, Role, Value** ✅
|
|
549
|
+
- All inputs have proper `role`, `name`, and `aria-` attributes
|
|
550
|
+
- Form status communicated via `aria-busy`
|
|
551
|
+
|
|
552
|
+
4. **WCAG 2.4.7 Focus Visible** ✅
|
|
553
|
+
- Focus indicators styled via `:focus-visible` in SCSS
|
|
554
|
+
|
|
555
|
+
5. **WCAG 2.1.1 Keyboard** ✅
|
|
556
|
+
- All interactive elements keyboard accessible
|
|
557
|
+
- `onEnter` prop for custom keyboard workflows
|
|
558
|
+
|
|
559
|
+
6. **Accessible Disabled State** ✅
|
|
560
|
+
- Uses `aria-disabled` pattern instead of native `disabled` attribute
|
|
561
|
+
- Elements remain keyboard focusable (stay in tab order)
|
|
562
|
+
- Screen readers announce disabled state while allowing discovery
|
|
563
|
+
- See [Disabled State](#disabled-state) section for details
|
|
564
|
+
|
|
565
|
+
### Screen Reader Support
|
|
566
|
+
|
|
567
|
+
- Form status changes announced via `aria-busy`
|
|
568
|
+
- Error messages linked to inputs via `aria-describedby`
|
|
569
|
+
- Required fields announced via `aria-required`
|
|
570
|
+
- Validation states communicated via `aria-invalid`
|
|
571
|
+
|
|
572
|
+
### Keyboard Navigation
|
|
573
|
+
|
|
574
|
+
- **Tab**: Navigate between form fields
|
|
575
|
+
- **Shift+Tab**: Navigate backwards
|
|
576
|
+
- **Enter**:
|
|
577
|
+
- In Input: Trigger `onEnter` handler
|
|
578
|
+
- In Textarea: Trigger `onEnter` (or new line with Shift)
|
|
579
|
+
- In Select: Trigger `onEnter` after selection
|
|
580
|
+
- In Form: Submit form (default button behavior)
|
|
581
|
+
- **Escape**: Clear focus (native browser behavior)
|
|
582
|
+
|
|
583
|
+
---
|
|
53
584
|
|
|
54
585
|
## Styling
|
|
55
586
|
|
|
56
|
-
|
|
587
|
+
Form components use CSS custom properties for theming:
|
|
588
|
+
|
|
589
|
+
```css
|
|
590
|
+
:root {
|
|
591
|
+
--input-border-color: gray;
|
|
592
|
+
--input-bg: inherit;
|
|
593
|
+
--input-outline: thin solid var(--input-border-color);
|
|
594
|
+
--input-px: 0.6rem;
|
|
595
|
+
--input-py: 0.4rem;
|
|
596
|
+
--input-fs: var(--fs);
|
|
597
|
+
--input-w: clamp(200px, 100%, 500px);
|
|
598
|
+
--placeholder-color: gray;
|
|
599
|
+
--form-direction: column;
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Status-Based Styling
|
|
604
|
+
|
|
605
|
+
Use the `data-status` attribute for CSS styling:
|
|
57
606
|
|
|
58
|
-
|
|
607
|
+
```css
|
|
608
|
+
form[data-status="submitting"] {
|
|
609
|
+
opacity: 0.6;
|
|
610
|
+
pointer-events: none;
|
|
611
|
+
}
|
|
59
612
|
|
|
60
|
-
|
|
61
|
-
|
|
613
|
+
form[data-status="error"] {
|
|
614
|
+
border-left: 0.25rem solid var(--color-error);
|
|
615
|
+
}
|
|
62
616
|
|
|
63
|
-
|
|
617
|
+
form[data-status="success"] {
|
|
618
|
+
border-left: 0.25rem solid var(--color-success);
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Validation Styling
|
|
623
|
+
|
|
624
|
+
Use the `data-validation` attribute on inputs:
|
|
625
|
+
|
|
626
|
+
```css
|
|
627
|
+
input[data-validation="invalid"] {
|
|
628
|
+
outline-color: var(--color-error);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
input[data-validation="valid"] {
|
|
632
|
+
outline-color: var(--color-success);
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## TypeScript Support
|
|
639
|
+
|
|
640
|
+
All components are fully typed with comprehensive interfaces:
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
import Form, {
|
|
644
|
+
FormProps,
|
|
645
|
+
FormStatus,
|
|
646
|
+
InputProps
|
|
647
|
+
} from '@fpkit/acss';
|
|
648
|
+
|
|
649
|
+
const MyForm: React.FC = () => {
|
|
650
|
+
const [status, setStatus] = useState<FormStatus>('idle');
|
|
651
|
+
|
|
652
|
+
const handleSubmit: FormProps['onSubmit'] = (e) => {
|
|
653
|
+
// Fully typed event
|
|
654
|
+
};
|
|
64
655
|
|
|
65
|
-
<
|
|
656
|
+
return <Form status={status} onSubmit={handleSubmit}>...</Form>;
|
|
657
|
+
};
|
|
66
658
|
```
|
|
67
659
|
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
## Testing
|
|
663
|
+
|
|
664
|
+
Form components are fully tested with Vitest and React Testing Library:
|
|
665
|
+
|
|
666
|
+
```tsx
|
|
667
|
+
import { render, screen, userEvent } from '@testing-library/react';
|
|
668
|
+
import Form from '@fpkit/acss';
|
|
669
|
+
|
|
670
|
+
test('submits form data', async () => {
|
|
671
|
+
const handleSubmit = vi.fn();
|
|
672
|
+
const user = userEvent.setup();
|
|
673
|
+
|
|
674
|
+
render(
|
|
675
|
+
<Form onSubmit={handleSubmit}>
|
|
676
|
+
<Form.Input name="email" />
|
|
677
|
+
<button type="submit">Submit</button>
|
|
678
|
+
</Form>
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
await user.type(screen.getByRole('textbox'), 'test@example.com');
|
|
682
|
+
await user.click(screen.getByRole('button'));
|
|
683
|
+
|
|
684
|
+
expect(handleSubmit).toHaveBeenCalled();
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## Best Practices
|
|
691
|
+
|
|
692
|
+
### ✅ DO
|
|
693
|
+
|
|
694
|
+
- Use `Form.Field` to wrap inputs for proper label association
|
|
695
|
+
- Provide `aria-label` or `aria-labelledby` for forms
|
|
696
|
+
- Use the `status` prop to manage loading states
|
|
697
|
+
- Disable form fields when `status="submitting"`
|
|
698
|
+
- Validate on `onBlur` for better UX
|
|
699
|
+
- Use `required` attribute for HTML5 validation
|
|
700
|
+
- Provide clear error messages
|
|
701
|
+
|
|
702
|
+
### ❌ DON'T
|
|
703
|
+
|
|
704
|
+
- Don't forget to associate labels with inputs
|
|
705
|
+
- Don't submit forms without validation
|
|
706
|
+
- Don't disable submit buttons without explaining why
|
|
707
|
+
- Don't use placeholders as labels
|
|
708
|
+
- Don't forget to handle keyboard events
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## More Examples
|
|
713
|
+
|
|
714
|
+
See the [Form Interactive Guide](./?path=/docs/fp-react-forms-form--docs) for complete examples including:
|
|
715
|
+
|
|
716
|
+
### Form with Hint Text
|
|
717
|
+
|
|
718
|
+
Guide users with helpful hint text below fields, properly associated using `aria-describedby`.
|
|
719
|
+
|
|
720
|
+
### Form with Select Dropdown
|
|
721
|
+
|
|
722
|
+
Combine different form control types for rich data collection.
|
|
723
|
+
|
|
724
|
+
### Form with Optional Fields
|
|
725
|
+
|
|
726
|
+
Clearly distinguish between required and optional fields for better UX.
|
|
727
|
+
|
|
728
|
+
### Complete Registration Form
|
|
729
|
+
|
|
730
|
+
A comprehensive example combining all features: validation, hints, different field types, and proper accessibility.
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## Browser Support
|
|
735
|
+
|
|
736
|
+
- ✅ Chrome/Edge 90+
|
|
737
|
+
- ✅ Firefox 88+
|
|
738
|
+
- ✅ Safari 14+
|
|
739
|
+
- ✅ Mobile Safari (iOS 14+)
|
|
740
|
+
- ✅ Chrome Android
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
68
744
|
## Contributing
|
|
69
745
|
|
|
70
|
-
|
|
746
|
+
Found an issue or have a suggestion? Please [open an issue](https://github.com/your-repo/issues) or submit a pull request.
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## License
|
|
751
|
+
|
|
752
|
+
MIT License - see LICENSE file for details
|