@fpkit/acss 6.1.0 → 6.2.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/layout/landmarks.css +1 -1
- package/libs/components/layout/landmarks.css.map +1 -1
- package/libs/components/layout/landmarks.min.css +2 -2
- package/libs/index.cjs +20 -19
- 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 +38 -1
- package/libs/index.d.ts +38 -1
- package/libs/index.js +9 -9
- package/libs/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/layout/README.mdx +1117 -0
- package/src/components/layout/STYLES.mdx +159 -4
- package/src/components/layout/fieldset.stories.tsx +387 -0
- package/src/components/layout/landmarks.scss +115 -2
- package/src/components/layout/landmarks.stories.tsx +2 -6
- package/src/components/layout/landmarks.tsx +96 -27
- package/src/styles/index.css +82 -0
- package/src/styles/index.css.map +1 -1
- package/src/styles/layout/landmarks.css +83 -0
- package/src/styles/layout/landmarks.css.map +1 -1
|
@@ -344,6 +344,159 @@ footer > div {
|
|
|
344
344
|
</footer>
|
|
345
345
|
```
|
|
346
346
|
|
|
347
|
+
## Fieldset
|
|
348
|
+
|
|
349
|
+
The `<fieldset>` element provides semantic grouping for related content with an optional `<legend>` label. Fieldsets automatically create an accessible group (`role="group"`) for better screen reader support.
|
|
350
|
+
|
|
351
|
+
### Basic Fieldset
|
|
352
|
+
|
|
353
|
+
```html
|
|
354
|
+
<fieldset>
|
|
355
|
+
<legend>Personal Information</legend>
|
|
356
|
+
<section>
|
|
357
|
+
<p>Name: John Doe</p>
|
|
358
|
+
<p>Email: john@example.com</p>
|
|
359
|
+
</section>
|
|
360
|
+
</fieldset>
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**CSS Applied:**
|
|
364
|
+
|
|
365
|
+
```css
|
|
366
|
+
fieldset {
|
|
367
|
+
border: 1px solid #ccc;
|
|
368
|
+
border-radius: 0.5rem; /* 8px */
|
|
369
|
+
padding: 1rem; /* 16px */
|
|
370
|
+
padding-inline: 1.5rem; /* 24px */
|
|
371
|
+
padding-block: 1rem; /* 16px */
|
|
372
|
+
margin-block: 2rem; /* 32px */
|
|
373
|
+
background: transparent;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fieldset > legend {
|
|
377
|
+
font-size: 1rem; /* 16px */
|
|
378
|
+
font-weight: 600;
|
|
379
|
+
padding-inline: 0.5rem; /* 8px */
|
|
380
|
+
color: currentColor;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
fieldset > section {
|
|
384
|
+
display: flex;
|
|
385
|
+
flex-direction: column;
|
|
386
|
+
gap: 1rem; /* 16px */
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Fieldset Without Legend
|
|
391
|
+
|
|
392
|
+
Fieldsets can group content without a visible legend:
|
|
393
|
+
|
|
394
|
+
```html
|
|
395
|
+
<fieldset>
|
|
396
|
+
<section>
|
|
397
|
+
<p>Grouped content without visible legend</p>
|
|
398
|
+
<p>Still maintains semantic grouping</p>
|
|
399
|
+
</section>
|
|
400
|
+
</fieldset>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Inline Legend Variant
|
|
404
|
+
|
|
405
|
+
For compact layouts, legends can be displayed inline:
|
|
406
|
+
|
|
407
|
+
```html
|
|
408
|
+
<fieldset data-legend="inline">
|
|
409
|
+
<legend>Status:</legend>
|
|
410
|
+
<section>
|
|
411
|
+
<p>Active</p>
|
|
412
|
+
</section>
|
|
413
|
+
</fieldset>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**CSS Applied:**
|
|
417
|
+
|
|
418
|
+
```css
|
|
419
|
+
fieldset[data-legend="inline"] {
|
|
420
|
+
--fieldset-border: none;
|
|
421
|
+
--fieldset-padding: 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
fieldset[data-legend="inline"] > legend {
|
|
425
|
+
float: inline-start;
|
|
426
|
+
margin-inline-end: 1rem; /* 16px */
|
|
427
|
+
margin-block-end: 0.5rem; /* 8px */
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Grouped Variant
|
|
432
|
+
|
|
433
|
+
Emphasized fieldsets for important content sections:
|
|
434
|
+
|
|
435
|
+
```html
|
|
436
|
+
<fieldset data-fieldset="grouped">
|
|
437
|
+
<legend>Account Settings</legend>
|
|
438
|
+
<section>
|
|
439
|
+
<p><strong>Username:</strong> johndoe</p>
|
|
440
|
+
<p><strong>Role:</strong> Admin</p>
|
|
441
|
+
</section>
|
|
442
|
+
</fieldset>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**CSS Applied:**
|
|
446
|
+
|
|
447
|
+
```css
|
|
448
|
+
fieldset[data-fieldset="grouped"] {
|
|
449
|
+
--fieldset-bg: #f9f9f9;
|
|
450
|
+
--fieldset-padding-block: 1.5rem; /* 24px */
|
|
451
|
+
--fieldset-border: 2px solid #0066cc;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fieldset[data-fieldset="grouped"] > legend {
|
|
455
|
+
--legend-fs: 1.125rem; /* 18px */
|
|
456
|
+
--legend-fw: 700;
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Fieldset CSS Custom Properties
|
|
461
|
+
|
|
462
|
+
```css
|
|
463
|
+
fieldset {
|
|
464
|
+
/* Border */
|
|
465
|
+
--fieldset-border: 1px solid #ccc;
|
|
466
|
+
--fieldset-border-radius: 0.5rem;
|
|
467
|
+
|
|
468
|
+
/* Spacing */
|
|
469
|
+
--fieldset-padding: 1rem;
|
|
470
|
+
--fieldset-padding-inline: 1.5rem;
|
|
471
|
+
--fieldset-padding-block: 1rem;
|
|
472
|
+
--fieldset-margin-block: 2rem;
|
|
473
|
+
|
|
474
|
+
/* Colors */
|
|
475
|
+
--fieldset-bg: transparent;
|
|
476
|
+
|
|
477
|
+
/* Legend */
|
|
478
|
+
--legend-fs: 1rem;
|
|
479
|
+
--legend-fw: 600;
|
|
480
|
+
--legend-padding-inline: 0.5rem;
|
|
481
|
+
--legend-color: currentColor;
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Customizing Fieldset Styles
|
|
486
|
+
|
|
487
|
+
Override CSS custom properties for themed fieldsets:
|
|
488
|
+
|
|
489
|
+
```html
|
|
490
|
+
<fieldset
|
|
491
|
+
style="--fieldset-border: 2px dashed #666; --fieldset-bg: #f0f0f0; --legend-color: #0066cc"
|
|
492
|
+
>
|
|
493
|
+
<legend>Custom Styled Fieldset</legend>
|
|
494
|
+
<section>
|
|
495
|
+
<p>Content with custom styling</p>
|
|
496
|
+
</section>
|
|
497
|
+
</fieldset>
|
|
498
|
+
```
|
|
499
|
+
|
|
347
500
|
## Real-World Examples
|
|
348
501
|
|
|
349
502
|
### Full Page Layout
|
|
@@ -484,10 +637,12 @@ Hero background images should use empty alt or role="presentation":
|
|
|
484
637
|
|
|
485
638
|
## CSS Variable Naming Convention
|
|
486
639
|
|
|
487
|
-
| Category
|
|
488
|
-
|
|
|
489
|
-
| **Overlay**
|
|
490
|
-
| **Content**
|
|
640
|
+
| Category | Variable Pattern | Example |
|
|
641
|
+
| ------------ | ----------------------- | ---------------------------------------- |
|
|
642
|
+
| **Overlay** | `--overlay-{property}` | `--overlay-bg`, `--overlay-height` |
|
|
643
|
+
| **Content** | `--content-{property}` | `--content-width`, `--content-gap` |
|
|
644
|
+
| **Fieldset** | `--fieldset-{property}` | `--fieldset-border`, `--fieldset-bg` |
|
|
645
|
+
| **Legend** | `--legend-{property}` | `--legend-fs`, `--legend-fw` |
|
|
491
646
|
|
|
492
647
|
## Browser Support
|
|
493
648
|
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { StoryObj, Meta } from "@storybook/react-vite";
|
|
2
|
+
import { within, expect, userEvent } from "storybook/test";
|
|
3
|
+
|
|
4
|
+
import { Fieldset } from "./landmarks";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Fieldset> = {
|
|
7
|
+
title: "FP.React Components/Layout/Fieldset",
|
|
8
|
+
component: Fieldset,
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component: `Fieldset component for semantic content grouping with optional legend.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Semantic HTML** - Uses native \`<fieldset>\` and \`<legend>\` elements
|
|
17
|
+
- **Accessible** - Automatic \`role="group"\` for screen readers
|
|
18
|
+
- **Optional Legend** - Flexible content grouping with or without visible label
|
|
19
|
+
- **CSS Custom Properties** - Full theming control
|
|
20
|
+
- **Variants** - Inline legend and grouped emphasis styles
|
|
21
|
+
|
|
22
|
+
## CSS Variables
|
|
23
|
+
|
|
24
|
+
### Fieldset
|
|
25
|
+
- \`--fieldset-border\`: Border style (default: 1px solid #ccc)
|
|
26
|
+
- \`--fieldset-border-radius\`: Border radius (default: 0.5rem)
|
|
27
|
+
- \`--fieldset-padding\`: General padding (default: 1rem)
|
|
28
|
+
- \`--fieldset-padding-inline\`: Horizontal padding (default: 1.5rem)
|
|
29
|
+
- \`--fieldset-padding-block\`: Vertical padding (default: 1rem)
|
|
30
|
+
- \`--fieldset-margin-block\`: Vertical margin (default: 2rem)
|
|
31
|
+
- \`--fieldset-bg\`: Background color (default: transparent)
|
|
32
|
+
|
|
33
|
+
### Legend
|
|
34
|
+
- \`--legend-fs\`: Font size (default: 1rem)
|
|
35
|
+
- \`--legend-fw\`: Font weight (default: 600)
|
|
36
|
+
- \`--legend-padding-inline\`: Horizontal padding (default: 0.5rem)
|
|
37
|
+
- \`--legend-color\`: Text color (default: currentColor)
|
|
38
|
+
|
|
39
|
+
## Variants
|
|
40
|
+
|
|
41
|
+
### Inline Legend (\`data-legend="inline"\`)
|
|
42
|
+
Compact layout with legend displayed inline:
|
|
43
|
+
- Removes border and padding
|
|
44
|
+
- Floats legend to inline-start
|
|
45
|
+
- Adds horizontal margin after legend
|
|
46
|
+
|
|
47
|
+
### Grouped (\`data-fieldset="grouped"\`)
|
|
48
|
+
Emphasized styling for important sections:
|
|
49
|
+
- Subtle background color
|
|
50
|
+
- Thicker accent border
|
|
51
|
+
- Increased padding and font weight
|
|
52
|
+
`,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
args: {
|
|
57
|
+
children: "Default Fieldset Content",
|
|
58
|
+
},
|
|
59
|
+
tags: ["stable"],
|
|
60
|
+
} as Meta;
|
|
61
|
+
|
|
62
|
+
export default meta;
|
|
63
|
+
type Story = StoryObj<typeof Fieldset>;
|
|
64
|
+
|
|
65
|
+
// Basic fieldset
|
|
66
|
+
export const BasicFieldset: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
legend: "Personal Information",
|
|
69
|
+
children: (
|
|
70
|
+
<>
|
|
71
|
+
<p>Name: John Doe</p>
|
|
72
|
+
<p>Email: john@example.com</p>
|
|
73
|
+
</>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
play: async ({ canvasElement }) => {
|
|
77
|
+
const canvas = within(canvasElement);
|
|
78
|
+
expect(canvas.getByRole("group")).toBeInTheDocument();
|
|
79
|
+
expect(canvas.getByText("Personal Information")).toBeInTheDocument();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Inline legend
|
|
84
|
+
export const InlineLegendFieldset: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
legend: "Status:",
|
|
87
|
+
"data-legend": "inline",
|
|
88
|
+
children: <p>Active</p>,
|
|
89
|
+
},
|
|
90
|
+
play: async ({ canvasElement }) => {
|
|
91
|
+
const canvas = within(canvasElement);
|
|
92
|
+
expect(canvas.getByRole("group")).toHaveAttribute("data-legend", "inline");
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Grouped variant
|
|
97
|
+
export const GroupedFieldset: Story = {
|
|
98
|
+
args: {
|
|
99
|
+
legend: "Account Settings",
|
|
100
|
+
"data-fieldset": "grouped",
|
|
101
|
+
children: (
|
|
102
|
+
<>
|
|
103
|
+
<p>
|
|
104
|
+
<strong>Username:</strong> johndoe
|
|
105
|
+
</p>
|
|
106
|
+
<p>
|
|
107
|
+
<strong>Role:</strong> Admin
|
|
108
|
+
</p>
|
|
109
|
+
</>
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
play: async ({ canvasElement }) => {
|
|
113
|
+
const canvas = within(canvasElement);
|
|
114
|
+
expect(canvas.getByRole("group")).toHaveAttribute(
|
|
115
|
+
"data-fieldset",
|
|
116
|
+
"grouped"
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// No legend example
|
|
122
|
+
export const FieldsetNoLegend: Story = {
|
|
123
|
+
args: {
|
|
124
|
+
children: <p>Grouped content without visible legend</p>,
|
|
125
|
+
},
|
|
126
|
+
play: async ({ canvasElement }) => {
|
|
127
|
+
const canvas = within(canvasElement);
|
|
128
|
+
expect(canvas.getByRole("group")).toBeInTheDocument();
|
|
129
|
+
expect(canvas.queryAllByRole("legend")).toHaveLength(0);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Custom styled fieldset
|
|
134
|
+
export const CustomStyledFieldset: Story = {
|
|
135
|
+
args: {
|
|
136
|
+
legend: "Custom Styled",
|
|
137
|
+
children: (
|
|
138
|
+
<>
|
|
139
|
+
<p>This fieldset demonstrates custom CSS variable overrides.</p>
|
|
140
|
+
<p>Border, background, and typography are customized.</p>
|
|
141
|
+
</>
|
|
142
|
+
),
|
|
143
|
+
styles: {
|
|
144
|
+
"--fieldset-border": "2px dashed #666",
|
|
145
|
+
"--fieldset-bg": "#f0f0f0",
|
|
146
|
+
"--legend-color": "#0066cc",
|
|
147
|
+
"--legend-fs": "1.25rem",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
play: async ({ canvasElement }) => {
|
|
151
|
+
const canvas = within(canvasElement);
|
|
152
|
+
expect(canvas.getByRole("group")).toBeInTheDocument();
|
|
153
|
+
expect(canvas.getByText("Custom Styled")).toBeInTheDocument();
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Accessibility Test - WCAG 2.1 Level AA Compliance
|
|
158
|
+
export const AccessibilityTest: Story = {
|
|
159
|
+
args: {
|
|
160
|
+
id: "contact-fieldset",
|
|
161
|
+
legend: "Contact Information",
|
|
162
|
+
description: "Please provide your contact details for follow-up",
|
|
163
|
+
children: (
|
|
164
|
+
<>
|
|
165
|
+
<label htmlFor="a11y-name">
|
|
166
|
+
Name <span aria-label="required">*</span>
|
|
167
|
+
</label>
|
|
168
|
+
<input id="a11y-name" type="text" required />
|
|
169
|
+
|
|
170
|
+
<label htmlFor="a11y-email">
|
|
171
|
+
Email <span aria-label="required">*</span>
|
|
172
|
+
</label>
|
|
173
|
+
<input id="a11y-email" type="email" required />
|
|
174
|
+
|
|
175
|
+
<label htmlFor="a11y-phone">Phone (optional)</label>
|
|
176
|
+
<input id="a11y-phone" type="tel" />
|
|
177
|
+
</>
|
|
178
|
+
),
|
|
179
|
+
},
|
|
180
|
+
play: async ({ canvasElement, step }) => {
|
|
181
|
+
const canvas = within(canvasElement);
|
|
182
|
+
|
|
183
|
+
await step("Verify group role", async () => {
|
|
184
|
+
const fieldset = canvas.getByRole("group");
|
|
185
|
+
expect(fieldset).toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await step("Verify aria-describedby", async () => {
|
|
189
|
+
const fieldset = canvas.getByRole("group");
|
|
190
|
+
expect(fieldset).toHaveAttribute("aria-describedby");
|
|
191
|
+
expect(fieldset.getAttribute("aria-describedby")).toBe(
|
|
192
|
+
"contact-fieldset-desc"
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await step("Verify description is present", async () => {
|
|
197
|
+
expect(
|
|
198
|
+
canvas.getByText(/provide your contact details/i)
|
|
199
|
+
).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await step("Test keyboard navigation", async () => {
|
|
203
|
+
const nameInput = canvas.getByLabelText(/name/i);
|
|
204
|
+
await userEvent.tab();
|
|
205
|
+
expect(nameInput).toHaveFocus();
|
|
206
|
+
|
|
207
|
+
const emailInput = canvas.getByLabelText(/email/i);
|
|
208
|
+
await userEvent.tab();
|
|
209
|
+
expect(emailInput).toHaveFocus();
|
|
210
|
+
|
|
211
|
+
const phoneInput = canvas.getByLabelText(/phone/i);
|
|
212
|
+
await userEvent.tab();
|
|
213
|
+
expect(phoneInput).toHaveFocus();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await step("Verify required fields marked", async () => {
|
|
217
|
+
const requiredIndicators = canvas.getAllByLabelText(/required/i);
|
|
218
|
+
expect(requiredIndicators).toHaveLength(2);
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Disabled Fieldset
|
|
224
|
+
export const DisabledFieldset: Story = {
|
|
225
|
+
args: {
|
|
226
|
+
id: "disabled-fieldset",
|
|
227
|
+
legend: "Disabled Fieldset",
|
|
228
|
+
description: "This fieldset and all its controls are disabled",
|
|
229
|
+
disabled: true,
|
|
230
|
+
children: (
|
|
231
|
+
<>
|
|
232
|
+
<label htmlFor="disabled-input">Input</label>
|
|
233
|
+
<input id="disabled-input" type="text" disabled />
|
|
234
|
+
|
|
235
|
+
<label htmlFor="disabled-select">Select</label>
|
|
236
|
+
<select id="disabled-select" disabled>
|
|
237
|
+
<option>Option 1</option>
|
|
238
|
+
<option>Option 2</option>
|
|
239
|
+
</select>
|
|
240
|
+
</>
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
play: async ({ canvasElement, step }) => {
|
|
244
|
+
const canvas = within(canvasElement);
|
|
245
|
+
|
|
246
|
+
await step("Verify fieldset is disabled", async () => {
|
|
247
|
+
expect(canvas.getByRole("group")).toBeDisabled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await step("Verify inputs are disabled", async () => {
|
|
251
|
+
expect(canvas.getByLabelText(/input/i)).toBeDisabled();
|
|
252
|
+
expect(canvas.getByLabelText(/select/i)).toBeDisabled();
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// With Form Controls - Real world example
|
|
258
|
+
export const WithFormControls: Story = {
|
|
259
|
+
args: {
|
|
260
|
+
id: "shipping-address",
|
|
261
|
+
legend: "Shipping Address",
|
|
262
|
+
description: "This address will be used for delivery",
|
|
263
|
+
"data-fieldset": "grouped",
|
|
264
|
+
children: (
|
|
265
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
266
|
+
<div>
|
|
267
|
+
<label htmlFor="street">
|
|
268
|
+
Street Address <span aria-label="required">*</span>
|
|
269
|
+
</label>
|
|
270
|
+
<input
|
|
271
|
+
id="street"
|
|
272
|
+
type="text"
|
|
273
|
+
style={{ width: "100%", padding: "0.5rem" }}
|
|
274
|
+
required
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div style={{ display: "flex", gap: "1rem" }}>
|
|
279
|
+
<div style={{ flex: 1 }}>
|
|
280
|
+
<label htmlFor="city">
|
|
281
|
+
City <span aria-label="required">*</span>
|
|
282
|
+
</label>
|
|
283
|
+
<input
|
|
284
|
+
id="city"
|
|
285
|
+
type="text"
|
|
286
|
+
style={{ width: "100%", padding: "0.5rem" }}
|
|
287
|
+
required
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div style={{ flex: 1 }}>
|
|
292
|
+
<label htmlFor="state">
|
|
293
|
+
State <span aria-label="required">*</span>
|
|
294
|
+
</label>
|
|
295
|
+
<input
|
|
296
|
+
id="state"
|
|
297
|
+
type="text"
|
|
298
|
+
style={{ width: "100%", padding: "0.5rem" }}
|
|
299
|
+
required
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div>
|
|
305
|
+
<label htmlFor="zip">
|
|
306
|
+
ZIP Code <span aria-label="required">*</span>
|
|
307
|
+
</label>
|
|
308
|
+
<input
|
|
309
|
+
id="zip"
|
|
310
|
+
type="text"
|
|
311
|
+
style={{ width: "200px", padding: "0.5rem" }}
|
|
312
|
+
required
|
|
313
|
+
/>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
),
|
|
317
|
+
},
|
|
318
|
+
play: async ({ canvasElement, step }) => {
|
|
319
|
+
const canvas = within(canvasElement);
|
|
320
|
+
|
|
321
|
+
await step("Verify fieldset structure", async () => {
|
|
322
|
+
const fieldset = canvas.getByRole("group");
|
|
323
|
+
expect(fieldset).toBeInTheDocument();
|
|
324
|
+
expect(fieldset).toHaveAttribute("data-fieldset", "grouped");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await step("Verify all form fields are accessible", async () => {
|
|
328
|
+
expect(canvas.getByLabelText(/street address/i)).toBeInTheDocument();
|
|
329
|
+
expect(canvas.getByLabelText(/city/i)).toBeInTheDocument();
|
|
330
|
+
expect(canvas.getByLabelText(/state/i)).toBeInTheDocument();
|
|
331
|
+
expect(canvas.getByLabelText(/zip code/i)).toBeInTheDocument();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await step("Test form navigation", async () => {
|
|
335
|
+
const streetInput = canvas.getByLabelText(/street address/i);
|
|
336
|
+
await userEvent.tab();
|
|
337
|
+
expect(streetInput).toHaveFocus();
|
|
338
|
+
|
|
339
|
+
await userEvent.type(streetInput, "123 Main St");
|
|
340
|
+
expect(streetInput).toHaveValue("123 Main St");
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Multiple Fieldsets - Complex form example
|
|
346
|
+
export const MultipleFieldsets: Story = {
|
|
347
|
+
render: () => (
|
|
348
|
+
<>
|
|
349
|
+
<Fieldset
|
|
350
|
+
id="personal-info"
|
|
351
|
+
legend="Personal Information"
|
|
352
|
+
description="Basic information about you"
|
|
353
|
+
>
|
|
354
|
+
<label htmlFor="multi-name">Name</label>
|
|
355
|
+
<input id="multi-name" type="text" style={{ padding: "0.5rem" }} />
|
|
356
|
+
</Fieldset>
|
|
357
|
+
|
|
358
|
+
<Fieldset
|
|
359
|
+
id="contact-info"
|
|
360
|
+
legend="Contact Information"
|
|
361
|
+
description="How we can reach you"
|
|
362
|
+
data-fieldset="grouped"
|
|
363
|
+
>
|
|
364
|
+
<label htmlFor="multi-email">Email</label>
|
|
365
|
+
<input id="multi-email" type="email" style={{ padding: "0.5rem" }} />
|
|
366
|
+
|
|
367
|
+
<label htmlFor="multi-phone">Phone</label>
|
|
368
|
+
<input id="multi-phone" type="tel" style={{ padding: "0.5rem" }} />
|
|
369
|
+
</Fieldset>
|
|
370
|
+
</>
|
|
371
|
+
),
|
|
372
|
+
play: async ({ canvasElement, step }) => {
|
|
373
|
+
const canvas = within(canvasElement);
|
|
374
|
+
|
|
375
|
+
await step("Verify multiple fieldsets rendered", async () => {
|
|
376
|
+
const fieldsets = canvas.getAllByRole("group");
|
|
377
|
+
expect(fieldsets).toHaveLength(2);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await step("Verify each has aria-describedby", async () => {
|
|
381
|
+
const fieldsets = canvas.getAllByRole("group");
|
|
382
|
+
fieldsets.forEach((fieldset) => {
|
|
383
|
+
expect(fieldset).toHaveAttribute("aria-describedby");
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
@use
|
|
2
|
-
|
|
1
|
+
@use "./header";
|
|
3
2
|
|
|
4
3
|
main,
|
|
5
4
|
footer {
|
|
@@ -49,3 +48,117 @@ footer {
|
|
|
49
48
|
text-align: center;
|
|
50
49
|
}
|
|
51
50
|
}
|
|
51
|
+
|
|
52
|
+
// Fieldset base styles
|
|
53
|
+
// WCAG 2.1 Level AA compliant with verified contrast ratios
|
|
54
|
+
fieldset {
|
|
55
|
+
// Border - Contrast verified: 3:1 minimum against white background
|
|
56
|
+
border: var(--fieldset-border, 1px solid var(--border-default, #999));
|
|
57
|
+
border-radius: var(--fieldset-border-radius, 0.5rem);
|
|
58
|
+
padding: var(--fieldset-padding, 1rem);
|
|
59
|
+
padding-inline: var(--fieldset-padding-inline, 1.5rem);
|
|
60
|
+
padding-block: var(--fieldset-padding-block, 1rem);
|
|
61
|
+
margin-block: var(--fieldset-margin-block, 2rem);
|
|
62
|
+
|
|
63
|
+
// Background - Maintains 4.5:1 contrast with default text
|
|
64
|
+
background: var(--fieldset-bg, transparent);
|
|
65
|
+
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 1rem;
|
|
69
|
+
|
|
70
|
+
> legend {
|
|
71
|
+
// Typography - Font size in rem for user scaling
|
|
72
|
+
font-size: var(--legend-fs, 1rem);
|
|
73
|
+
font-weight: var(--legend-fw, 600);
|
|
74
|
+
padding-inline: var(--legend-padding-inline, 0.5rem);
|
|
75
|
+
color: var(--legend-color, currentColor);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Description text for aria-describedby
|
|
79
|
+
.fieldset-description {
|
|
80
|
+
margin-block-start: 0.5rem;
|
|
81
|
+
margin-block-end: 0;
|
|
82
|
+
font-size: var(--fieldset-description-fs, 0.875rem);
|
|
83
|
+
color: var(--fieldset-description-color, var(--text-subtle, #666));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Focus-within indicator for keyboard navigation
|
|
88
|
+
// WCAG 2.4.7 (Focus Visible)
|
|
89
|
+
fieldset:focus-within {
|
|
90
|
+
outline: var(--fieldset-focus-outline, 2px solid var(--focus-color, #005a9c));
|
|
91
|
+
outline-offset: var(--fieldset-focus-offset, 2px);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Remove outline for mouse users, keep for keyboard
|
|
95
|
+
@media (hover: hover) {
|
|
96
|
+
fieldset:focus-within:not(:focus-visible) {
|
|
97
|
+
outline: none;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Disabled state styling
|
|
102
|
+
// WCAG 1.4.3 (Contrast Minimum), 3.3.2 (Labels or Instructions)
|
|
103
|
+
fieldset:disabled {
|
|
104
|
+
opacity: var(--fieldset-disabled-opacity, 0.6);
|
|
105
|
+
cursor: not-allowed;
|
|
106
|
+
|
|
107
|
+
> legend {
|
|
108
|
+
color: var(--legend-disabled-color, var(--text-disabled, #757575));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Inline legend variant
|
|
113
|
+
fieldset[data-legend="inline"] {
|
|
114
|
+
--fieldset-border: none;
|
|
115
|
+
--fieldset-padding: 0;
|
|
116
|
+
|
|
117
|
+
> legend {
|
|
118
|
+
float: inline-start;
|
|
119
|
+
margin-inline-end: 1rem;
|
|
120
|
+
margin-block-end: 0.5rem;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Grouped variant - Enhanced contrast
|
|
125
|
+
fieldset[data-fieldset="grouped"] {
|
|
126
|
+
// Background - Verified 4.5:1 contrast with text
|
|
127
|
+
--fieldset-bg: var(--surface-subtle, #f5f5f5);
|
|
128
|
+
--fieldset-padding-block: 1.5rem;
|
|
129
|
+
|
|
130
|
+
// Border - Verified 3:1 contrast against background
|
|
131
|
+
--fieldset-border: 2px solid var(--border-focus, #005a9c);
|
|
132
|
+
|
|
133
|
+
> legend {
|
|
134
|
+
--legend-fs: 1.125rem;
|
|
135
|
+
--legend-fw: 700;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// High Contrast Mode Support
|
|
140
|
+
// WCAG 1.4.11 (Non-text Contrast)
|
|
141
|
+
@media (prefers-contrast: high) {
|
|
142
|
+
fieldset {
|
|
143
|
+
border-width: 2px;
|
|
144
|
+
|
|
145
|
+
> legend {
|
|
146
|
+
font-weight: 700;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Windows High Contrast Mode
|
|
152
|
+
@media (forced-colors: active) {
|
|
153
|
+
fieldset {
|
|
154
|
+
border: 1px solid CanvasText;
|
|
155
|
+
|
|
156
|
+
> legend {
|
|
157
|
+
color: CanvasText;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.fieldset-description {
|
|
161
|
+
color: CanvasText;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -4,13 +4,8 @@ import { StoryObj, Meta } from "@storybook/react-vite";
|
|
|
4
4
|
*/
|
|
5
5
|
import { within, expect } from "storybook/test";
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Import jest matchers
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
7
|
import { Header } from "./landmarks";
|
|
12
|
-
|
|
13
|
-
import Img from "#components/images/img";
|
|
8
|
+
import Img from "../images/img";
|
|
14
9
|
|
|
15
10
|
const meta: Meta<typeof Header> = {
|
|
16
11
|
title: "FP.React Components/Layout/Landmarks",
|
|
@@ -61,6 +56,7 @@ const meta: Meta<typeof Header> = {
|
|
|
61
56
|
children: "Default Header",
|
|
62
57
|
"data-testid": "banner",
|
|
63
58
|
},
|
|
59
|
+
tags: ["stable"],
|
|
64
60
|
} as Meta;
|
|
65
61
|
|
|
66
62
|
const headerChildren = () => (
|