@fpkit/acss 0.5.12 → 0.5.13
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/README.md +89 -0
- package/libs/{chunk-DV56L5YX.cjs → chunk-2LTJ7HHX.cjs} +4 -4
- package/libs/{chunk-EQ67LF46.js → chunk-2Y7W75TT.js} +3 -3
- package/libs/{chunk-KKLTUJFB.cjs → chunk-3MKLDCKQ.cjs} +5 -5
- package/libs/chunk-3MKLDCKQ.cjs.map +1 -0
- package/libs/{chunk-X3EVB7VS.cjs → chunk-5S4ORA4C.cjs} +3 -3
- package/libs/{chunk-O6QZBB6G.js → chunk-772NRB75.js} +5 -5
- package/libs/chunk-772NRB75.js.map +1 -0
- package/libs/{chunk-6BVXFW7U.cjs → chunk-AHDJGCG5.cjs} +3 -3
- package/libs/{chunk-E3XP6BEX.cjs → chunk-B7F5FS6D.cjs} +3 -3
- package/libs/chunk-D4YLRWAO.cjs +18 -0
- package/libs/chunk-D4YLRWAO.cjs.map +1 -0
- package/libs/chunk-ETFLFC2S.js +10 -0
- package/libs/chunk-ETFLFC2S.js.map +1 -0
- package/libs/chunk-GZ4QFPRY.js +9 -0
- package/libs/chunk-GZ4QFPRY.js.map +1 -0
- package/libs/{chunk-LHVJKDMA.cjs → chunk-J32EZPYD.cjs} +3 -3
- package/libs/chunk-JJ43O4Y5.js +8 -0
- package/libs/chunk-JJ43O4Y5.js.map +1 -0
- package/libs/chunk-KUKIVRC2.js +7 -0
- package/libs/chunk-KUKIVRC2.js.map +1 -0
- package/libs/chunk-L75OQKEI.cjs +13 -0
- package/libs/chunk-L75OQKEI.cjs.map +1 -0
- package/libs/{chunk-LL7HTLMS.cjs → chunk-M5RRNTVX.cjs} +3 -3
- package/libs/{chunk-LIQJ7ZZR.js → chunk-NGTJDDFO.js} +2 -2
- package/libs/chunk-OK5QEIMD.cjs +17 -0
- package/libs/chunk-OK5QEIMD.cjs.map +1 -0
- package/libs/chunk-P2DC76ZZ.cjs +18 -0
- package/libs/chunk-P2DC76ZZ.cjs.map +1 -0
- package/libs/chunk-PQ2K3BM6.cjs +17 -0
- package/libs/chunk-PQ2K3BM6.cjs.map +1 -0
- package/libs/{chunk-QCMV4VQZ.js → chunk-QLZWHAMK.js} +2 -2
- package/libs/{chunk-BIP2NY53.js → chunk-RIVUMPOG.js} +2 -2
- package/libs/{chunk-ICCKQ2GC.cjs → chunk-ROZI23GS.cjs} +4 -4
- package/libs/{chunk-NHYXGV3L.js → chunk-SMYRLO3E.js} +2 -2
- package/libs/{chunk-5ZM4XL44.js → chunk-TYRCEX2L.js} +2 -2
- package/libs/chunk-VUH3FXGJ.js +11 -0
- package/libs/chunk-VUH3FXGJ.js.map +1 -0
- package/libs/{chunk-PPOOBUOS.js → chunk-XBA562WW.js} +2 -2
- package/libs/{chunk-QVV34QEH.cjs → chunk-XTQKWY7W.cjs} +3 -3
- package/libs/{chunk-YWOYVRFT.js → chunk-ZANSFMTD.js} +3 -3
- 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/badge/badge.css +1 -1
- package/libs/components/badge/badge.css.map +1 -1
- package/libs/components/badge/badge.min.css +2 -2
- package/libs/components/breadcrumbs/breadcrumb.cjs +9 -5
- package/libs/components/breadcrumbs/breadcrumb.d.cts +271 -32
- package/libs/components/breadcrumbs/breadcrumb.d.ts +271 -32
- package/libs/components/breadcrumbs/breadcrumb.js +3 -3
- package/libs/components/button.cjs +4 -4
- package/libs/components/button.d.cts +2 -2
- package/libs/components/button.d.ts +2 -2
- package/libs/components/button.js +2 -2
- package/libs/components/buttons/button.css +1 -1
- package/libs/components/buttons/button.css.map +1 -1
- package/libs/components/buttons/button.min.css +2 -2
- package/libs/components/card.cjs +7 -7
- package/libs/components/card.d.cts +277 -33
- package/libs/components/card.d.ts +277 -33
- package/libs/components/card.js +2 -2
- package/libs/components/cards/card.css +1 -1
- package/libs/components/cards/card.css.map +1 -1
- package/libs/components/cards/card.min.css +2 -2
- package/libs/components/details/details.css +1 -1
- package/libs/components/details/details.css.map +1 -1
- package/libs/components/details/details.min.css +2 -2
- package/libs/components/dialog/dialog.cjs +7 -7
- package/libs/components/dialog/dialog.css +1 -1
- package/libs/components/dialog/dialog.css.map +1 -1
- package/libs/components/dialog/dialog.d.cts +88 -34
- package/libs/components/dialog/dialog.d.ts +88 -34
- package/libs/components/dialog/dialog.js +5 -5
- package/libs/components/dialog/dialog.min.css +2 -2
- package/libs/components/form/fields.cjs +4 -4
- package/libs/components/form/fields.d.cts +2 -2
- package/libs/components/form/fields.d.ts +2 -2
- package/libs/components/form/fields.js +2 -2
- package/libs/components/form/textarea.cjs +4 -4
- package/libs/components/form/textarea.d.cts +2 -2
- package/libs/components/form/textarea.d.ts +2 -2
- package/libs/components/form/textarea.js +2 -2
- package/libs/components/heading/heading.cjs +3 -3
- package/libs/components/heading/heading.d.cts +3 -14
- package/libs/components/heading/heading.d.ts +3 -14
- package/libs/components/heading/heading.js +2 -2
- package/libs/components/icons/icon.cjs +4 -4
- package/libs/components/icons/icon.d.cts +148 -4
- package/libs/components/icons/icon.d.ts +148 -4
- package/libs/components/icons/icon.js +2 -2
- package/libs/components/images/img.css +1 -1
- package/libs/components/images/img.css.map +1 -1
- package/libs/components/images/img.min.css +2 -2
- package/libs/components/link/link.cjs +4 -4
- package/libs/components/link/link.d.cts +2 -2
- package/libs/components/link/link.d.ts +2 -2
- package/libs/components/link/link.js +2 -2
- package/libs/components/list/list.cjs +5 -5
- package/libs/components/list/list.d.cts +3 -3
- package/libs/components/list/list.d.ts +3 -3
- package/libs/components/list/list.js +2 -2
- package/libs/components/modal.cjs +4 -4
- package/libs/components/modal.js +3 -3
- package/libs/components/nav/nav.cjs +7 -7
- package/libs/components/nav/nav.d.cts +2 -2
- package/libs/components/nav/nav.d.ts +2 -2
- package/libs/components/nav/nav.js +3 -3
- package/libs/components/text/text.cjs +5 -5
- package/libs/components/text/text.d.cts +2 -2
- package/libs/components/text/text.d.ts +2 -2
- package/libs/components/text/text.js +2 -2
- package/libs/heading-3648c538.d.ts +250 -0
- package/libs/hooks.cjs +7 -0
- package/libs/hooks.d.cts +5 -0
- package/libs/hooks.d.ts +5 -0
- package/libs/hooks.js +3 -0
- package/libs/icons.cjs +3 -3
- package/libs/icons.d.cts +1 -1
- package/libs/icons.d.ts +1 -1
- package/libs/icons.js +2 -2
- package/libs/index.cjs +112 -91
- 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 +515 -31
- package/libs/index.d.ts +515 -31
- package/libs/index.js +31 -19
- package/libs/index.js.map +1 -1
- package/libs/ui-645f95b5.d.ts +285 -0
- package/package.json +2 -83
- package/src/components/README-UI.mdx +416 -0
- package/src/components/alert/ACCESSIBILITY.md +319 -0
- package/src/components/alert/README.mdx +475 -19
- package/src/components/alert/alert.scss +113 -6
- package/src/components/alert/alert.stories.tsx +372 -0
- package/src/components/alert/alert.test.tsx +762 -0
- package/src/components/alert/alert.tsx +331 -66
- package/src/components/alert/views/alert-actions.tsx +13 -0
- package/src/components/alert/views/alert-content.tsx +17 -0
- package/src/components/alert/views/alert-icon.tsx +53 -0
- package/src/components/alert/views/alert-screen-reader-text.tsx +30 -0
- package/src/components/alert/views/alert-title.tsx +23 -0
- package/src/components/alert/views/alert-view.tsx +158 -0
- package/src/components/alert/views/index.ts +12 -0
- package/src/components/badge/badge.mdx +186 -49
- package/src/components/badge/badge.scss +20 -2
- package/src/components/badge/badge.stories.tsx +160 -14
- package/src/components/badge/badge.test.tsx +179 -0
- package/src/components/badge/badge.tsx +97 -4
- package/src/components/breadcrumbs/README.mdx +364 -45
- package/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap +152 -0
- package/src/components/breadcrumbs/breadcrumb.stories.tsx +7 -3
- package/src/components/breadcrumbs/breadcrumb.test.tsx +490 -0
- package/src/components/breadcrumbs/breadcrumb.tsx +427 -170
- package/src/components/buttons/button.scss +34 -31
- package/src/components/buttons/button.stories.tsx +35 -0
- package/src/components/cards/README.mdx +657 -0
- package/src/components/cards/card.scss +22 -0
- package/src/components/cards/card.stories.tsx +167 -5
- package/src/components/cards/card.test.tsx +360 -20
- package/src/components/cards/card.tsx +200 -79
- package/src/components/cards/card.types.ts +135 -0
- package/src/components/cards/card.utils.ts +79 -0
- package/src/components/details/ACCESSIBILITY-REVIEW-LIVE.md +1050 -0
- package/src/components/details/ACCESSIBILITY-REVIEW.md +502 -0
- package/src/components/details/README.mdx +437 -69
- package/src/components/details/details.scss +16 -7
- package/src/components/details/details.test.tsx +385 -0
- package/src/components/details/details.tsx +101 -69
- package/src/components/details/details.types.ts +76 -0
- package/src/components/dialog/README.mdx +513 -110
- package/src/components/dialog/dialog-modal.tsx +79 -56
- package/src/components/dialog/dialog.scss +53 -3
- package/src/components/dialog/dialog.stories.tsx +10 -7
- package/src/components/dialog/dialog.test.tsx +450 -0
- package/src/components/dialog/dialog.tsx +69 -59
- package/src/components/dialog/dialog.types.ts +133 -0
- package/src/components/dialog/views/dialog-footer.tsx +54 -11
- package/src/components/dialog/views/dialog-header.tsx +20 -15
- package/src/components/heading/heading.stories.tsx +44 -4
- package/src/components/heading/heading.tsx +89 -23
- package/src/components/icons/README.mdx +332 -0
- package/src/components/icons/icon.stories.tsx +74 -1
- package/src/components/icons/icon.tsx +89 -1
- package/src/components/icons/types.ts +47 -0
- package/src/components/images/README.mdx +340 -24
- package/src/components/images/img.scss +19 -3
- package/src/components/images/img.stories.tsx +424 -15
- package/src/components/images/img.test.tsx +354 -25
- package/src/components/images/img.tsx +186 -63
- package/src/components/images/img.types.ts +211 -0
- package/src/components/title/MIGRATION.md +199 -0
- package/src/components/title/README.md +326 -0
- package/src/components/title/README.mdx +452 -0
- package/src/components/title/title.stories.tsx +393 -0
- package/src/components/title/title.test.tsx +251 -0
- package/src/components/title/title.tsx +219 -0
- package/src/components/ui.stories.tsx +894 -0
- package/src/components/ui.test.tsx +559 -0
- package/src/components/ui.tsx +266 -15
- package/src/components/word-count/README.md +240 -0
- package/src/hooks.ts +1 -0
- package/src/index.ts +10 -2
- package/src/sass/_properties.scss +1 -0
- package/src/styles/alert/alert.css +94 -4
- package/src/styles/alert/alert.css.map +1 -1
- package/src/styles/badge/badge.css +20 -2
- package/src/styles/badge/badge.css.map +1 -1
- package/src/styles/buttons/button.css +31 -31
- package/src/styles/buttons/button.css.map +1 -1
- package/src/styles/cards/card.css +16 -0
- package/src/styles/cards/card.css.map +1 -1
- package/src/styles/details/details.css +19 -8
- package/src/styles/details/details.css.map +1 -1
- package/src/styles/dialog/dialog.css +43 -2
- package/src/styles/dialog/dialog.css.map +1 -1
- package/src/styles/images/img.css +15 -3
- package/src/styles/images/img.css.map +1 -1
- package/src/styles/index.css +240 -51
- package/src/styles/index.css.map +1 -1
- package/src/test/setup.d.ts +9 -0
- package/src/test/setup.ts +53 -1
- package/libs/chunk-6TE5QEVE.cjs +0 -13
- package/libs/chunk-6TE5QEVE.cjs.map +0 -1
- package/libs/chunk-7K76RW2A.cjs +0 -18
- package/libs/chunk-7K76RW2A.cjs.map +0 -1
- package/libs/chunk-BSPKFLO4.js +0 -8
- package/libs/chunk-BSPKFLO4.js.map +0 -1
- package/libs/chunk-BV5CLH44.cjs +0 -18
- package/libs/chunk-BV5CLH44.cjs.map +0 -1
- package/libs/chunk-DKGJHKGW.js +0 -9
- package/libs/chunk-DKGJHKGW.js.map +0 -1
- package/libs/chunk-ECLD37WN.cjs +0 -16
- package/libs/chunk-ECLD37WN.cjs.map +0 -1
- package/libs/chunk-HYBZBN4G.js +0 -8
- package/libs/chunk-HYBZBN4G.js.map +0 -1
- package/libs/chunk-KKLTUJFB.cjs.map +0 -1
- package/libs/chunk-M5QL5TAE.cjs +0 -14
- package/libs/chunk-M5QL5TAE.cjs.map +0 -1
- package/libs/chunk-NE6YXTMC.js +0 -7
- package/libs/chunk-NE6YXTMC.js.map +0 -1
- package/libs/chunk-O6QZBB6G.js.map +0 -1
- package/libs/chunk-SXVZSWX6.js +0 -11
- package/libs/chunk-SXVZSWX6.js.map +0 -1
- package/libs/ui-9a6f9f8d.d.ts +0 -24
- package/src/components/cards/README.md +0 -80
- package/src/components/dialog/hooks/useClickOutside.ts +0 -33
- /package/libs/{chunk-DV56L5YX.cjs.map → chunk-2LTJ7HHX.cjs.map} +0 -0
- /package/libs/{chunk-EQ67LF46.js.map → chunk-2Y7W75TT.js.map} +0 -0
- /package/libs/{chunk-X3EVB7VS.cjs.map → chunk-5S4ORA4C.cjs.map} +0 -0
- /package/libs/{chunk-6BVXFW7U.cjs.map → chunk-AHDJGCG5.cjs.map} +0 -0
- /package/libs/{chunk-E3XP6BEX.cjs.map → chunk-B7F5FS6D.cjs.map} +0 -0
- /package/libs/{chunk-LHVJKDMA.cjs.map → chunk-J32EZPYD.cjs.map} +0 -0
- /package/libs/{chunk-LL7HTLMS.cjs.map → chunk-M5RRNTVX.cjs.map} +0 -0
- /package/libs/{chunk-LIQJ7ZZR.js.map → chunk-NGTJDDFO.js.map} +0 -0
- /package/libs/{chunk-QCMV4VQZ.js.map → chunk-QLZWHAMK.js.map} +0 -0
- /package/libs/{chunk-BIP2NY53.js.map → chunk-RIVUMPOG.js.map} +0 -0
- /package/libs/{chunk-ICCKQ2GC.cjs.map → chunk-ROZI23GS.cjs.map} +0 -0
- /package/libs/{chunk-NHYXGV3L.js.map → chunk-SMYRLO3E.js.map} +0 -0
- /package/libs/{chunk-5ZM4XL44.js.map → chunk-TYRCEX2L.js.map} +0 -0
- /package/libs/{chunk-PPOOBUOS.js.map → chunk-XBA562WW.js.map} +0 -0
- /package/libs/{chunk-QVV34QEH.cjs.map → chunk-XTQKWY7W.cjs.map} +0 -0
- /package/libs/{chunk-YWOYVRFT.js.map → chunk-ZANSFMTD.js.map} +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="FP.REACT Components/Icons/Readme" />
|
|
4
|
+
|
|
5
|
+
# Icon Component
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
The `Icon` component is an accessibility-first wrapper for SVG icons that enforces WCAG 2.1 Level AA compliance by hiding decorative icons from screen readers by default. It provides a flexible, type-safe API for both decorative and semantic icon usage patterns.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Accessibility by Default** - Icons are hidden from screen readers (`aria-hidden="true"`) by default
|
|
14
|
+
- **Decorative vs. Semantic Patterns** - Clear distinction between decorative and meaningful icons
|
|
15
|
+
- **Type-Safe Props** - Full TypeScript support with comprehensive prop definitions
|
|
16
|
+
- **Rich Icon Library** - 30+ pre-built SVG icons (arrows, actions, alerts, media controls)
|
|
17
|
+
- **Customizable Styling** - Control size, color, stroke, and fill properties
|
|
18
|
+
- **Compound Component Pattern** - Access icons via `Icon.Code`, `Icon.Home`, etc.
|
|
19
|
+
- **WCAG 2.1 AA Compliant** - Follows accessibility best practices out of the box
|
|
20
|
+
- **Backward Compatible** - Deprecated `alt` prop maintained for legacy code
|
|
21
|
+
|
|
22
|
+
## Key Accessibility Principle
|
|
23
|
+
|
|
24
|
+
> **Decorative images must be hidden from assistive technology** (WCAG 1.1.1)
|
|
25
|
+
|
|
26
|
+
Most icons in user interfaces are **decorative** — they accompany visible text labels (e.g., a save icon next to "Save Changes"). Screen readers should not announce these redundantly.
|
|
27
|
+
|
|
28
|
+
When icons are **semantic** (standalone without text), developers must explicitly:
|
|
29
|
+
1. Set `aria-hidden={false}` to expose the icon
|
|
30
|
+
2. Provide `aria-label` for an accessible name
|
|
31
|
+
|
|
32
|
+
## Props
|
|
33
|
+
|
|
34
|
+
### Icon Wrapper Props
|
|
35
|
+
|
|
36
|
+
| Name | Type | Default | Description |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| `aria-hidden` | `boolean` | `true` | Controls screen reader visibility. `true` = decorative, `false` = semantic |
|
|
39
|
+
| `aria-label` | `string` | - | Accessible label (required for standalone semantic icons) |
|
|
40
|
+
| `role` | `string` | - | ARIA role override (use `"img"` for complex semantic SVGs) |
|
|
41
|
+
| `styles` | `React.CSSProperties` | - | Inline styles for the wrapper span |
|
|
42
|
+
| `classes` | `string` | - | CSS class names |
|
|
43
|
+
| `children` | `React.ReactNode` | - | Icon SVG component(s) to render |
|
|
44
|
+
|
|
45
|
+
### Individual Icon Props
|
|
46
|
+
|
|
47
|
+
Props for individual icon components (e.g., `Icon.Code`, `Icon.Home`):
|
|
48
|
+
|
|
49
|
+
| Name | Type | Default | Description |
|
|
50
|
+
| --- | --- | --- | --- |
|
|
51
|
+
| `size` | `number` | `16` | Icon size in pixels (sets both width and height) |
|
|
52
|
+
| `fill` | `string` | `'none'` | SVG fill color (CSS color value) |
|
|
53
|
+
| `strokeColor` | `string` | `'currentColor'` | SVG stroke color (CSS color value) |
|
|
54
|
+
| `strokeWidth` | `string` | `'2'` | SVG stroke width |
|
|
55
|
+
| `width` | `number` | - | Explicit width in pixels (overrides size) |
|
|
56
|
+
| `height` | `number` | - | Explicit height in pixels (overrides size) |
|
|
57
|
+
| `alt` | `string` | - | **@deprecated** Use `aria-label` instead |
|
|
58
|
+
|
|
59
|
+
## Available Icons
|
|
60
|
+
|
|
61
|
+
### Action Icons
|
|
62
|
+
- `Icon.Add`, `Icon.Minus`, `Icon.Remove` (Close), `Icon.Copy`, `Icon.Star`
|
|
63
|
+
|
|
64
|
+
### Navigation Icons
|
|
65
|
+
- `Icon.ArrowUp`, `Icon.ArrowDown`, `Icon.ArrowLeft`, `Icon.ArrowRight`
|
|
66
|
+
- `Icon.Up`, `Icon.Down`, `Icon.Left`, `Icon.Right`
|
|
67
|
+
- `Icon.Home`
|
|
68
|
+
|
|
69
|
+
### Media Control Icons
|
|
70
|
+
- `Icon.Play`, `Icon.Pause`, `Icon.Stop`, `Icon.Resume`
|
|
71
|
+
- `Icon.PlaySolid`, `Icon.PauseSolid`, `Icon.StopSolid`, `Icon.ResumeSolid`
|
|
72
|
+
|
|
73
|
+
### Status/Alert Icons
|
|
74
|
+
- `Icon.Info`, `Icon.InfoSolid`
|
|
75
|
+
- `Icon.AlertSolid`, `Icon.AlertSquareSolid`
|
|
76
|
+
- `Icon.WarnSolid`, `Icon.SuccessSolid`, `Icon.QuestionSolid`
|
|
77
|
+
|
|
78
|
+
### Other Icons
|
|
79
|
+
- `Icon.Code`, `Icon.Chat`, `Icon.User`
|
|
80
|
+
|
|
81
|
+
### Aliases
|
|
82
|
+
- `Icon.Close` → `Icon.Remove`
|
|
83
|
+
|
|
84
|
+
## Usage Examples
|
|
85
|
+
|
|
86
|
+
### ✅ Decorative Icon with Text (Default, Recommended)
|
|
87
|
+
|
|
88
|
+
When icons accompany visible text, they are decorative and should be hidden from screen readers:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { Button } from "@fpkit/acss";
|
|
92
|
+
import { Icon } from "@fpkit/acss";
|
|
93
|
+
|
|
94
|
+
// Screen reader announces: "Save Changes, button"
|
|
95
|
+
const SaveButton = () => (
|
|
96
|
+
<Button type="button" onClick={handleSave}>
|
|
97
|
+
<Icon>
|
|
98
|
+
<Icon.Code />
|
|
99
|
+
</Icon>
|
|
100
|
+
Save Changes
|
|
101
|
+
</Button>
|
|
102
|
+
);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Why this works:**
|
|
106
|
+
- Icon is decorative (accompanies "Save Changes" text)
|
|
107
|
+
- `aria-hidden="true"` is applied by default
|
|
108
|
+
- Screen reader users hear only "Save Changes, button" (no redundancy)
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### ✅ Semantic Icon-Only Button (Explicit Pattern)
|
|
113
|
+
|
|
114
|
+
When icons stand alone without visible text, they must be exposed to screen readers:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { Button } from "@fpkit/acss";
|
|
118
|
+
import { Icon } from "@fpkit/acss";
|
|
119
|
+
|
|
120
|
+
// Screen reader announces: "Close dialog, button"
|
|
121
|
+
const CloseButton = () => (
|
|
122
|
+
<Button type="button" aria-label="Close dialog" onClick={handleClose}>
|
|
123
|
+
<Icon aria-hidden={false}>
|
|
124
|
+
<Icon.Remove />
|
|
125
|
+
</Icon>
|
|
126
|
+
</Button>
|
|
127
|
+
);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Why this works:**
|
|
131
|
+
- Icon is semantic (no visible text)
|
|
132
|
+
- `aria-hidden={false}` exposes icon to screen readers
|
|
133
|
+
- `aria-label` on the button provides accessible name
|
|
134
|
+
- Meets WCAG 4.1.2 (Name, Role, Value)
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### ✅ Semantic Icon with role="img"
|
|
139
|
+
|
|
140
|
+
For complex SVG icons that convey meaning, use `role="img"`:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { Icon } from "@fpkit/acss";
|
|
144
|
+
|
|
145
|
+
// Screen reader announces: "Code snippet, image"
|
|
146
|
+
const CodeIcon = () => (
|
|
147
|
+
<Icon
|
|
148
|
+
aria-hidden={false}
|
|
149
|
+
aria-label="Code snippet"
|
|
150
|
+
role="img"
|
|
151
|
+
>
|
|
152
|
+
<Icon.Code size={32} />
|
|
153
|
+
</Icon>
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Why this works:**
|
|
158
|
+
- `role="img"` tells screen readers this is a meaningful image
|
|
159
|
+
- `aria-label` provides the image description
|
|
160
|
+
- Follows WCAG 1.1.1 (Non-text Content)
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### ✅ Custom Styling
|
|
165
|
+
|
|
166
|
+
Icons inherit text color by default (`strokeColor="currentColor"`):
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { Icon } from "@fpkit/acss";
|
|
170
|
+
|
|
171
|
+
const StyledIcon = () => (
|
|
172
|
+
<div style={{ color: "blue" }}>
|
|
173
|
+
<Icon>
|
|
174
|
+
<Icon.Star size={24} fill="gold" strokeColor="orange" />
|
|
175
|
+
</Icon>
|
|
176
|
+
Favorite
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### ❌ Anti-Pattern: Icon-Only Button Without Label
|
|
184
|
+
|
|
185
|
+
**This violates WCAG 4.1.2 (Name, Role, Value):**
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// ❌ BAD: Screen reader cannot identify button purpose
|
|
189
|
+
const BadButton = () => (
|
|
190
|
+
<Button type="button" onClick={handleClose}>
|
|
191
|
+
<Icon>
|
|
192
|
+
<Icon.Remove />
|
|
193
|
+
</Icon>
|
|
194
|
+
</Button>
|
|
195
|
+
);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Why this fails:**
|
|
199
|
+
- Icon-only button with no accessible name
|
|
200
|
+
- Screen reader announces only "button" (no context)
|
|
201
|
+
- Users cannot determine what the button does
|
|
202
|
+
|
|
203
|
+
**Fix:**
|
|
204
|
+
```tsx
|
|
205
|
+
// ✅ GOOD: Add aria-label to button
|
|
206
|
+
<Button type="button" aria-label="Close" onClick={handleClose}>
|
|
207
|
+
<Icon aria-hidden={false}>
|
|
208
|
+
<Icon.Remove />
|
|
209
|
+
</Icon>
|
|
210
|
+
</Button>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Technical Details
|
|
216
|
+
|
|
217
|
+
### Component Architecture
|
|
218
|
+
|
|
219
|
+
The Icon component uses a **compound component pattern**:
|
|
220
|
+
- `Icon` - Wrapper component handling accessibility
|
|
221
|
+
- `Icon.Code`, `Icon.Home`, etc. - Individual SVG icon components
|
|
222
|
+
- `Svg` - Base SVG component used by all icons
|
|
223
|
+
|
|
224
|
+
### Polymorphic UI Component
|
|
225
|
+
|
|
226
|
+
The Icon wrapper uses the polymorphic `UI` component from `#components/ui`, which:
|
|
227
|
+
- Supports ref forwarding for focus management
|
|
228
|
+
- Accepts all native HTML span attributes
|
|
229
|
+
- Merges default and custom styles
|
|
230
|
+
|
|
231
|
+
### Type Safety
|
|
232
|
+
|
|
233
|
+
The component is fully typed with TypeScript:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
export type IconProps = React.ComponentProps<typeof UI> & {
|
|
237
|
+
"aria-hidden"?: boolean;
|
|
238
|
+
"aria-label"?: string;
|
|
239
|
+
role?: string;
|
|
240
|
+
};
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Individual icon props extend `IconProps` from `types.ts` with visual styling properties.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Accessibility Checklist
|
|
248
|
+
|
|
249
|
+
When using icons, ensure:
|
|
250
|
+
|
|
251
|
+
- [ ] **Decorative icons** (with text) use default `aria-hidden="true"`
|
|
252
|
+
- [ ] **Semantic icons** (standalone) have `aria-hidden={false}`
|
|
253
|
+
- [ ] **Semantic icons** have `aria-label` or `aria-labelledby`
|
|
254
|
+
- [ ] **Icon-only buttons** have `aria-label` on the button element
|
|
255
|
+
- [ ] **Icon color contrast** meets WCAG 1.4.11 (3:1 for UI components)
|
|
256
|
+
- [ ] **Touch targets** are at least 44×44 CSS pixels (WCAG 2.5.5)
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Migration Guide
|
|
261
|
+
|
|
262
|
+
### Deprecation: `alt` Prop
|
|
263
|
+
|
|
264
|
+
The `alt` prop is deprecated because it's only valid for `<img>` elements, not SVG.
|
|
265
|
+
|
|
266
|
+
**Before:**
|
|
267
|
+
```tsx
|
|
268
|
+
<Icon.Code alt="Code icon" />
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**After:**
|
|
272
|
+
```tsx
|
|
273
|
+
<Icon aria-hidden={false} aria-label="Code icon">
|
|
274
|
+
<Icon.Code />
|
|
275
|
+
</Icon>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Adding aria-hidden to Existing Icons
|
|
279
|
+
|
|
280
|
+
If you have existing icon usage without explicit accessibility attributes:
|
|
281
|
+
|
|
282
|
+
**Before:**
|
|
283
|
+
```tsx
|
|
284
|
+
<button>
|
|
285
|
+
<Icon><Icon.Remove /></Icon>
|
|
286
|
+
Close
|
|
287
|
+
</button>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**After (no change needed):**
|
|
291
|
+
```tsx
|
|
292
|
+
// Icons are now hidden by default - existing code is more accessible!
|
|
293
|
+
<button>
|
|
294
|
+
<Icon><Icon.Remove /></Icon>
|
|
295
|
+
Close
|
|
296
|
+
</button>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
For icon-only buttons, update to:
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
<button aria-label="Close">
|
|
303
|
+
<Icon aria-hidden={false}>
|
|
304
|
+
<Icon.Remove />
|
|
305
|
+
</Icon>
|
|
306
|
+
</button>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Additional Notes
|
|
312
|
+
|
|
313
|
+
- **Color inheritance**: Icons use `strokeColor="currentColor"` by default, inheriting parent text color
|
|
314
|
+
- **Size units**: Icon sizes are in pixels (e.g., `size={24}` = 24px)
|
|
315
|
+
- **Performance**: SVG icons are lightweight and render efficiently
|
|
316
|
+
- **Bundle size**: Icons are tree-shakeable when using named imports
|
|
317
|
+
- **Browser support**: Works in all modern browsers supporting SVG
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Related Components
|
|
322
|
+
|
|
323
|
+
- [Button](/docs/fp-react-components-buttons-readme--docs) - Learn about accessible button patterns
|
|
324
|
+
- [Alert](/docs/fp-react-components-alert-readme--docs) - Uses icons for severity indicators
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Further Reading
|
|
329
|
+
|
|
330
|
+
- [WCAG 1.1.1: Non-text Content](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html)
|
|
331
|
+
- [WCAG 4.1.2: Name, Role, Value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
|
|
332
|
+
- [Accessible Icon Buttons (Sara Soueidan)](https://www.sarasoueidan.com/blog/accessible-icon-buttons/)
|
|
@@ -53,9 +53,14 @@ export const IconButton = {
|
|
|
53
53
|
},
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Decorative icon (default behavior).
|
|
58
|
+
* Icon is hidden from screen readers via aria-hidden="true".
|
|
59
|
+
* Use when icon accompanies visible text.
|
|
60
|
+
*/
|
|
56
61
|
export const Code: Story = {
|
|
57
62
|
args: {
|
|
58
|
-
children: <Icon.Code
|
|
63
|
+
children: <Icon.Code />,
|
|
59
64
|
},
|
|
60
65
|
} as Story;
|
|
61
66
|
|
|
@@ -249,3 +254,71 @@ export const AlertSquareSolid: Story = {
|
|
|
249
254
|
children: <Icon.AlertSquareSolid />,
|
|
250
255
|
},
|
|
251
256
|
} as Story;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* ✅ ACCESSIBLE PATTERN: Decorative icon with visible text.
|
|
260
|
+
* Icon is hidden from screen readers (default aria-hidden="true").
|
|
261
|
+
* Screen readers announce only "Save Changes".
|
|
262
|
+
*/
|
|
263
|
+
export const DecorativeIconWithText: Story = {
|
|
264
|
+
args: {},
|
|
265
|
+
render: () => (
|
|
266
|
+
<Button type="button">
|
|
267
|
+
<Icon>
|
|
268
|
+
<Icon.Code />
|
|
269
|
+
</Icon>
|
|
270
|
+
Save Changes
|
|
271
|
+
</Button>
|
|
272
|
+
),
|
|
273
|
+
} as Story;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* ✅ ACCESSIBLE PATTERN: Semantic icon-only button.
|
|
277
|
+
* Icon is exposed to screen readers with aria-hidden={false}.
|
|
278
|
+
* aria-label provides accessible name for screen readers.
|
|
279
|
+
*/
|
|
280
|
+
export const SemanticIconOnlyButton: Story = {
|
|
281
|
+
args: {},
|
|
282
|
+
render: () => (
|
|
283
|
+
<Button type="button" aria-label="Close dialog">
|
|
284
|
+
<Icon aria-hidden={false}>
|
|
285
|
+
<Icon.Remove />
|
|
286
|
+
</Icon>
|
|
287
|
+
</Button>
|
|
288
|
+
),
|
|
289
|
+
} as Story;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* ✅ ACCESSIBLE PATTERN: Icon with role="img" for semantic meaning.
|
|
293
|
+
* Use when icon conveys information beyond decoration.
|
|
294
|
+
* Requires aria-label for accessible name.
|
|
295
|
+
*/
|
|
296
|
+
export const SemanticIconWithRole: Story = {
|
|
297
|
+
args: {
|
|
298
|
+
"aria-hidden": false,
|
|
299
|
+
"aria-label": "Code snippet",
|
|
300
|
+
role: "img",
|
|
301
|
+
children: <Icon.Code />,
|
|
302
|
+
},
|
|
303
|
+
} as Story;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* ❌ ANTI-PATTERN: Icon-only button without accessible name.
|
|
307
|
+
* This will fail WCAG 4.1.2 (Name, Role, Value).
|
|
308
|
+
* Screen readers cannot identify the button's purpose.
|
|
309
|
+
*/
|
|
310
|
+
export const IconOnlyButtonNoLabel: Story = {
|
|
311
|
+
args: {},
|
|
312
|
+
render: () => (
|
|
313
|
+
<div style={{ opacity: 0.5, border: "2px dashed red", padding: "1rem" }}>
|
|
314
|
+
<p style={{ color: "red", marginBottom: "0.5rem" }}>
|
|
315
|
+
❌ BAD: No accessible name
|
|
316
|
+
</p>
|
|
317
|
+
<Button type="button">
|
|
318
|
+
<Icon>
|
|
319
|
+
<Icon.Remove />
|
|
320
|
+
</Icon>
|
|
321
|
+
</Button>
|
|
322
|
+
</div>
|
|
323
|
+
),
|
|
324
|
+
} as Story;
|
|
@@ -53,13 +53,98 @@ const defaultStyles = {
|
|
|
53
53
|
width: "auto",
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
export type IconProps = React.ComponentProps<typeof UI
|
|
56
|
+
export type IconProps = React.ComponentProps<typeof UI> & {
|
|
57
|
+
/**
|
|
58
|
+
* Controls screen reader visibility.
|
|
59
|
+
* - `true` (default): Hides icon from screen readers (decorative icon)
|
|
60
|
+
* - `false`: Makes icon visible to screen readers (semantic icon)
|
|
61
|
+
*
|
|
62
|
+
* **When to use decorative (aria-hidden="true", default):**
|
|
63
|
+
* - Icon accompanies visible text (e.g., button with icon + label)
|
|
64
|
+
* - Icon is purely visual decoration
|
|
65
|
+
*
|
|
66
|
+
* **When to use semantic (aria-hidden="false"):**
|
|
67
|
+
* - Icon is the only content (e.g., icon-only button)
|
|
68
|
+
* - Must provide `aria-label` or `aria-labelledby` for accessible name
|
|
69
|
+
*
|
|
70
|
+
* @default true
|
|
71
|
+
* @see https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html
|
|
72
|
+
*/
|
|
73
|
+
"aria-hidden"?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Accessible label for semantic icons.
|
|
76
|
+
* Required when icon is standalone (not accompanied by visible text).
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* // ✅ GOOD: Icon-only button with accessible name
|
|
81
|
+
* <button>
|
|
82
|
+
* <Icon aria-hidden={false} aria-label="Close dialog">
|
|
83
|
+
* <Icon.Remove />
|
|
84
|
+
* </Icon>
|
|
85
|
+
* </button>
|
|
86
|
+
*
|
|
87
|
+
* // ✅ GOOD: Icon with visible text (default decorative)
|
|
88
|
+
* <button>
|
|
89
|
+
* <Icon><Icon.Add /></Icon>
|
|
90
|
+
* Add Item
|
|
91
|
+
* </button>
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
"aria-label"?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Semantic role override.
|
|
97
|
+
* Use `role="img"` when icon conveys meaning and is not decorative.
|
|
98
|
+
*
|
|
99
|
+
* @default undefined (no role for decorative icons)
|
|
100
|
+
*/
|
|
101
|
+
role?: string;
|
|
102
|
+
};
|
|
57
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Icon wrapper component that enforces accessibility best practices.
|
|
106
|
+
*
|
|
107
|
+
* **Default behavior (decorative icon):**
|
|
108
|
+
* - Hidden from screen readers (`aria-hidden="true"`)
|
|
109
|
+
* - Used when icon accompanies visible text
|
|
110
|
+
*
|
|
111
|
+
* **Semantic icon pattern:**
|
|
112
|
+
* - Set `aria-hidden={false}` to expose to screen readers
|
|
113
|
+
* - Must provide `aria-label` for accessible name
|
|
114
|
+
* - Consider `role="img"` for complex SVG icons
|
|
115
|
+
*
|
|
116
|
+
* @param {IconProps} props - Component props
|
|
117
|
+
* @returns {React.ReactElement} Accessible icon wrapper
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* // ✅ Decorative icon with text (default)
|
|
121
|
+
* <button>
|
|
122
|
+
* <Icon><Icon.Save /></Icon>
|
|
123
|
+
* Save Changes
|
|
124
|
+
* </button>
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* // ✅ Semantic icon-only button
|
|
128
|
+
* <button aria-label="Close dialog">
|
|
129
|
+
* <Icon aria-hidden={false}>
|
|
130
|
+
* <Icon.Remove />
|
|
131
|
+
* </Icon>
|
|
132
|
+
* </button>
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* // ❌ BAD: Icon-only button without accessible name
|
|
136
|
+
* <button>
|
|
137
|
+
* <Icon><Icon.Remove /></Icon>
|
|
138
|
+
* </button>
|
|
139
|
+
*/
|
|
58
140
|
export const Icon = ({
|
|
59
141
|
id,
|
|
60
142
|
classes,
|
|
61
143
|
children,
|
|
62
144
|
styles,
|
|
145
|
+
"aria-hidden": ariaHidden = true,
|
|
146
|
+
"aria-label": ariaLabel,
|
|
147
|
+
role,
|
|
63
148
|
...props
|
|
64
149
|
}: IconProps) => {
|
|
65
150
|
return (
|
|
@@ -70,6 +155,9 @@ export const Icon = ({
|
|
|
70
155
|
className={classes}
|
|
71
156
|
data-icon
|
|
72
157
|
data-style="icons"
|
|
158
|
+
aria-hidden={ariaHidden}
|
|
159
|
+
aria-label={ariaLabel}
|
|
160
|
+
role={role}
|
|
73
161
|
{...props}
|
|
74
162
|
>
|
|
75
163
|
{children}
|
|
@@ -1,12 +1,59 @@
|
|
|
1
1
|
import { ComponentProps } from '#/types'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Props for individual icon SVG components (e.g., Icon.Code, Icon.Home).
|
|
5
|
+
*
|
|
6
|
+
* These props control the visual presentation of SVG icons. Icon components
|
|
7
|
+
* are decorative by default and should be wrapped in the `Icon` container
|
|
8
|
+
* component which handles accessibility concerns.
|
|
9
|
+
*
|
|
10
|
+
* @property {string} [fill] - SVG fill color (CSS color value)
|
|
11
|
+
* @property {number} [size] - Icon size in pixels (sets both width and height)
|
|
12
|
+
* @property {string} [strokeColor] - SVG stroke color (CSS color value)
|
|
13
|
+
* @property {string} [strokeWidth] - SVG stroke width (e.g., '2px', '1.5')
|
|
14
|
+
* @property {string} [role] - ARIA role (use 'img' for semantic icons)
|
|
15
|
+
* @property {string} [aria-label] - Accessible label for standalone icons
|
|
16
|
+
* @property {boolean} [aria-hidden] - Hide from screen readers (default: true via Icon wrapper)
|
|
17
|
+
* @property {string} [alt] - @deprecated Use aria-label instead. Legacy prop for accessible label.
|
|
18
|
+
* @property {number} [width] - Explicit width in pixels (overrides size)
|
|
19
|
+
* @property {number} [height] - Explicit height in pixels (overrides size)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* // Decorative icon with custom styling
|
|
24
|
+
* <Icon>
|
|
25
|
+
* <Icon.Code size={24} fill="blue" />
|
|
26
|
+
* View Code
|
|
27
|
+
* </Icon>
|
|
28
|
+
*
|
|
29
|
+
* // Standalone semantic icon
|
|
30
|
+
* <Icon aria-hidden={false} aria-label="Code snippet">
|
|
31
|
+
* <Icon.Code size={20} />
|
|
32
|
+
* </Icon>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
3
35
|
export interface IconProps extends Partial<ComponentProps> {
|
|
36
|
+
/** SVG fill color */
|
|
4
37
|
fill?: string
|
|
38
|
+
/** Icon size in pixels (sets both width and height) */
|
|
5
39
|
size?: number
|
|
40
|
+
/** SVG stroke color */
|
|
6
41
|
strokeColor?: string
|
|
42
|
+
/** SVG stroke width */
|
|
7
43
|
strokeWidth?: string
|
|
44
|
+
/** ARIA role (use 'img' for semantic icons) */
|
|
8
45
|
role?: string
|
|
46
|
+
/** Accessible label (required for standalone icons) */
|
|
47
|
+
'aria-label'?: string
|
|
48
|
+
/** Hide from screen readers (default: true) */
|
|
49
|
+
'aria-hidden'?: boolean
|
|
50
|
+
/**
|
|
51
|
+
* @deprecated Use aria-label instead. This prop exists for backward compatibility.
|
|
52
|
+
* The alt attribute is only valid for img elements, not SVG.
|
|
53
|
+
*/
|
|
9
54
|
alt?: string
|
|
55
|
+
/** Explicit width in pixels */
|
|
10
56
|
width?: number
|
|
57
|
+
/** Explicit height in pixels */
|
|
11
58
|
height?: number
|
|
12
59
|
}
|