@fpkit/acss 5.0.0 → 6.0.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/components/form/checkbox.css +1 -1
- package/libs/components/form/checkbox.css.map +1 -1
- package/libs/components/form/checkbox.min.css +2 -2
- package/libs/components/form/form.css +1 -1
- package/libs/components/form/form.css.map +1 -1
- package/libs/components/form/form.min.css +2 -2
- package/libs/index.cjs +13 -13
- 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 +69 -20
- package/libs/index.d.ts +69 -20
- package/libs/index.js +3 -3
- package/libs/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/form/CHECKBOX-STYLES.mdx +766 -0
- package/src/components/form/CHECKBOX.mdx +665 -0
- package/src/components/form/checkbox.scss +28 -0
- package/src/components/form/checkbox.tsx +72 -22
- package/src/components/form/form.scss +7 -0
- package/src/components/form/input.stories.tsx +71 -22
- package/src/styles/form/checkbox.css +19 -0
- package/src/styles/form/checkbox.css.map +1 -1
- package/src/styles/form/form.css +25 -0
- package/src/styles/form/form.css.map +1 -1
- package/src/styles/index.css +25 -0
- package/src/styles/index.css.map +1 -1
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { Meta, Canvas, Story, Controls } from "@storybook/addon-docs/blocks";
|
|
2
|
+
import * as CheckboxStories from "./input.stories";
|
|
3
|
+
|
|
4
|
+
<Meta title="FP.REACT Forms/Checkbox/Guide" />
|
|
5
|
+
|
|
6
|
+
# Checkbox Component
|
|
7
|
+
|
|
8
|
+
An accessible checkbox input component with automatic label association,
|
|
9
|
+
simplified boolean API, and semantic size variants.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
The `Checkbox` component provides a clean, accessible API for checkbox inputs
|
|
14
|
+
with automatic label association via `htmlFor`, boolean `onChange` handler, and
|
|
15
|
+
predefined size variants. Built on top of the base `Input` component, it
|
|
16
|
+
maintains full WCAG 2.1 AA compliance while offering an enhanced developer
|
|
17
|
+
experience.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
✅ **Semantic Size Prop** - Predefined `xs`, `sm`, `md`, `lg` sizes via
|
|
22
|
+
intuitive prop API ✅ **Boolean onChange** - Simplified
|
|
23
|
+
`onChange={(checked) => ...}` instead of event objects ✅ **Auto Label
|
|
24
|
+
Association** - Uses `htmlFor` for proper accessibility ✅ **CSS Variable
|
|
25
|
+
Overrides** - Custom sizes beyond presets via `styles` prop ✅ **WCAG 2.1 AA
|
|
26
|
+
Compliant** - aria-disabled pattern for screen reader compatibility ✅
|
|
27
|
+
**Controlled & Uncontrolled** - Both `checked` and `defaultChecked` modes
|
|
28
|
+
supported ✅ **Validation States** - Built-in error, valid, and neutral states
|
|
29
|
+
✅ **Keyboard Accessible** - Space key toggles, full keyboard navigation ✅
|
|
30
|
+
**Focus Indicators** - High contrast focus rings for keyboard users
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @fpkit/acss
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Import
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { Checkbox } from "@fpkit/acss";
|
|
44
|
+
// Import styles
|
|
45
|
+
import "@fpkit/acss/styles";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Basic Usage
|
|
51
|
+
|
|
52
|
+
### Uncontrolled Mode
|
|
53
|
+
|
|
54
|
+
The simplest way to use a checkbox - React doesn't manage the state:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<Checkbox
|
|
58
|
+
id="terms"
|
|
59
|
+
label="I accept the terms and conditions"
|
|
60
|
+
defaultChecked={false}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Controlled Mode
|
|
65
|
+
|
|
66
|
+
For forms where you need to track the checkbox state:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { useState } from "react";
|
|
70
|
+
|
|
71
|
+
function MyForm() {
|
|
72
|
+
const [agreed, setAgreed] = useState(false);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Checkbox
|
|
76
|
+
id="terms"
|
|
77
|
+
label="I accept the terms"
|
|
78
|
+
checked={agreed}
|
|
79
|
+
onChange={setAgreed} // Boolean API - no event object!
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Size Variants
|
|
88
|
+
|
|
89
|
+
### Using the `size` Prop (Recommended)
|
|
90
|
+
|
|
91
|
+
The `size` prop provides predefined size variants optimized for common use
|
|
92
|
+
cases:
|
|
93
|
+
|
|
94
|
+
<Canvas of={CheckboxStories.CheckboxCustomSize} />
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// Extra Small - Compact layouts
|
|
98
|
+
<Checkbox id="xs" label="Extra Small" size="xs" />
|
|
99
|
+
|
|
100
|
+
// Small - Dense forms
|
|
101
|
+
<Checkbox id="sm" label="Small" size="sm" />
|
|
102
|
+
|
|
103
|
+
// Medium - Default, optimal for most cases
|
|
104
|
+
<Checkbox id="md" label="Medium (Default)" size="md" />
|
|
105
|
+
|
|
106
|
+
// Large - Touch-friendly interfaces
|
|
107
|
+
<Checkbox id="lg" label="Large" size="lg" />
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
| Size | Dimensions | Gap | Use Case |
|
|
111
|
+
| ---- | --------------- | --------------- | ---------------------------------------- |
|
|
112
|
+
| `xs` | 0.875rem (14px) | 0.375rem (6px) | Compact forms, space-constrained UIs |
|
|
113
|
+
| `sm` | 1rem (16px) | 0.5rem (8px) | Dense layouts, secondary forms |
|
|
114
|
+
| `md` | 1.25rem (20px) | 0.5rem (8px) | **Default** - Optimal for most use cases |
|
|
115
|
+
| `lg` | 1.5rem (24px) | 0.625rem (10px) | Touch-friendly, mobile-first, prominent |
|
|
116
|
+
|
|
117
|
+
### Custom Sizes via CSS Variables
|
|
118
|
+
|
|
119
|
+
For sizes beyond the standard presets, use the `styles` prop to override CSS
|
|
120
|
+
variables:
|
|
121
|
+
|
|
122
|
+
<Canvas of={CheckboxStories.CheckboxCustomSizeCSSOverride} />
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<Checkbox
|
|
126
|
+
id="custom"
|
|
127
|
+
label="Custom sized checkbox"
|
|
128
|
+
styles={{
|
|
129
|
+
"--checkbox-size": "2rem", // 32px
|
|
130
|
+
"--checkbox-gap": "1rem", // 16px
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**When to use each approach:**
|
|
136
|
+
|
|
137
|
+
- **Size prop**: For 90% of use cases - provides semantic, consistent sizes
|
|
138
|
+
- **CSS variables**: For custom designs, branding, or sizes outside standard
|
|
139
|
+
range
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Validation & States
|
|
144
|
+
|
|
145
|
+
### Required Field
|
|
146
|
+
|
|
147
|
+
Display a red asterisk indicator:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<Checkbox id="required" label="This field is required" required />
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Sets `aria-required="true"` for screen readers.
|
|
154
|
+
|
|
155
|
+
### Error State
|
|
156
|
+
|
|
157
|
+
Show validation errors with associated error message:
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<Checkbox
|
|
161
|
+
id="error"
|
|
162
|
+
label="I accept the terms"
|
|
163
|
+
validationState="invalid"
|
|
164
|
+
errorMessage="You must accept the terms to continue"
|
|
165
|
+
/>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The error message is automatically linked via `aria-describedby`.
|
|
169
|
+
|
|
170
|
+
### Valid State
|
|
171
|
+
|
|
172
|
+
Indicate successful validation:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<Checkbox
|
|
176
|
+
id="valid"
|
|
177
|
+
label="I accept the terms"
|
|
178
|
+
checked={true}
|
|
179
|
+
validationState="valid"
|
|
180
|
+
/>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Example: Form with Validation
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
function TermsAcceptance() {
|
|
187
|
+
const [agreed, setAgreed] = useState(false);
|
|
188
|
+
const [attempted, setAttempted] = useState(false);
|
|
189
|
+
|
|
190
|
+
const handleSubmit = () => {
|
|
191
|
+
setAttempted(true);
|
|
192
|
+
if (!agreed) return; // Block submission
|
|
193
|
+
// Proceed...
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<>
|
|
198
|
+
<Checkbox
|
|
199
|
+
id="terms"
|
|
200
|
+
label="I accept the terms and conditions"
|
|
201
|
+
checked={agreed}
|
|
202
|
+
onChange={setAgreed}
|
|
203
|
+
required
|
|
204
|
+
validationState={attempted && !agreed ? "invalid" : "none"}
|
|
205
|
+
errorMessage={attempted && !agreed ? "Required field" : undefined}
|
|
206
|
+
/>
|
|
207
|
+
<button onClick={handleSubmit}>Submit</button>
|
|
208
|
+
</>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Disabled State
|
|
216
|
+
|
|
217
|
+
### WCAG-Compliant Disabled Pattern
|
|
218
|
+
|
|
219
|
+
Uses `aria-disabled` instead of native `disabled` for better accessibility:
|
|
220
|
+
|
|
221
|
+
<Canvas of={CheckboxStories.CheckboxDisabled} />
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<Checkbox id="disabled" label="Disabled option" disabled defaultChecked />
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Why aria-disabled?**
|
|
228
|
+
|
|
229
|
+
| Feature | `aria-disabled` | Native `disabled` |
|
|
230
|
+
| -------------------------- | --------------- | ----------------- |
|
|
231
|
+
| Keyboard focusable | ✅ Yes | ❌ No |
|
|
232
|
+
| Screen reader discoverable | ✅ Yes | ❌ No |
|
|
233
|
+
| Can show tooltips | ✅ Yes | ❌ No |
|
|
234
|
+
| WCAG 2.1.1 compliant | ✅ Yes | ⚠️ Questionable |
|
|
235
|
+
|
|
236
|
+
**Behavior:**
|
|
237
|
+
|
|
238
|
+
- Remains in keyboard tab order (discoverable)
|
|
239
|
+
- Prevents all interactions (click, change events)
|
|
240
|
+
- Screen readers announce "disabled" state
|
|
241
|
+
- Visual opacity applied via `--checkbox-disabled-opacity`
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Hint Text & Help
|
|
246
|
+
|
|
247
|
+
Provide contextual help without cluttering the label:
|
|
248
|
+
|
|
249
|
+
<Canvas of={CheckboxStories.CheckboxWithHint} />
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
<Checkbox
|
|
253
|
+
id="2fa"
|
|
254
|
+
label="Enable two-factor authentication"
|
|
255
|
+
hintText="Adds an extra layer of security to your account"
|
|
256
|
+
/>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Hint text is associated via `aria-describedby` for screen readers.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Checkbox Groups
|
|
264
|
+
|
|
265
|
+
Group related checkboxes using semantic HTML:
|
|
266
|
+
|
|
267
|
+
<Canvas of={CheckboxStories.CheckboxGroup} />
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
<fieldset>
|
|
271
|
+
<legend>Notification Preferences</legend>
|
|
272
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
273
|
+
<Checkbox
|
|
274
|
+
id="email"
|
|
275
|
+
name="notifications"
|
|
276
|
+
value="email"
|
|
277
|
+
label="Email notifications"
|
|
278
|
+
defaultChecked
|
|
279
|
+
size="lg"
|
|
280
|
+
/>
|
|
281
|
+
<Checkbox
|
|
282
|
+
id="sms"
|
|
283
|
+
name="notifications"
|
|
284
|
+
value="sms"
|
|
285
|
+
label="SMS notifications"
|
|
286
|
+
size="lg"
|
|
287
|
+
/>
|
|
288
|
+
<Checkbox
|
|
289
|
+
id="push"
|
|
290
|
+
name="notifications"
|
|
291
|
+
value="push"
|
|
292
|
+
label="Push notifications"
|
|
293
|
+
size="lg"
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
</fieldset>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Best Practices:**
|
|
300
|
+
|
|
301
|
+
- Use `<fieldset>` to group related checkboxes
|
|
302
|
+
- Provide a `<legend>` describing the group
|
|
303
|
+
- Use the same `name` attribute for backend processing
|
|
304
|
+
- Unique `id` for each checkbox
|
|
305
|
+
- Different `value` for each option
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Accessibility
|
|
310
|
+
|
|
311
|
+
### WCAG 2.1 AA Compliance
|
|
312
|
+
|
|
313
|
+
| Success Criterion | Status | Implementation |
|
|
314
|
+
| ------------------------------ | ------- | ---------------------------------------------- |
|
|
315
|
+
| **2.1.1 Keyboard** | ✅ Pass | Space key toggles, full keyboard navigation |
|
|
316
|
+
| **2.4.7 Focus Visible** | ✅ Pass | High contrast focus rings via `:focus-visible` |
|
|
317
|
+
| **3.3.1 Error Identification** | ✅ Pass | Error messages via `aria-describedby` |
|
|
318
|
+
| **3.3.2 Labels** | ✅ Pass | Required `label` prop with `htmlFor` |
|
|
319
|
+
| **4.1.2 Name, Role, Value** | ✅ Pass | Proper ARIA attributes |
|
|
320
|
+
|
|
321
|
+
### Screen Reader Support
|
|
322
|
+
|
|
323
|
+
- **Label announcement**: Automatically associated via `htmlFor={id}`
|
|
324
|
+
- **Required state**: Announced via `aria-required="true"` + visual asterisk
|
|
325
|
+
- **Error messages**: Linked via `aria-describedby` attribute
|
|
326
|
+
- **Disabled state**: Uses `aria-disabled="true"` (remains focusable)
|
|
327
|
+
- **Validation state**: Communicated via `aria-invalid`
|
|
328
|
+
|
|
329
|
+
Tested with:
|
|
330
|
+
|
|
331
|
+
- NVDA (Windows)
|
|
332
|
+
- JAWS (Windows)
|
|
333
|
+
- VoiceOver (macOS, iOS)
|
|
334
|
+
- TalkBack (Android)
|
|
335
|
+
|
|
336
|
+
### Keyboard Navigation
|
|
337
|
+
|
|
338
|
+
| Key | Action |
|
|
339
|
+
| ------------- | ---------------------- |
|
|
340
|
+
| `Tab` | Focus checkbox |
|
|
341
|
+
| `Space` | Toggle checked state |
|
|
342
|
+
| `Shift + Tab` | Focus previous element |
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Props API
|
|
347
|
+
|
|
348
|
+
### CheckboxProps
|
|
349
|
+
|
|
350
|
+
| Prop | Type | Default | Required | Description |
|
|
351
|
+
| ----------------- | -------------------------------- | ------------------ | -------- | ------------------------------------------------ |
|
|
352
|
+
| `id` | `string` | - | ✅ Yes | Unique identifier for label association |
|
|
353
|
+
| `label` | `ReactNode` | - | ✅ Yes | Label text or element displayed next to checkbox |
|
|
354
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'md'` | No | Predefined size variant |
|
|
355
|
+
| `checked` | `boolean` | - | No | Controlled mode: current checked state |
|
|
356
|
+
| `defaultChecked` | `boolean` | `false` | No | Uncontrolled mode: initial checked state |
|
|
357
|
+
| `onChange` | `(checked: boolean) => void` | - | No | Boolean change handler (not event object!) |
|
|
358
|
+
| `disabled` | `boolean` | `false` | No | Disable checkbox (aria-disabled pattern) |
|
|
359
|
+
| `required` | `boolean` | `false` | No | Mark as required (shows asterisk) |
|
|
360
|
+
| `validationState` | `'valid' \| 'invalid' \| 'none'` | `'none'` | No | Validation state |
|
|
361
|
+
| `errorMessage` | `string` | - | No | Error message text (linked via aria-describedby) |
|
|
362
|
+
| `hintText` | `string` | - | No | Helper text below checkbox |
|
|
363
|
+
| `name` | `string` | - | No | Form input name attribute |
|
|
364
|
+
| `value` | `string` | `'on'` | No | Form submission value when checked |
|
|
365
|
+
| `classes` | `string` | - | No | Custom CSS classes for wrapper div |
|
|
366
|
+
| `inputClasses` | `string` | `'checkbox-input'` | No | Custom CSS classes for input element |
|
|
367
|
+
| `styles` | `CSSProperties` | - | No | Inline styles with CSS variables |
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## CSS Customization
|
|
372
|
+
|
|
373
|
+
See [CHECKBOX-STYLES.mdx](./CHECKBOX-STYLES.mdx) for comprehensive CSS
|
|
374
|
+
documentation.
|
|
375
|
+
|
|
376
|
+
### Quick Reference
|
|
377
|
+
|
|
378
|
+
Override these CSS variables for custom styling:
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
<Checkbox
|
|
382
|
+
id="custom"
|
|
383
|
+
label="Custom styled"
|
|
384
|
+
styles={{
|
|
385
|
+
"--checkbox-size": "2rem",
|
|
386
|
+
"--checkbox-gap": "1rem",
|
|
387
|
+
"--checkbox-radius": "0.5rem",
|
|
388
|
+
"--checkbox-checked-bg": "#0066cc",
|
|
389
|
+
"--checkbox-focus-ring-color": "#ff0000",
|
|
390
|
+
}}
|
|
391
|
+
/>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Examples
|
|
397
|
+
|
|
398
|
+
### Multi-Step Form
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
function MultiStepForm() {
|
|
402
|
+
const [step1Complete, setStep1Complete] = useState(false);
|
|
403
|
+
const [step2Complete, setStep2Complete] = useState(false);
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<div>
|
|
407
|
+
<Checkbox
|
|
408
|
+
id="step1"
|
|
409
|
+
label="Complete registration details"
|
|
410
|
+
checked={step1Complete}
|
|
411
|
+
onChange={setStep1Complete}
|
|
412
|
+
size="lg"
|
|
413
|
+
/>
|
|
414
|
+
<Checkbox
|
|
415
|
+
id="step2"
|
|
416
|
+
label="Verify email address"
|
|
417
|
+
checked={step2Complete}
|
|
418
|
+
onChange={setStep2Complete}
|
|
419
|
+
disabled={!step1Complete}
|
|
420
|
+
size="lg"
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Select All Pattern
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
function SelectAllList() {
|
|
431
|
+
const [items, setItems] = useState([
|
|
432
|
+
{ id: 1, label: "Item 1", checked: false },
|
|
433
|
+
{ id: 2, label: "Item 2", checked: false },
|
|
434
|
+
{ id: 3, label: "Item 3", checked: false },
|
|
435
|
+
]);
|
|
436
|
+
|
|
437
|
+
const allChecked = items.every((item) => item.checked);
|
|
438
|
+
const someChecked = items.some((item) => item.checked) && !allChecked;
|
|
439
|
+
|
|
440
|
+
const toggleAll = (checked: boolean) => {
|
|
441
|
+
setItems(items.map((item) => ({ ...item, checked })));
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const toggleItem = (id: number, checked: boolean) => {
|
|
445
|
+
setItems(
|
|
446
|
+
items.map((item) => (item.id === id ? { ...item, checked } : item))
|
|
447
|
+
);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div>
|
|
452
|
+
<Checkbox
|
|
453
|
+
id="select-all"
|
|
454
|
+
label="Select All"
|
|
455
|
+
checked={allChecked}
|
|
456
|
+
onChange={toggleAll}
|
|
457
|
+
styles={{
|
|
458
|
+
"--checkbox-fw": someChecked ? "600" : "400",
|
|
459
|
+
}}
|
|
460
|
+
/>
|
|
461
|
+
<hr />
|
|
462
|
+
{items.map((item) => (
|
|
463
|
+
<Checkbox
|
|
464
|
+
key={item.id}
|
|
465
|
+
id={`item-${item.id}`}
|
|
466
|
+
label={item.label}
|
|
467
|
+
checked={item.checked}
|
|
468
|
+
onChange={(checked) => toggleItem(item.id, checked)}
|
|
469
|
+
/>
|
|
470
|
+
))}
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Migration Guide
|
|
479
|
+
|
|
480
|
+
### From v4.x to v5.0
|
|
481
|
+
|
|
482
|
+
**New Feature: Size Prop**
|
|
483
|
+
|
|
484
|
+
No breaking changes! The new `size` prop is optional and fully backward
|
|
485
|
+
compatible.
|
|
486
|
+
|
|
487
|
+
**Before (still works):**
|
|
488
|
+
|
|
489
|
+
```tsx
|
|
490
|
+
<Checkbox
|
|
491
|
+
id="large"
|
|
492
|
+
label="Large checkbox"
|
|
493
|
+
styles={{ "--checkbox-size": "1.5rem" }}
|
|
494
|
+
/>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**After (recommended):**
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
<Checkbox id="large" label="Large checkbox" size="lg" />
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Benefits of migration:**
|
|
504
|
+
|
|
505
|
+
- ✅ Cleaner API - prop instead of CSS variable string
|
|
506
|
+
- ✅ Type safety - TypeScript autocomplete for sizes
|
|
507
|
+
- ✅ Consistency - matches Button component pattern
|
|
508
|
+
- ✅ Flexibility - `styles` prop still works for custom sizes
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Browser Support
|
|
513
|
+
|
|
514
|
+
- ✅ Chrome 105+ (`:has()` selector support)
|
|
515
|
+
- ✅ Firefox 121+
|
|
516
|
+
- ✅ Safari 15.4+
|
|
517
|
+
- ✅ Edge 105+
|
|
518
|
+
- ✅ Mobile Safari (iOS 15.4+)
|
|
519
|
+
- ✅ Chrome Android
|
|
520
|
+
|
|
521
|
+
**Graceful degradation:** Older browsers receive standard checkbox styling
|
|
522
|
+
without advanced `:has()` selectors.
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## TypeScript
|
|
527
|
+
|
|
528
|
+
Full TypeScript support with exported types:
|
|
529
|
+
|
|
530
|
+
```tsx
|
|
531
|
+
import { Checkbox, type CheckboxProps } from "@fpkit/acss";
|
|
532
|
+
|
|
533
|
+
// Custom wrapper with pre-configured props
|
|
534
|
+
const TermsCheckbox: React.FC<Omit<CheckboxProps, "label">> = (props) => {
|
|
535
|
+
return (
|
|
536
|
+
<Checkbox label="I accept the terms and conditions" size="lg" {...props} />
|
|
537
|
+
);
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// Type-safe onChange handler
|
|
541
|
+
const handleChange: CheckboxProps["onChange"] = (checked) => {
|
|
542
|
+
console.log("Checked:", checked); // boolean, not event!
|
|
543
|
+
};
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Testing
|
|
549
|
+
|
|
550
|
+
### Unit Testing with React Testing Library
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
553
|
+
import { render, screen } from "@testing-library/react";
|
|
554
|
+
import userEvent from "@testing-library/user-event";
|
|
555
|
+
import { Checkbox } from "@fpkit/acss";
|
|
556
|
+
|
|
557
|
+
test("toggles on click", async () => {
|
|
558
|
+
const handleChange = jest.fn();
|
|
559
|
+
|
|
560
|
+
render(<Checkbox id="test" label="Test checkbox" onChange={handleChange} />);
|
|
561
|
+
|
|
562
|
+
const checkbox = screen.getByRole("checkbox");
|
|
563
|
+
await userEvent.click(checkbox);
|
|
564
|
+
|
|
565
|
+
expect(handleChange).toHaveBeenCalledWith(true);
|
|
566
|
+
expect(checkbox).toBeChecked();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("label click toggles checkbox", async () => {
|
|
570
|
+
render(<Checkbox id="test" label="Click me" />);
|
|
571
|
+
|
|
572
|
+
const label = screen.getByText("Click me");
|
|
573
|
+
await userEvent.click(label);
|
|
574
|
+
|
|
575
|
+
expect(screen.getByRole("checkbox")).toBeChecked();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("size prop applies data attribute", () => {
|
|
579
|
+
const { container } = render(<Checkbox id="test" label="Test" size="lg" />);
|
|
580
|
+
|
|
581
|
+
const wrapper = container.querySelector('[data-checkbox-size="lg"]');
|
|
582
|
+
expect(wrapper).toBeInTheDocument();
|
|
583
|
+
});
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Accessibility Testing
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
test("has proper ARIA attributes", () => {
|
|
590
|
+
render(
|
|
591
|
+
<Checkbox
|
|
592
|
+
id="test"
|
|
593
|
+
label="Test"
|
|
594
|
+
required
|
|
595
|
+
validationState="invalid"
|
|
596
|
+
errorMessage="Required"
|
|
597
|
+
/>
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
const checkbox = screen.getByRole("checkbox");
|
|
601
|
+
expect(checkbox).toHaveAttribute("aria-required", "true");
|
|
602
|
+
expect(checkbox).toHaveAttribute("aria-invalid", "true");
|
|
603
|
+
expect(checkbox).toHaveAttribute("aria-describedby");
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("disabled checkbox is focusable", async () => {
|
|
607
|
+
render(<Checkbox id="test" label="Disabled" disabled />);
|
|
608
|
+
|
|
609
|
+
const checkbox = screen.getByRole("checkbox");
|
|
610
|
+
expect(checkbox).toHaveAttribute("aria-disabled", "true");
|
|
611
|
+
|
|
612
|
+
// Should be focusable
|
|
613
|
+
await userEvent.tab();
|
|
614
|
+
expect(checkbox).toHaveFocus();
|
|
615
|
+
|
|
616
|
+
// But interactions should be prevented
|
|
617
|
+
await userEvent.click(checkbox);
|
|
618
|
+
expect(checkbox).not.toBeChecked();
|
|
619
|
+
});
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## Related Components
|
|
625
|
+
|
|
626
|
+
- [Input](../input) - Base input component
|
|
627
|
+
- [Field](../field) - Form field wrapper
|
|
628
|
+
- [Button](../../buttons/button) - Button component (also uses size prop)
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## Changelog
|
|
633
|
+
|
|
634
|
+
### v5.0.0 (2025-01-11)
|
|
635
|
+
|
|
636
|
+
**✨ Added:**
|
|
637
|
+
|
|
638
|
+
- New `size` prop with `xs`, `sm`, `md`, `lg` variants
|
|
639
|
+
- Size tokens in CSS: `--checkbox-size-xs/sm/md/lg`
|
|
640
|
+
- Gap tokens: `--checkbox-gap-xs/sm/md/lg`
|
|
641
|
+
- Data attribute pattern: `data-checkbox-size`
|
|
642
|
+
- Comprehensive documentation (this guide + STYLES guide)
|
|
643
|
+
|
|
644
|
+
**🔧 Changed:**
|
|
645
|
+
|
|
646
|
+
- None - fully backward compatible
|
|
647
|
+
|
|
648
|
+
**🗑️ Deprecated:**
|
|
649
|
+
|
|
650
|
+
- None
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
## Support & Resources
|
|
655
|
+
|
|
656
|
+
- **NPM**: [@fpkit/acss](https://www.npmjs.com/package/@fpkit/acss)
|
|
657
|
+
- **GitHub**: [Report issues](https://github.com/your-org/fpkit/issues)
|
|
658
|
+
- **Storybook**: Interactive examples
|
|
659
|
+
- **API Docs**: See Props API section above
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## License
|
|
664
|
+
|
|
665
|
+
MIT © fpkit
|
|
@@ -85,6 +85,34 @@ div:has(> input[type="checkbox"]) {
|
|
|
85
85
|
color: var(--checkbox-valid-label-color, currentColor);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
/* ==========================================================================
|
|
90
|
+
Size Variants - Applied via data-checkbox-size attribute
|
|
91
|
+
========================================================================== */
|
|
92
|
+
|
|
93
|
+
// Extra Small variant
|
|
94
|
+
&[data-checkbox-size~="xs"] {
|
|
95
|
+
--checkbox-size: var(--checkbox-size-xs);
|
|
96
|
+
--checkbox-gap: var(--checkbox-gap-xs, 0.375rem);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Small variant
|
|
100
|
+
&[data-checkbox-size~="sm"] {
|
|
101
|
+
--checkbox-size: var(--checkbox-size-sm);
|
|
102
|
+
--checkbox-gap: var(--checkbox-gap-sm, 0.5rem);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Medium variant (default - no attribute needed, but included for explicitness)
|
|
106
|
+
&[data-checkbox-size~="md"] {
|
|
107
|
+
--checkbox-size: var(--checkbox-size-md);
|
|
108
|
+
--checkbox-gap: var(--checkbox-gap-md, 0.5rem);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Large variant
|
|
112
|
+
&[data-checkbox-size~="lg"] {
|
|
113
|
+
--checkbox-size: var(--checkbox-size-lg);
|
|
114
|
+
--checkbox-gap: var(--checkbox-gap-lg, 0.625rem);
|
|
115
|
+
}
|
|
88
116
|
}
|
|
89
117
|
|
|
90
118
|
// Checkbox label styling
|