@fpkit/acss 0.5.11 → 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 +514 -18
- package/libs/chunk-23ANBDCR.js +8 -0
- package/libs/chunk-23ANBDCR.js.map +1 -0
- package/libs/chunk-2LTJ7HHX.cjs +18 -0
- package/libs/chunk-2LTJ7HHX.cjs.map +1 -0
- package/libs/chunk-2Y7W75TT.js +9 -0
- package/libs/chunk-2Y7W75TT.js.map +1 -0
- package/libs/chunk-3MKLDCKQ.cjs +31 -0
- package/libs/chunk-3MKLDCKQ.cjs.map +1 -0
- package/libs/chunk-5M57K4SW.js +8 -0
- package/libs/chunk-5M57K4SW.js.map +1 -0
- package/libs/chunk-5S4ORA4C.cjs +15 -0
- package/libs/chunk-5S4ORA4C.cjs.map +1 -0
- package/libs/chunk-772NRB75.js +9 -0
- package/libs/chunk-772NRB75.js.map +1 -0
- package/libs/chunk-AHDJGCG5.cjs +15 -0
- package/libs/chunk-AHDJGCG5.cjs.map +1 -0
- package/libs/chunk-B7F5FS6D.cjs +16 -0
- package/libs/chunk-B7F5FS6D.cjs.map +1 -0
- package/libs/chunk-BHRQBJRY.js +8 -0
- package/libs/chunk-BHRQBJRY.js.map +1 -0
- 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-G55UJ53G.cjs +16 -0
- package/libs/chunk-G55UJ53G.cjs.map +1 -0
- package/libs/chunk-GZ4QFPRY.js +9 -0
- package/libs/chunk-GZ4QFPRY.js.map +1 -0
- package/libs/chunk-IYUN2EW3.cjs +15 -0
- package/libs/chunk-IYUN2EW3.cjs.map +1 -0
- package/libs/chunk-J32EZPYD.cjs +15 -0
- package/libs/chunk-J32EZPYD.cjs.map +1 -0
- 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-LT5KZ2QW.cjs +22 -0
- package/libs/chunk-LT5KZ2QW.cjs.map +1 -0
- package/libs/chunk-M5RRNTVX.cjs +15 -0
- package/libs/chunk-M5RRNTVX.cjs.map +1 -0
- package/libs/chunk-NGTJDDFO.js +8 -0
- package/libs/chunk-NGTJDDFO.js.map +1 -0
- 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-P7TTEYCD.js +7 -0
- package/libs/chunk-P7TTEYCD.js.map +1 -0
- package/libs/chunk-PQ2K3BM6.cjs +17 -0
- package/libs/chunk-PQ2K3BM6.cjs.map +1 -0
- package/libs/chunk-QLZWHAMK.js +8 -0
- package/libs/chunk-QLZWHAMK.js.map +1 -0
- package/libs/chunk-RIVUMPOG.js +8 -0
- package/libs/chunk-RIVUMPOG.js.map +1 -0
- package/libs/chunk-ROZI23GS.cjs +15 -0
- package/libs/chunk-ROZI23GS.cjs.map +1 -0
- package/libs/chunk-S7BABR7Z.cjs +13 -0
- package/libs/chunk-S7BABR7Z.cjs.map +1 -0
- package/libs/chunk-SMYRLO3E.js +8 -0
- package/libs/chunk-SMYRLO3E.js.map +1 -0
- package/libs/chunk-TYRCEX2L.js +8 -0
- package/libs/chunk-TYRCEX2L.js.map +1 -0
- package/libs/chunk-VUH3FXGJ.js +11 -0
- package/libs/chunk-VUH3FXGJ.js.map +1 -0
- package/libs/chunk-XBA562WW.js +8 -0
- package/libs/chunk-XBA562WW.js.map +1 -0
- package/libs/chunk-XTQKWY7W.cjs +32 -0
- package/libs/chunk-XTQKWY7W.cjs.map +1 -0
- package/libs/chunk-ZANSFMTD.js +9 -0
- package/libs/chunk-ZANSFMTD.js.map +1 -0
- package/libs/component-props-a8a2f97e.d.ts +38 -0
- 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 +24 -0
- package/libs/components/breadcrumbs/breadcrumb.cjs.map +1 -0
- package/libs/components/breadcrumbs/breadcrumb.d.cts +290 -0
- package/libs/components/breadcrumbs/breadcrumb.d.ts +290 -0
- package/libs/components/breadcrumbs/breadcrumb.js +5 -0
- package/libs/components/breadcrumbs/breadcrumb.js.map +1 -0
- package/libs/components/button.cjs +19 -0
- package/libs/components/button.cjs.map +1 -0
- package/libs/components/button.d.cts +16 -0
- package/libs/components/button.d.ts +16 -0
- package/libs/components/button.js +4 -0
- package/libs/components/button.js.map +1 -0
- 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 +31 -0
- package/libs/components/card.cjs.map +1 -0
- package/libs/components/card.d.cts +302 -0
- package/libs/components/card.d.ts +302 -0
- package/libs/components/card.js +4 -0
- package/libs/components/card.js.map +1 -0
- 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 +22 -0
- package/libs/components/dialog/dialog.cjs.map +1 -0
- package/libs/components/dialog/dialog.css +1 -1
- package/libs/components/dialog/dialog.css.map +1 -1
- package/libs/components/dialog/dialog.d.cts +105 -0
- package/libs/components/dialog/dialog.d.ts +105 -0
- package/libs/components/dialog/dialog.js +7 -0
- package/libs/components/dialog/dialog.js.map +1 -0
- package/libs/components/dialog/dialog.min.css +2 -2
- package/libs/components/form/fields.cjs +19 -0
- package/libs/components/form/fields.cjs.map +1 -0
- package/libs/components/form/fields.d.cts +24 -0
- package/libs/components/form/fields.d.ts +24 -0
- package/libs/components/form/fields.js +4 -0
- package/libs/components/form/fields.js.map +1 -0
- package/libs/components/form/inputs.cjs +19 -0
- package/libs/components/form/inputs.cjs.map +1 -0
- package/libs/components/form/inputs.d.cts +2 -0
- package/libs/components/form/inputs.d.ts +2 -0
- package/libs/components/form/inputs.js +4 -0
- package/libs/components/form/inputs.js.map +1 -0
- package/libs/components/form/textarea.cjs +19 -0
- package/libs/components/form/textarea.cjs.map +1 -0
- package/libs/components/form/textarea.d.cts +29 -0
- package/libs/components/form/textarea.d.ts +29 -0
- package/libs/components/form/textarea.js +4 -0
- package/libs/components/form/textarea.js.map +1 -0
- package/libs/components/heading/heading.cjs +10 -0
- package/libs/components/heading/heading.cjs.map +1 -0
- package/libs/components/heading/heading.d.cts +3 -0
- package/libs/components/heading/heading.d.ts +3 -0
- package/libs/components/heading/heading.js +4 -0
- package/libs/components/heading/heading.js.map +1 -0
- package/libs/components/icons/icon.cjs +19 -0
- package/libs/components/icons/icon.cjs.map +1 -0
- package/libs/{icons-31ace3de.d.ts → components/icons/icon.d.cts} +151 -61
- package/libs/components/icons/icon.d.ts +445 -0
- package/libs/components/icons/icon.js +4 -0
- package/libs/components/icons/icon.js.map +1 -0
- 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 +19 -0
- package/libs/components/link/link.cjs.map +1 -0
- package/libs/components/link/link.d.cts +19 -0
- package/libs/components/link/link.d.ts +19 -0
- package/libs/components/link/link.js +4 -0
- package/libs/components/link/link.js.map +1 -0
- package/libs/components/list/list.cjs +23 -0
- package/libs/components/list/list.cjs.map +1 -0
- package/libs/components/list/list.d.cts +39 -0
- package/libs/components/list/list.d.ts +39 -0
- package/libs/components/list/list.js +4 -0
- package/libs/components/list/list.js.map +1 -0
- package/libs/components/modal.cjs +14 -0
- package/libs/components/modal.cjs.map +1 -0
- package/libs/components/modal.d.cts +35 -0
- package/libs/components/modal.d.ts +35 -0
- package/libs/components/modal.js +5 -0
- package/libs/components/modal.js.map +1 -0
- package/libs/components/nav/nav.cjs +28 -0
- package/libs/components/nav/nav.cjs.map +1 -0
- package/libs/components/nav/nav.d.cts +44 -0
- package/libs/components/nav/nav.d.ts +44 -0
- package/libs/components/nav/nav.js +5 -0
- package/libs/components/nav/nav.js.map +1 -0
- package/libs/components/popover/popover.cjs +23 -0
- package/libs/components/popover/popover.cjs.map +1 -0
- package/libs/components/popover/popover.d.cts +40 -0
- package/libs/components/popover/popover.d.ts +40 -0
- package/libs/components/popover/popover.js +4 -0
- package/libs/components/popover/popover.js.map +1 -0
- package/libs/components/tables/table.cjs +21 -0
- package/libs/components/tables/table.cjs.map +1 -0
- package/libs/components/tables/table.d.cts +36 -0
- package/libs/components/tables/table.d.ts +36 -0
- package/libs/components/tables/table.js +4 -0
- package/libs/components/tables/table.js.map +1 -0
- package/libs/components/text/text.cjs +23 -0
- package/libs/components/text/text.cjs.map +1 -0
- package/libs/components/text/text.d.cts +30 -0
- package/libs/components/text/text.d.ts +30 -0
- package/libs/components/text/text.js +4 -0
- package/libs/components/text/text.js.map +1 -0
- 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 -2
- package/libs/icons.d.cts +3 -1
- package/libs/icons.d.ts +3 -1
- package/libs/icons.js +2 -1
- package/libs/index.cjs +174 -62
- 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 +529 -446
- package/libs/index.d.ts +529 -446
- package/libs/index.js +36 -7
- package/libs/index.js.map +1 -1
- package/libs/inputs-f3a216db.d.ts +45 -0
- package/libs/ui-645f95b5.d.ts +285 -0
- package/package.json +2 -2
- 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/button.ts +2 -0
- package/src/components/buttons/button.scss +34 -31
- package/src/components/buttons/button.stories.tsx +35 -0
- package/src/components/card.ts +2 -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 -0
- 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/modal.ts +1 -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 +51 -19
- 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 -0
- 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 -43
- 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-PWVRDQ3R.js +0 -8
- package/libs/chunk-PWVRDQ3R.js.map +0 -1
- package/libs/chunk-SVS4MX3U.cjs +0 -31
- package/libs/chunk-SVS4MX3U.cjs.map +0 -1
- package/src/components/cards/README.md +0 -80
- package/src/components/dialog/hooks/useClickOutside.ts +0 -33
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# 🔍 Accessibility Review: Details Component
|
|
2
|
+
|
|
3
|
+
**Review Date:** 2025-10-20
|
|
4
|
+
**WCAG Version:** 2.1 Level AA
|
|
5
|
+
**Reviewer:** Claude Code (WCAG Compliance Reviewer)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Executive Summary
|
|
10
|
+
|
|
11
|
+
**Overall Rating: ✅ Excellent** - The Details component demonstrates strong accessibility practices by leveraging native HTML `<details>` and `<summary>` elements, which provide built-in keyboard support and ARIA semantics.
|
|
12
|
+
|
|
13
|
+
**Issues Found:** 0 errors, 2 warnings, 3 recommendations
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## ✅ What's Working Well
|
|
18
|
+
|
|
19
|
+
### 1. **Semantic HTML Foundation (WCAG 4.1.2, 1.3.1)**
|
|
20
|
+
The component uses native `<details>` and `<summary>` elements, which provide:
|
|
21
|
+
- Built-in `aria-expanded` state management by the browser
|
|
22
|
+
- Native keyboard support (Space, Enter)
|
|
23
|
+
- Automatic screen reader announcements as "disclosure" or "expandable" widget
|
|
24
|
+
- Proper role semantics without additional ARIA
|
|
25
|
+
|
|
26
|
+
### 2. **Keyboard Accessibility (WCAG 2.1.1)**
|
|
27
|
+
✅ Native `<details>` element handles all keyboard interactions
|
|
28
|
+
✅ No custom JavaScript required for basic keyboard functionality
|
|
29
|
+
✅ No keyboard traps
|
|
30
|
+
|
|
31
|
+
### 3. **Focus Indicators (WCAG 2.4.7)**
|
|
32
|
+
From `details.scss:77-81`:
|
|
33
|
+
```scss
|
|
34
|
+
&:focus-within {
|
|
35
|
+
outline: none;
|
|
36
|
+
border-bottom: solid 2px currentColor;
|
|
37
|
+
background-color: whitesmoke;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
✅ Custom focus indicator with visible border and background change
|
|
41
|
+
✅ Uses `currentColor` which adapts to text color
|
|
42
|
+
|
|
43
|
+
### 4. **Component Forwarding (React Best Practice)**
|
|
44
|
+
From `details.tsx:54`:
|
|
45
|
+
```tsx
|
|
46
|
+
export const Details = React.forwardRef<HTMLDetailsElement, DetailsProps>(
|
|
47
|
+
```
|
|
48
|
+
✅ Properly forwards refs to enable programmatic focus management
|
|
49
|
+
|
|
50
|
+
### 5. **Accordion Behavior (WCAG 4.1.2)**
|
|
51
|
+
The component supports the native `name` attribute for accordion groups where only one details can be open at a time - this is a modern HTML feature with excellent accessibility.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## ⚠️ Warnings (Should Fix)
|
|
56
|
+
|
|
57
|
+
### Warning 1: Focus Indicator Contrast Not Guaranteed
|
|
58
|
+
|
|
59
|
+
**Location:** `details.scss:77-81`
|
|
60
|
+
|
|
61
|
+
**Issue (WCAG 2.4.7 - Level AA):**
|
|
62
|
+
```scss
|
|
63
|
+
&:focus-within {
|
|
64
|
+
outline: none; // ← Removes native outline
|
|
65
|
+
border-bottom: solid 2px currentColor; // ← Uses currentColor
|
|
66
|
+
background-color: whitesmoke;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Problem:**
|
|
71
|
+
- The custom focus indicator uses `currentColor`, which means the border color depends on the text color
|
|
72
|
+
- If text color has low contrast with the background, the focus indicator might fall below the required 3:1 contrast ratio
|
|
73
|
+
- `whitesmoke` (#f5f5f5) background might not provide sufficient contrast in all color schemes
|
|
74
|
+
|
|
75
|
+
**Recommended Fix:**
|
|
76
|
+
```scss
|
|
77
|
+
&:focus-within {
|
|
78
|
+
outline: 2px solid #0066CC; /* Specific high-contrast color */
|
|
79
|
+
outline-offset: 2px;
|
|
80
|
+
background-color: #e8f4f8; /* Light blue tint */
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Or use focus-visible for better UX */
|
|
84
|
+
&:focus-visible {
|
|
85
|
+
outline: 2px solid #0066CC;
|
|
86
|
+
outline-offset: 2px;
|
|
87
|
+
background-color: #e8f4f8;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
&:focus:not(:focus-visible) {
|
|
91
|
+
outline: none;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Why:** WCAG 2.4.7 requires focus indicators to have at least 3:1 contrast ratio against adjacent colors. Using a specific color ensures this requirement is consistently met.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Warning 2: Optional `aria-label` May Cause Confusion
|
|
100
|
+
|
|
101
|
+
**Location:** `details.tsx:94`
|
|
102
|
+
|
|
103
|
+
**Issue (WCAG 4.1.2 - Best Practice):**
|
|
104
|
+
```tsx
|
|
105
|
+
<UI
|
|
106
|
+
as="details"
|
|
107
|
+
aria-label={ariaLabel} // ← Applied to <details> element
|
|
108
|
+
...>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Problem:**
|
|
112
|
+
- The native `<details>` element already has proper semantics
|
|
113
|
+
- Adding `aria-label` to the `<details>` element might override the automatic screen reader announcement
|
|
114
|
+
- The label should typically be on the `<summary>` element, not the `<details>` wrapper
|
|
115
|
+
- Most screen readers announce: "Disclosure: [summary content], collapsed/expanded"
|
|
116
|
+
|
|
117
|
+
**Current Type Definition:** `details.types.ts:49-61`
|
|
118
|
+
```tsx
|
|
119
|
+
/**
|
|
120
|
+
* Accessible label for screen readers.
|
|
121
|
+
* If not provided, the native `<details>` semantic will be used.
|
|
122
|
+
*
|
|
123
|
+
* Note: Native `<details>` elements are already semantic and announced properly
|
|
124
|
+
* by screen readers. Only provide this if you need to override the default behavior.
|
|
125
|
+
*/
|
|
126
|
+
ariaLabel?: string;
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Recommended Fix:**
|
|
130
|
+
|
|
131
|
+
Option A - Remove `aria-label` from details (preferred):
|
|
132
|
+
```tsx
|
|
133
|
+
<UI
|
|
134
|
+
as="details"
|
|
135
|
+
// Remove aria-label from details element
|
|
136
|
+
...>
|
|
137
|
+
<UI
|
|
138
|
+
as="summary"
|
|
139
|
+
onPointerDown={handlePointerDown}
|
|
140
|
+
aria-label={ariaLabel} // Move here if needed
|
|
141
|
+
>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Option B - Keep current implementation with better documentation:
|
|
145
|
+
The documentation is actually quite good! It warns users this should rarely be used. Consider moving the `ariaLabel` to apply to the summary element instead for better screen reader support.
|
|
146
|
+
|
|
147
|
+
**Why:** Adding `aria-label` to `<details>` can interfere with the native semantic announcement. If additional labeling is needed, it should be on the `<summary>` element.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 💡 Recommendations (Best Practices)
|
|
152
|
+
|
|
153
|
+
### Recommendation 1: Add `aria-controls` Relationship
|
|
154
|
+
|
|
155
|
+
**Enhancement:** Strengthen the relationship between summary and content for screen readers.
|
|
156
|
+
|
|
157
|
+
**Suggested Implementation:**
|
|
158
|
+
```tsx
|
|
159
|
+
export const Details = React.forwardRef<HTMLDetailsElement, DetailsProps>(
|
|
160
|
+
({ summary, icon, styles, classes, ariaLabel, name, open, onPointerDown, onToggle, children, ...props }, ref) => {
|
|
161
|
+
const contentId = React.useId(); // Generate unique ID
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<UI as="details" ...>
|
|
165
|
+
<UI
|
|
166
|
+
as="summary"
|
|
167
|
+
onPointerDown={handlePointerDown}
|
|
168
|
+
aria-controls={contentId} // ← Links to content
|
|
169
|
+
>
|
|
170
|
+
{icon}
|
|
171
|
+
{summary}
|
|
172
|
+
</UI>
|
|
173
|
+
<UI as="section" id={contentId}> {/* ← Receives ID */}
|
|
174
|
+
{children}
|
|
175
|
+
</UI>
|
|
176
|
+
</UI>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Benefit:** Explicitly declares the relationship between trigger and content for assistive technologies.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### Recommendation 2: Consider `aria-hidden` for Decorative Icons
|
|
187
|
+
|
|
188
|
+
**Enhancement:** If the `icon` prop is purely decorative (like a chevron), it should be hidden from screen readers.
|
|
189
|
+
|
|
190
|
+
**Current Usage Example:**
|
|
191
|
+
```tsx
|
|
192
|
+
<Details
|
|
193
|
+
summary="Shipping Information"
|
|
194
|
+
icon={<ChevronDownIcon />} // ← Should this be announced?
|
|
195
|
+
>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Recommendation:**
|
|
199
|
+
Add guidance in documentation or automatically apply `aria-hidden`:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
// Option 1: Document in examples
|
|
203
|
+
<Details
|
|
204
|
+
summary="Shipping Information"
|
|
205
|
+
icon={<ChevronDownIcon aria-hidden="true" />}
|
|
206
|
+
>
|
|
207
|
+
|
|
208
|
+
// Option 2: Wrap icon automatically (recommended)
|
|
209
|
+
<UI as="summary" onPointerDown={handlePointerDown}>
|
|
210
|
+
{icon && <span aria-hidden="true">{icon}</span>}
|
|
211
|
+
{summary}
|
|
212
|
+
</UI>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Why:** Decorative icons add visual context but should not be announced by screen readers, as the summary text already conveys the meaning (WCAG 1.1.1).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### Recommendation 3: Add Example with ARIA Live Region for Dynamic Content
|
|
220
|
+
|
|
221
|
+
**Enhancement:** Document how to handle dynamically loaded content within details.
|
|
222
|
+
|
|
223
|
+
**Add to JSDoc documentation:**
|
|
224
|
+
```tsx
|
|
225
|
+
/**
|
|
226
|
+
* @example
|
|
227
|
+
* ```tsx
|
|
228
|
+
* // Details with async content loading
|
|
229
|
+
* <Details summary="Load More Data" onToggle={handleLoadData}>
|
|
230
|
+
* {isLoading ? (
|
|
231
|
+
* <div role="status" aria-live="polite">Loading...</div>
|
|
232
|
+
* ) : (
|
|
233
|
+
* <div>{data}</div>
|
|
234
|
+
* )}
|
|
235
|
+
* </Details>
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Why:** Users may load content dynamically when details open. Screen readers need to be informed of loading states (WCAG 4.1.3).
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 🎨 CSS Accessibility Review
|
|
245
|
+
|
|
246
|
+
### ✅ Good Practices Found:
|
|
247
|
+
|
|
248
|
+
1. **rem Units Throughout** - Text and spacing use relative units for zoom compatibility (WCAG 1.4.4)
|
|
249
|
+
2. **CSS Custom Properties** - Themeable design supports user customization (WCAG 1.4.12)
|
|
250
|
+
3. **Smooth Transitions** - Visual feedback for state changes aids understanding
|
|
251
|
+
4. **Marker Removal** - Custom styling without breaking semantics
|
|
252
|
+
5. **Responsive Design** - Proper border and radius handling for stacked details
|
|
253
|
+
|
|
254
|
+
### ⚠️ CSS Concern: Text Spacing Support (WCAG 1.4.12)
|
|
255
|
+
|
|
256
|
+
The component should be tested with increased text spacing requirements:
|
|
257
|
+
```css
|
|
258
|
+
/* Test with these overrides */
|
|
259
|
+
* {
|
|
260
|
+
line-height: 1.5 !important;
|
|
261
|
+
letter-spacing: 0.12em !important;
|
|
262
|
+
word-spacing: 0.16em !important;
|
|
263
|
+
margin-bottom: 2em !important; /* Paragraph spacing */
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Recommendation:** Add to testing documentation to ensure content doesn't overflow or become clipped.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 🧪 Testing Recommendations
|
|
272
|
+
|
|
273
|
+
### Automated Testing
|
|
274
|
+
|
|
275
|
+
**Install Dependencies:**
|
|
276
|
+
```bash
|
|
277
|
+
npm install --save-dev jest-axe @axe-core/react @testing-library/user-event
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Component Test Example:**
|
|
281
|
+
```tsx
|
|
282
|
+
import { render } from '@testing-library/react';
|
|
283
|
+
import userEvent from '@testing-library/user-event';
|
|
284
|
+
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
285
|
+
import { Details } from './details';
|
|
286
|
+
|
|
287
|
+
expect.extend(toHaveNoViolations);
|
|
288
|
+
|
|
289
|
+
describe('Details - Accessibility', () => {
|
|
290
|
+
test('has no accessibility violations', async () => {
|
|
291
|
+
const { container } = render(
|
|
292
|
+
<Details summary="Test Summary">
|
|
293
|
+
<p>Test content</p>
|
|
294
|
+
</Details>
|
|
295
|
+
);
|
|
296
|
+
const results = await axe(container);
|
|
297
|
+
expect(results).toHaveNoViolations();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('is keyboard accessible', async () => {
|
|
301
|
+
const user = userEvent.setup();
|
|
302
|
+
const { getByText } = render(
|
|
303
|
+
<Details summary="Click to expand">
|
|
304
|
+
<p>Hidden content</p>
|
|
305
|
+
</Details>
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const summary = getByText('Click to expand');
|
|
309
|
+
|
|
310
|
+
// Tab to summary
|
|
311
|
+
await user.tab();
|
|
312
|
+
expect(summary).toHaveFocus();
|
|
313
|
+
|
|
314
|
+
// Press Enter to open
|
|
315
|
+
await user.keyboard('{Enter}');
|
|
316
|
+
expect(getByText('Hidden content')).toBeVisible();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('accordion mode works correctly', async () => {
|
|
320
|
+
const { container } = render(
|
|
321
|
+
<>
|
|
322
|
+
<Details name="accordion" summary="Item 1">Content 1</Details>
|
|
323
|
+
<Details name="accordion" summary="Item 2" open>Content 2</Details>
|
|
324
|
+
<Details name="accordion" summary="Item 3">Content 3</Details>
|
|
325
|
+
</>
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const allDetails = container.querySelectorAll('details');
|
|
329
|
+
const openDetails = container.querySelectorAll('details[open]');
|
|
330
|
+
|
|
331
|
+
expect(allDetails).toHaveLength(3);
|
|
332
|
+
expect(openDetails).toHaveLength(1);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Manual Testing Checklist
|
|
338
|
+
|
|
339
|
+
#### Keyboard Navigation
|
|
340
|
+
- [ ] Tab to summary - should receive focus with visible indicator
|
|
341
|
+
- [ ] Press Space - should toggle open/closed
|
|
342
|
+
- [ ] Press Enter - should toggle open/closed
|
|
343
|
+
- [ ] Tab when open - should move to content inside
|
|
344
|
+
- [ ] Shift+Tab - should move focus backwards correctly
|
|
345
|
+
- [ ] No keyboard traps when navigating through content
|
|
346
|
+
|
|
347
|
+
#### Screen Reader Testing (NVDA/VoiceOver/JAWS)
|
|
348
|
+
- [ ] Announces as "disclosure", "expandable", or similar widget type
|
|
349
|
+
- [ ] Announces current state (expanded/collapsed)
|
|
350
|
+
- [ ] Summary text is read correctly
|
|
351
|
+
- [ ] Content is accessible and announced when expanded
|
|
352
|
+
- [ ] Icon (if present) is not announced if decorative
|
|
353
|
+
- [ ] State changes are announced when toggling
|
|
354
|
+
|
|
355
|
+
#### Visual Testing
|
|
356
|
+
- [ ] Focus indicator visible with sufficient contrast (minimum 3:1)
|
|
357
|
+
- [ ] Focus indicator works in dark mode (if applicable)
|
|
358
|
+
- [ ] Works at 200% browser zoom without horizontal scrolling
|
|
359
|
+
- [ ] Content reflows properly at 320px viewport width
|
|
360
|
+
- [ ] Works with increased text spacing (1.5 line-height, 0.12em letter-spacing)
|
|
361
|
+
- [ ] Custom CSS properties can be overridden for theming
|
|
362
|
+
|
|
363
|
+
#### Accordion Mode Testing
|
|
364
|
+
- [ ] Only one details open at a time when using `name` attribute
|
|
365
|
+
- [ ] Opening one closes the others in the same group
|
|
366
|
+
- [ ] State changes announced to screen readers
|
|
367
|
+
- [ ] Keyboard navigation works between accordion items
|
|
368
|
+
|
|
369
|
+
#### Browser Testing
|
|
370
|
+
- [ ] Chrome/Edge (latest)
|
|
371
|
+
- [ ] Firefox (latest)
|
|
372
|
+
- [ ] Safari (latest)
|
|
373
|
+
- [ ] Mobile Safari (iOS)
|
|
374
|
+
- [ ] Chrome Mobile (Android)
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## 📊 WCAG 2.1 AA Compliance Summary
|
|
379
|
+
|
|
380
|
+
| Criterion | Level | Status | Notes |
|
|
381
|
+
|-----------|-------|--------|-------|
|
|
382
|
+
| **1.1.1 Non-text Content** | A | ✅ Pass | No images in component core; icon handling documented |
|
|
383
|
+
| **1.3.1 Info & Relationships** | A | ✅ Pass | Semantic `<details>` and `<summary>` elements |
|
|
384
|
+
| **1.3.2 Meaningful Sequence** | A | ✅ Pass | Logical DOM order maintained |
|
|
385
|
+
| **1.4.3 Contrast (Minimum)** | AA | ⚠️ Warning | Focus indicator contrast not guaranteed with `currentColor` |
|
|
386
|
+
| **1.4.10 Reflow** | AA | ✅ Pass | Uses flexible layout; recommend testing at 320px |
|
|
387
|
+
| **1.4.11 Non-text Contrast** | AA | ⚠️ Warning | Focus indicator needs contrast verification |
|
|
388
|
+
| **1.4.12 Text Spacing** | AA | ✅ Pass | Uses rem units; recommend manual testing |
|
|
389
|
+
| **1.4.13 Content on Hover/Focus** | AA | N/A | No hover/focus tooltips |
|
|
390
|
+
| **2.1.1 Keyboard** | A | ✅ Pass | Native keyboard support via `<details>` |
|
|
391
|
+
| **2.1.2 No Keyboard Trap** | A | ✅ Pass | No traps detected |
|
|
392
|
+
| **2.4.3 Focus Order** | A | ✅ Pass | Logical DOM and focus order |
|
|
393
|
+
| **2.4.7 Focus Visible** | AA | ⚠️ Warning | Custom focus indicator needs guaranteed contrast |
|
|
394
|
+
| **3.2.1 On Focus** | A | ✅ Pass | No unexpected context changes |
|
|
395
|
+
| **3.2.2 On Input** | A | ✅ Pass | Toggle requires explicit activation |
|
|
396
|
+
| **4.1.2 Name, Role, Value** | A | ✅ Pass | Native semantics provide all required properties |
|
|
397
|
+
| **4.1.3 Status Messages** | AA | 💡 Recommend | Document pattern for dynamic content loading |
|
|
398
|
+
|
|
399
|
+
**Legend:**
|
|
400
|
+
- ✅ Pass - Meets requirement
|
|
401
|
+
- ⚠️ Warning - Needs attention to ensure compliance
|
|
402
|
+
- 💡 Recommend - Optional enhancement
|
|
403
|
+
- N/A - Not applicable to this component
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## 🎯 Quick Wins (Priority Fixes)
|
|
408
|
+
|
|
409
|
+
### High Priority (5-15 minutes total)
|
|
410
|
+
|
|
411
|
+
1. **Update focus indicator for guaranteed contrast** (5 min)
|
|
412
|
+
```scss
|
|
413
|
+
summary {
|
|
414
|
+
&:focus-visible {
|
|
415
|
+
outline: 2px solid #0066CC;
|
|
416
|
+
outline-offset: 2px;
|
|
417
|
+
background-color: #e8f4f8;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
&:focus:not(:focus-visible) {
|
|
421
|
+
outline: none;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
2. **Consider moving `ariaLabel` to summary or removing** (10 min)
|
|
427
|
+
- Review if `aria-label` on `<details>` is truly needed
|
|
428
|
+
- If needed, apply to `<summary>` instead
|
|
429
|
+
- Update TypeScript types accordingly
|
|
430
|
+
|
|
431
|
+
### Medium Priority (15-30 minutes)
|
|
432
|
+
|
|
433
|
+
3. **Add `aria-controls` relationship** (10 min)
|
|
434
|
+
- Use `React.useId()` to generate unique IDs
|
|
435
|
+
- Link summary to content section
|
|
436
|
+
|
|
437
|
+
4. **Auto-hide decorative icons from screen readers** (10 min)
|
|
438
|
+
```tsx
|
|
439
|
+
{icon && <span aria-hidden="true">{icon}</span>}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
5. **Add automated accessibility tests** (10 min)
|
|
443
|
+
- Install jest-axe
|
|
444
|
+
- Add basic axe tests to existing test file
|
|
445
|
+
|
|
446
|
+
### Low Priority (Optional Enhancements)
|
|
447
|
+
|
|
448
|
+
6. **Document dynamic content patterns** (5 min)
|
|
449
|
+
- Add JSDoc examples for loading states
|
|
450
|
+
- Show ARIA live region usage
|
|
451
|
+
|
|
452
|
+
7. **Create comprehensive manual test checklist** (Already done in this document!)
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## 📚 Additional Resources
|
|
457
|
+
|
|
458
|
+
### WCAG Guidelines
|
|
459
|
+
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/?versions=2.1&levels=aa)
|
|
460
|
+
- [Understanding WCAG 2.1](https://www.w3.org/WAI/WCAG21/Understanding/)
|
|
461
|
+
- [ARIA Authoring Practices Guide - Disclosure Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/)
|
|
462
|
+
|
|
463
|
+
### HTML Details Element
|
|
464
|
+
- [MDN: The Details Disclosure Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details)
|
|
465
|
+
- [HTML Spec: The details element](https://html.spec.whatwg.org/multipage/interactive-elements.html#the-details-element)
|
|
466
|
+
|
|
467
|
+
### Testing Tools
|
|
468
|
+
- [axe DevTools Browser Extension](https://www.deque.com/axe/devtools/)
|
|
469
|
+
- [WAVE Browser Extension](https://wave.webaim.org/extension/)
|
|
470
|
+
- [NVDA Screen Reader](https://www.nvaccess.org/) (Windows, Free)
|
|
471
|
+
- [VoiceOver Screen Reader](https://www.apple.com/accessibility/voiceover/) (macOS/iOS, Built-in)
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## 💡 Key Insights
|
|
476
|
+
|
|
477
|
+
### 1. Native HTML Wins
|
|
478
|
+
This component brilliantly leverages `<details>` and `<summary>`, which are among the most accessible HTML5 elements. The browser handles `aria-expanded`, keyboard events, and screen reader semantics automatically - no custom JavaScript needed! This is a perfect example of "use platform features first."
|
|
479
|
+
|
|
480
|
+
### 2. Focus-visible Pattern
|
|
481
|
+
Modern CSS supports `:focus-visible` which shows focus indicators only for keyboard users (not mouse clicks), providing better UX without compromising accessibility. This is preferable to the older `:focus-within` approach used currently.
|
|
482
|
+
|
|
483
|
+
### 3. Progressive Enhancement
|
|
484
|
+
The `name` attribute for accordion behavior is a modern HTML feature with excellent accessibility built-in. It's a perfect example of progressive enhancement where newer browsers get richer functionality while older browsers still work perfectly with independent details elements.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Summary
|
|
489
|
+
|
|
490
|
+
The Details component is **already highly accessible** thanks to its use of semantic HTML. The warnings are minor and relate to ensuring consistent contrast in all theming scenarios. The recommendations are optional enhancements that would make an already excellent component even better.
|
|
491
|
+
|
|
492
|
+
**Estimated time to address all warnings:** 15-20 minutes
|
|
493
|
+
**Current accessibility grade:** A- (Excellent, with minor improvements possible)
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
**Next Steps:**
|
|
498
|
+
1. Review and address the two warnings (focus indicator contrast)
|
|
499
|
+
2. Consider implementing the recommended enhancements
|
|
500
|
+
3. Add automated accessibility tests to CI/CD pipeline
|
|
501
|
+
4. Conduct manual screen reader testing with NVDA and VoiceOver
|
|
502
|
+
5. Test with users who rely on assistive technologies (if possible)
|