@fpkit/acss 1.0.0-beta.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/docs/README.md +325 -0
- package/docs/guides/accessibility.md +764 -0
- package/docs/guides/architecture.md +705 -0
- package/docs/guides/composition.md +688 -0
- package/docs/guides/css-variables.md +522 -0
- package/docs/guides/storybook.md +828 -0
- package/docs/guides/testing.md +817 -0
- package/docs/testing/focus-indicator-testing.md +437 -0
- package/libs/{chunk-7XPFW7CB.js → chunk-43TK2ICH.js} +2 -2
- package/libs/chunk-5PJYLVFY.cjs +17 -0
- package/libs/chunk-5PJYLVFY.cjs.map +1 -0
- package/libs/chunk-E4OSROCA.cjs +17 -0
- package/libs/chunk-E4OSROCA.cjs.map +1 -0
- package/libs/chunk-KVKQLRJG.js +10 -0
- package/libs/chunk-KVKQLRJG.js.map +1 -0
- package/libs/{chunk-QVW6W76L.cjs → chunk-MGPWZRBX.cjs} +3 -3
- package/libs/chunk-NNTBIHSD.js +8 -0
- package/libs/chunk-NNTBIHSD.js.map +1 -0
- package/libs/{chunk-X3JCTEPD.js → chunk-QKHPHMG2.js} +2 -2
- package/libs/{chunk-T4T6GWYQ.cjs → chunk-R7NLLZU2.cjs} +3 -3
- package/libs/{chunk-X5LGFCWG.js → chunk-UJAQVHWC.js} +3 -3
- package/libs/{chunk-DKTHCQ5P.cjs → chunk-X5RKCLDC.cjs} +3 -3
- package/libs/components/breadcrumbs/breadcrumb.cjs +5 -5
- package/libs/components/breadcrumbs/breadcrumb.d.cts +1 -1
- package/libs/components/breadcrumbs/breadcrumb.d.ts +1 -1
- package/libs/components/breadcrumbs/breadcrumb.js +2 -2
- package/libs/components/button.cjs +3 -3
- package/libs/components/button.d.cts +1 -1
- package/libs/components/button.d.ts +1 -1
- package/libs/components/button.js +1 -1
- 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/dialog/dialog.cjs +4 -4
- package/libs/components/dialog/dialog.js +2 -2
- package/libs/components/icons/icon.d.cts +32 -32
- package/libs/components/icons/icon.d.ts +32 -32
- package/libs/components/link/link.cjs +11 -3
- package/libs/components/link/link.d.cts +131 -3
- package/libs/components/link/link.d.ts +131 -3
- package/libs/components/link/link.js +1 -1
- package/libs/components/list/list.css +1 -1
- package/libs/components/list/list.min.css +1 -1
- package/libs/components/modal.cjs +3 -3
- package/libs/components/modal.js +2 -2
- package/libs/hooks.cjs +3 -3
- package/libs/hooks.d.cts +1 -1
- package/libs/hooks.d.ts +1 -1
- package/libs/hooks.js +2 -2
- package/libs/index.cjs +12 -12
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +237 -2
- package/libs/index.d.ts +237 -2
- package/libs/index.js +5 -5
- package/package.json +4 -3
- package/src/components/README.mdx +1 -1
- package/src/components/breadcrumbs/breadcrumb.test.tsx +1 -2
- package/src/components/buttons/README.mdx +19 -9
- package/src/components/buttons/button.scss +5 -0
- package/src/components/buttons/button.stories.tsx +8 -5
- package/src/components/buttons/button.tsx +19 -15
- package/src/components/cards/card.stories.tsx +1 -1
- package/src/components/details/details.stories.tsx +1 -1
- package/src/components/form/form.stories.tsx +1 -1
- package/src/components/form/input.stories.tsx +1 -1
- package/src/components/form/select.stories.tsx +1 -1
- package/src/components/heading/README.mdx +292 -0
- package/src/components/icons/icon.stories.tsx +1 -1
- package/src/components/link/link.stories.tsx +205 -8
- package/src/components/link/link.test.tsx +1 -1
- package/src/components/link/link.tsx +22 -0
- package/src/components/link/link.types.ts +11 -3
- package/src/components/list/list.scss +1 -1
- package/src/components/nav/nav.stories.tsx +1 -1
- package/src/components/ui.stories.tsx +53 -19
- package/src/docs/accessibility.mdx +484 -0
- package/src/docs/composition.mdx +549 -0
- package/src/docs/css-variables.mdx +380 -0
- package/src/docs/fpkit-developer.mdx +623 -0
- package/src/introduction.mdx +356 -0
- package/src/styles/buttons/button.css +4 -0
- package/src/styles/buttons/button.css.map +1 -1
- package/src/styles/index.css +9 -3
- package/src/styles/index.css.map +1 -1
- package/src/styles/list/list.css +1 -1
- package/src/styles/utilities/_disabled.scss +5 -4
- package/libs/chunk-33PNJ4LO.cjs +0 -15
- package/libs/chunk-33PNJ4LO.cjs.map +0 -1
- package/libs/chunk-GT77BX4L.cjs +0 -17
- package/libs/chunk-GT77BX4L.cjs.map +0 -1
- package/libs/chunk-OVWLQYMK.js +0 -10
- package/libs/chunk-OVWLQYMK.js.map +0 -1
- package/libs/chunk-UEPAWMDF.js +0 -8
- package/libs/chunk-UEPAWMDF.js.map +0 -1
- package/libs/link-5192f411.d.ts +0 -323
- /package/libs/{chunk-7XPFW7CB.js.map → chunk-43TK2ICH.js.map} +0 -0
- /package/libs/{chunk-QVW6W76L.cjs.map → chunk-MGPWZRBX.cjs.map} +0 -0
- /package/libs/{chunk-X3JCTEPD.js.map → chunk-QKHPHMG2.js.map} +0 -0
- /package/libs/{chunk-T4T6GWYQ.cjs.map → chunk-R7NLLZU2.cjs.map} +0 -0
- /package/libs/{chunk-X5LGFCWG.js.map → chunk-UJAQVHWC.js.map} +0 -0
- /package/libs/{chunk-DKTHCQ5P.cjs.map → chunk-X5RKCLDC.cjs.map} +0 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# Focus Indicator Testing Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This guide provides manual testing procedures for verifying that focus indicators meet WCAG 2.4.7 (Focus Visible) Level AA requirements. Focus indicators must have at least **3:1 contrast ratio** against both the background and adjacent colors.
|
|
6
|
+
|
|
7
|
+
## WCAG 2.4.7 Requirements
|
|
8
|
+
|
|
9
|
+
**Success Criterion:** Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible.
|
|
10
|
+
|
|
11
|
+
**Level AA Requirements:**
|
|
12
|
+
- Focus indicators must be visible when an element receives keyboard focus
|
|
13
|
+
- Minimum **3:1 contrast ratio** against:
|
|
14
|
+
- The background color
|
|
15
|
+
- Adjacent (non-focused) component colors
|
|
16
|
+
- Minimum **2px thick** or equivalent area coverage
|
|
17
|
+
|
|
18
|
+
**Reference:** [WCAG 2.4.7 Focus Visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Testing Tools
|
|
23
|
+
|
|
24
|
+
### Required Tools
|
|
25
|
+
|
|
26
|
+
1. **Keyboard** - For navigation testing
|
|
27
|
+
2. **Chrome DevTools** or **Firefox DevTools** - For contrast measurement
|
|
28
|
+
3. **axe DevTools Browser Extension** (Recommended)
|
|
29
|
+
- [Chrome](https://chrome.google.com/webstore/detail/axe-devtools-web-accessib/lhdoppojpmngadmnindnejefpokejbdd)
|
|
30
|
+
- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/)
|
|
31
|
+
|
|
32
|
+
### Optional Tools
|
|
33
|
+
|
|
34
|
+
- **WAVE Browser Extension** - Additional accessibility scanning
|
|
35
|
+
- **Color Contrast Analyzer** - Desktop app for detailed contrast checking
|
|
36
|
+
- **Screen Readers:**
|
|
37
|
+
- NVDA (Windows) - [Download](https://www.nvaccess.org/download/)
|
|
38
|
+
- VoiceOver (macOS) - Built-in
|
|
39
|
+
- JAWS (Windows) - Commercial
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Test Procedures
|
|
44
|
+
|
|
45
|
+
### 1. Keyboard Navigation Test
|
|
46
|
+
|
|
47
|
+
**Objective:** Verify all interactive elements show visible focus indicators when navigated with keyboard.
|
|
48
|
+
|
|
49
|
+
**Steps:**
|
|
50
|
+
|
|
51
|
+
1. **Open the component/page** in your browser
|
|
52
|
+
2. **Click in the address bar** to ensure page loses focus
|
|
53
|
+
3. **Press Tab key** to start keyboard navigation
|
|
54
|
+
4. **Continue pressing Tab** through all interactive elements
|
|
55
|
+
5. **Press Shift+Tab** to navigate backwards
|
|
56
|
+
|
|
57
|
+
**Pass Criteria:**
|
|
58
|
+
- ✅ Every interactive element (buttons, inputs, links, selects) shows a visible outline when focused
|
|
59
|
+
- ✅ Focus indicator is clearly distinguishable from the non-focused state
|
|
60
|
+
- ✅ Disabled elements are focusable and show focus indicators
|
|
61
|
+
- ✅ Focus order follows logical reading order
|
|
62
|
+
|
|
63
|
+
**Common Issues:**
|
|
64
|
+
- ❌ No visible outline on focused elements
|
|
65
|
+
- ❌ Outline color matches background (invisible)
|
|
66
|
+
- ❌ Disabled elements skip in tab order (should NOT happen with aria-disabled pattern)
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### 2. Contrast Verification Test
|
|
71
|
+
|
|
72
|
+
**Objective:** Verify focus indicators meet 3:1 contrast ratio requirement.
|
|
73
|
+
|
|
74
|
+
#### Method 1: Chrome DevTools (Recommended)
|
|
75
|
+
|
|
76
|
+
**Steps:**
|
|
77
|
+
|
|
78
|
+
1. **Open DevTools** (F12 or Right-click → Inspect)
|
|
79
|
+
2. **Navigate to element** using Tab key until it's focused
|
|
80
|
+
3. **Click on the element** in the Elements panel
|
|
81
|
+
4. **In the Styles panel**, find the `:focus-visible` styles
|
|
82
|
+
5. **Click the color swatch** next to `outline-color`
|
|
83
|
+
6. **Check the Contrast Ratio section** in the color picker
|
|
84
|
+
|
|
85
|
+
**Pass Criteria:**
|
|
86
|
+
- ✅ Contrast ratio shows **≥ 3.0:1** against background
|
|
87
|
+
- ✅ Both AA and AAA indicators show checkmarks (for UI components)
|
|
88
|
+
|
|
89
|
+
**Example Screenshot Interpretation:**
|
|
90
|
+
```
|
|
91
|
+
Contrast Ratio:
|
|
92
|
+
AA ✓ 4.5 (Pass - Text contrast)
|
|
93
|
+
AAA ✓ 7.1 (Pass - Enhanced contrast)
|
|
94
|
+
|
|
95
|
+
UI Components:
|
|
96
|
+
AA ✓ 3.2 (Pass - Meets 3:1 minimum)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Method 2: axe DevTools Browser Extension
|
|
100
|
+
|
|
101
|
+
**Steps:**
|
|
102
|
+
|
|
103
|
+
1. **Install axe DevTools** browser extension
|
|
104
|
+
2. **Open the page** with your components
|
|
105
|
+
3. **Open DevTools** and select the **axe DevTools** tab
|
|
106
|
+
4. **Click "Scan ALL of my page"**
|
|
107
|
+
5. **Review Issues** section for focus indicator violations
|
|
108
|
+
|
|
109
|
+
**Pass Criteria:**
|
|
110
|
+
- ✅ No violations related to "Focus Visible" or "Color Contrast"
|
|
111
|
+
- ✅ Disabled elements appear in "Needs Review" (expected, manual verification needed)
|
|
112
|
+
|
|
113
|
+
#### Method 3: Manual Color Contrast Calculation
|
|
114
|
+
|
|
115
|
+
**Steps:**
|
|
116
|
+
|
|
117
|
+
1. **Identify focus indicator color** (e.g., from DevTools Computed styles)
|
|
118
|
+
2. **Identify background color** of the page/container
|
|
119
|
+
3. **Use WebAIM Contrast Checker:** https://webaim.org/resources/contrastchecker/
|
|
120
|
+
4. **Enter foreground color** (focus indicator color)
|
|
121
|
+
5. **Enter background color** (page/container background)
|
|
122
|
+
6. **Check "Graphical Objects and UI Components" section**
|
|
123
|
+
|
|
124
|
+
**Pass Criteria:**
|
|
125
|
+
- ✅ Contrast ratio shows **≥ 3:1** in the UI Components section
|
|
126
|
+
|
|
127
|
+
**Example Values:**
|
|
128
|
+
```
|
|
129
|
+
Foreground: #005fcc (example custom --focus-color)
|
|
130
|
+
Background: #ffffff (white)
|
|
131
|
+
Result: 8.59:1 - PASS ✓
|
|
132
|
+
|
|
133
|
+
Foreground: #666666 (disabled color, currentColor fallback)
|
|
134
|
+
Background: #ffffff (white)
|
|
135
|
+
Result: 4.54:1 - PASS ✓
|
|
136
|
+
|
|
137
|
+
Foreground: #666666 (disabled color)
|
|
138
|
+
Background: #999999 (gray container)
|
|
139
|
+
Result: 1.54:1 - FAIL ✗ (This is why --focus-color override is needed!)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### 3. Theme Testing
|
|
145
|
+
|
|
146
|
+
**Objective:** Verify focus indicators work correctly across different themes and backgrounds.
|
|
147
|
+
|
|
148
|
+
**Test Matrix:**
|
|
149
|
+
|
|
150
|
+
| Background | Focus Color | Expected Contrast | Pass/Fail |
|
|
151
|
+
|------------|-------------|-------------------|-----------|
|
|
152
|
+
| `#ffffff` (white) | `currentColor` (#666) | 4.54:1 | ✅ Pass |
|
|
153
|
+
| `#ffffff` (white) | `--focus-color` (#005fcc) | 8.59:1 | ✅ Pass |
|
|
154
|
+
| `#f5f5f5` (light gray) | `currentColor` (#666) | 3.84:1 | ✅ Pass |
|
|
155
|
+
| `#999999` (gray) | `currentColor` (#666) | 1.54:1 | ⚠️ **Needs --focus-color** |
|
|
156
|
+
| `#333333` (dark) | `currentColor` (#666) | 2.4:1 | ⚠️ **Needs --focus-color** |
|
|
157
|
+
|
|
158
|
+
**Steps:**
|
|
159
|
+
|
|
160
|
+
1. **Test on white background** (default theme)
|
|
161
|
+
- Navigate with Tab
|
|
162
|
+
- Verify focus indicator is visible
|
|
163
|
+
- Measure contrast with DevTools
|
|
164
|
+
|
|
165
|
+
2. **Test on gray backgrounds** (if your theme uses gray containers)
|
|
166
|
+
- Add test component to gray container
|
|
167
|
+
- Navigate with Tab
|
|
168
|
+
- Measure contrast - if < 3:1, add `--focus-color` override
|
|
169
|
+
|
|
170
|
+
3. **Test in dark mode** (if your theme supports it)
|
|
171
|
+
- Switch to dark mode
|
|
172
|
+
- Verify focus indicators are visible
|
|
173
|
+
- Measure contrast against dark backgrounds
|
|
174
|
+
|
|
175
|
+
**Pass Criteria:**
|
|
176
|
+
- ✅ All backgrounds show focus indicators with ≥ 3:1 contrast
|
|
177
|
+
- ✅ Custom themes define `--focus-color` when needed
|
|
178
|
+
|
|
179
|
+
**Fix for Failing Themes:**
|
|
180
|
+
|
|
181
|
+
If contrast fails on certain backgrounds, add theme-specific `--focus-color`:
|
|
182
|
+
|
|
183
|
+
```css
|
|
184
|
+
/* Light theme (default) - currentColor (#666) works fine */
|
|
185
|
+
:root {
|
|
186
|
+
/* No override needed, currentColor provides 4.54:1 on white */
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Gray theme - needs custom focus color */
|
|
190
|
+
.theme-gray {
|
|
191
|
+
--focus-color: #005fcc; /* 8.59:1 on white, 3.2:1 on #999 */
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Dark theme - needs lighter focus color */
|
|
195
|
+
.theme-dark {
|
|
196
|
+
--focus-color: #4da6ff; /* Lighter blue for dark backgrounds */
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
### 4. Disabled Element Focus Test
|
|
203
|
+
|
|
204
|
+
**Objective:** Verify disabled elements remain focusable and show focus indicators (aria-disabled pattern).
|
|
205
|
+
|
|
206
|
+
**Steps:**
|
|
207
|
+
|
|
208
|
+
1. **Create test form** with disabled elements:
|
|
209
|
+
```tsx
|
|
210
|
+
<form>
|
|
211
|
+
<Input id="name" name="name" placeholder="Name" />
|
|
212
|
+
<Input id="email" name="email" disabled={true} placeholder="Email (disabled)" />
|
|
213
|
+
<Button type="button" disabled={true}>Disabled Button</Button>
|
|
214
|
+
<Button type="submit">Submit</Button>
|
|
215
|
+
</form>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
2. **Press Tab** to navigate through the form
|
|
219
|
+
3. **Verify disabled elements receive focus** (tab stops on them)
|
|
220
|
+
4. **Verify focus indicator is visible** on disabled elements
|
|
221
|
+
5. **Measure contrast** of focus indicator on disabled elements
|
|
222
|
+
|
|
223
|
+
**Pass Criteria:**
|
|
224
|
+
- ✅ Disabled elements receive keyboard focus (tab stops on them)
|
|
225
|
+
- ✅ Focus indicator is visible with ≥ 3:1 contrast
|
|
226
|
+
- ✅ Disabled elements have `.is-disabled` class or `aria-disabled="true"`
|
|
227
|
+
- ✅ Visual disabled styling (opacity, color) is applied
|
|
228
|
+
- ✅ Clicking or pressing Enter/Space does NOT trigger actions
|
|
229
|
+
|
|
230
|
+
**Common Issues:**
|
|
231
|
+
- ❌ Disabled elements not in tab order (indicates native `disabled` attribute used instead of `aria-disabled`)
|
|
232
|
+
- ❌ No focus indicator on disabled elements
|
|
233
|
+
- ❌ Focus indicator has insufficient contrast due to disabled opacity
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### 5. Screen Reader Announcement Test
|
|
238
|
+
|
|
239
|
+
**Objective:** Verify screen readers properly announce focus indicators and disabled states.
|
|
240
|
+
|
|
241
|
+
#### Windows - NVDA
|
|
242
|
+
|
|
243
|
+
**Steps:**
|
|
244
|
+
|
|
245
|
+
1. **Start NVDA** (Control+Alt+N)
|
|
246
|
+
2. **Navigate to test page**
|
|
247
|
+
3. **Press Tab** to focus on disabled element
|
|
248
|
+
4. **Listen for announcement**
|
|
249
|
+
|
|
250
|
+
**Expected Announcement:**
|
|
251
|
+
```
|
|
252
|
+
"Email, edit, disabled, blank"
|
|
253
|
+
or
|
|
254
|
+
"Submit, button, disabled"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Key Commands:**
|
|
258
|
+
- `Tab` - Navigate to next element
|
|
259
|
+
- `Shift+Tab` - Navigate to previous element
|
|
260
|
+
- `Insert+↓` - Read current element
|
|
261
|
+
- `Insert+F7` - List all form fields
|
|
262
|
+
|
|
263
|
+
**Pass Criteria:**
|
|
264
|
+
- ✅ Screen reader announces "disabled" state
|
|
265
|
+
- ✅ Element type is announced (button, edit, etc.)
|
|
266
|
+
- ✅ Label or placeholder is announced
|
|
267
|
+
|
|
268
|
+
#### macOS - VoiceOver
|
|
269
|
+
|
|
270
|
+
**Steps:**
|
|
271
|
+
|
|
272
|
+
1. **Start VoiceOver** (Cmd+F5)
|
|
273
|
+
2. **Navigate to test page**
|
|
274
|
+
3. **Press Tab** to focus on disabled element
|
|
275
|
+
4. **Listen for announcement**
|
|
276
|
+
|
|
277
|
+
**Expected Announcement:**
|
|
278
|
+
```
|
|
279
|
+
"Email, dimmed, edit text"
|
|
280
|
+
or
|
|
281
|
+
"Submit, dimmed, button"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Key Commands:**
|
|
285
|
+
- `Tab` - Navigate to next element
|
|
286
|
+
- `Shift+Tab` - Navigate to previous element
|
|
287
|
+
- `Control+Option+A` - Read element attributes
|
|
288
|
+
- `Control+Option+Shift+H` - Read hint
|
|
289
|
+
|
|
290
|
+
**Pass Criteria:**
|
|
291
|
+
- ✅ Screen reader announces "dimmed" or "disabled" state
|
|
292
|
+
- ✅ Element type is announced (button, edit text, etc.)
|
|
293
|
+
- ✅ Label or placeholder is announced
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Testing Checklist
|
|
298
|
+
|
|
299
|
+
Use this checklist for comprehensive focus indicator testing:
|
|
300
|
+
|
|
301
|
+
### Basic Functionality
|
|
302
|
+
- [ ] All interactive elements are keyboard accessible
|
|
303
|
+
- [ ] Tab key navigates through all elements in logical order
|
|
304
|
+
- [ ] Shift+Tab navigates backwards
|
|
305
|
+
- [ ] Disabled elements remain in tab order (aria-disabled pattern)
|
|
306
|
+
- [ ] Focus indicator is visible on all focused elements
|
|
307
|
+
|
|
308
|
+
### Contrast Requirements
|
|
309
|
+
- [ ] Focus indicator has ≥ 3:1 contrast against white background
|
|
310
|
+
- [ ] Focus indicator has ≥ 3:1 contrast against light gray backgrounds
|
|
311
|
+
- [ ] Focus indicator has ≥ 3:1 contrast against dark backgrounds (if applicable)
|
|
312
|
+
- [ ] Focus indicator is visible on disabled elements
|
|
313
|
+
- [ ] No information conveyed by focus indicator color alone
|
|
314
|
+
|
|
315
|
+
### Theme Compatibility
|
|
316
|
+
- [ ] Focus indicator works in default light theme
|
|
317
|
+
- [ ] Focus indicator works in dark mode (if supported)
|
|
318
|
+
- [ ] Custom themes define `--focus-color` when needed
|
|
319
|
+
- [ ] Focus indicator contrast verified with DevTools
|
|
320
|
+
|
|
321
|
+
### Screen Reader Compatibility
|
|
322
|
+
- [ ] NVDA announces disabled state correctly
|
|
323
|
+
- [ ] VoiceOver announces disabled state correctly
|
|
324
|
+
- [ ] Element labels are read by screen readers
|
|
325
|
+
- [ ] Form structure is understandable without visual cues
|
|
326
|
+
|
|
327
|
+
### Component-Specific Tests
|
|
328
|
+
- [ ] Buttons: Visible focus indicator, "disabled" announced
|
|
329
|
+
- [ ] Inputs: Visible focus indicator, "disabled" announced
|
|
330
|
+
- [ ] Selects: Visible focus indicator, "disabled" announced
|
|
331
|
+
- [ ] Textareas: Visible focus indicator, "disabled" announced
|
|
332
|
+
- [ ] Links: Visible focus indicator (if used in disabled state)
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Reporting Issues
|
|
337
|
+
|
|
338
|
+
When reporting focus indicator issues, include:
|
|
339
|
+
|
|
340
|
+
1. **Component name and state** (e.g., "Button, disabled state")
|
|
341
|
+
2. **Browser and version** (e.g., "Chrome 120")
|
|
342
|
+
3. **Theme/background** (e.g., "Gray container background #999999")
|
|
343
|
+
4. **Measured contrast ratio** (e.g., "1.54:1 - FAIL")
|
|
344
|
+
5. **Screenshot** showing the focused element
|
|
345
|
+
6. **DevTools screenshot** showing the contrast measurement
|
|
346
|
+
|
|
347
|
+
**Example Issue Report:**
|
|
348
|
+
|
|
349
|
+
```markdown
|
|
350
|
+
## Focus Indicator Contrast Failure
|
|
351
|
+
|
|
352
|
+
**Component:** Input (disabled)
|
|
353
|
+
**Browser:** Chrome 120
|
|
354
|
+
**Background:** Gray container (#999999)
|
|
355
|
+
**Measured Contrast:** 1.54:1 (FAIL - requires 3:1 minimum)
|
|
356
|
+
|
|
357
|
+
**Screenshot:** [attach screenshot]
|
|
358
|
+
|
|
359
|
+
**Recommendation:** Add custom --focus-color for gray theme:
|
|
360
|
+
.theme-gray {
|
|
361
|
+
--focus-color: #005fcc; /* Provides 3.2:1 on gray */
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Automated Testing Recommendations
|
|
368
|
+
|
|
369
|
+
While this guide focuses on manual testing, consider adding these automated tests:
|
|
370
|
+
|
|
371
|
+
### jest-axe Integration
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
375
|
+
|
|
376
|
+
expect.extend(toHaveNoViolations);
|
|
377
|
+
|
|
378
|
+
describe('Focus Indicator Accessibility', () => {
|
|
379
|
+
it('should not have focus indicator violations', async () => {
|
|
380
|
+
const { container } = render(
|
|
381
|
+
<Button type="button" disabled={true}>
|
|
382
|
+
Disabled Button
|
|
383
|
+
</Button>
|
|
384
|
+
);
|
|
385
|
+
const results = await axe(container);
|
|
386
|
+
expect(results).toHaveNoViolations();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Keyboard Navigation Test
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
it('should be focusable when disabled', () => {
|
|
395
|
+
render(<Button type="button" disabled={true}>Disabled</Button>);
|
|
396
|
+
const button = screen.getByRole('button');
|
|
397
|
+
|
|
398
|
+
button.focus();
|
|
399
|
+
expect(button).toHaveFocus();
|
|
400
|
+
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Visual Regression Testing
|
|
405
|
+
|
|
406
|
+
Consider using tools like:
|
|
407
|
+
- **Chromatic** - Visual regression testing for Storybook
|
|
408
|
+
- **Percy** - Visual testing platform
|
|
409
|
+
- **BackstopJS** - Visual regression testing
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Additional Resources
|
|
414
|
+
|
|
415
|
+
### WCAG References
|
|
416
|
+
- [WCAG 2.4.7 Focus Visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible)
|
|
417
|
+
- [WCAG 2.1.1 Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard)
|
|
418
|
+
- [WCAG 1.4.11 Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast)
|
|
419
|
+
|
|
420
|
+
### Tools
|
|
421
|
+
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
|
422
|
+
- [Chrome DevTools](https://developer.chrome.com/docs/devtools/)
|
|
423
|
+
- [axe DevTools Extension](https://www.deque.com/axe/devtools/)
|
|
424
|
+
- [WAVE Browser Extension](https://wave.webaim.org/extension/)
|
|
425
|
+
|
|
426
|
+
### Articles
|
|
427
|
+
- [Understanding Focus Indicators](https://www.sarasoueidan.com/blog/focus-indicators/)
|
|
428
|
+
- [Accessible Focus Indicators](https://www.deque.com/blog/give-site-focus-tips-designing-usable-focus-indicators/)
|
|
429
|
+
- [The :focus-visible Selector](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible)
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Changelog
|
|
434
|
+
|
|
435
|
+
| Date | Version | Changes |
|
|
436
|
+
|------|---------|---------|
|
|
437
|
+
| 2025-11-02 | 1.0.0 | Initial focus indicator testing guide |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as a$1 } from './chunk-
|
|
1
|
+
import { a as a$1 } from './chunk-KVKQLRJG.js';
|
|
2
2
|
import { a } from './chunk-HHLNOC5T.js';
|
|
3
3
|
import e from 'react';
|
|
4
4
|
|
|
@@ -6,4 +6,4 @@ var l=({id:n,children:a$1,classes:r,modalRef:d,openOnMount:i,...t})=>e.createEle
|
|
|
6
6
|
|
|
7
7
|
export { u as a };
|
|
8
8
|
//# sourceMappingURL=out.js.map
|
|
9
|
-
//# sourceMappingURL=chunk-
|
|
9
|
+
//# sourceMappingURL=chunk-43TK2ICH.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkENTCUJ3A_cjs = require('./chunk-ENTCUJ3A.cjs');
|
|
4
|
+
var o = require('react');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var o__default = /*#__PURE__*/_interopDefault(o);
|
|
9
|
+
|
|
10
|
+
var i=o__default.default.forwardRef(({href:e,target:n,rel:r,children:t,styles:k,prefetch:s=!1,btnStyle:m,onClick:L,onPointerDown:d,...c},l)=>{let u=o__default.default.useMemo(()=>{if(n==="_blank"){let f=new Set(["noopener","noreferrer"]);return s&&f.add("prefetch"),r&&r.split(/\s+/).forEach(p=>{p&&f.add(p);}),Array.from(f).join(" ")}return r},[n,r,s]);return o__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"a",ref:l,href:e,target:n,rel:u,styles:k,"data-btn":m,onClick:L,onPointerDown:d,...c},t)}),y=o__default.default.forwardRef(({href:e,icon:n,...r},t)=>o__default.default.createElement(i,{ref:t,href:e,...r},n)),h=o__default.default.forwardRef(({href:e,children:n,...r},t)=>o__default.default.createElement(i,{ref:t,href:e,...r},n));y.displayName="IconLink";h.displayName="LinkButton";i.displayName="Link";var x=i;
|
|
11
|
+
|
|
12
|
+
exports.a = i;
|
|
13
|
+
exports.b = y;
|
|
14
|
+
exports.c = h;
|
|
15
|
+
exports.d = x;
|
|
16
|
+
//# sourceMappingURL=out.js.map
|
|
17
|
+
//# sourceMappingURL=chunk-5PJYLVFY.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/link/link.tsx"],"names":["React","Link","href","target","rel","children","styles","prefetch","btnStyle","onClick","onPointerDown","props","ref","computedRel","securityTokens","token","ui_default","IconLink","icon","LinkButton","link_default"],"mappings":"oCAAA,OAAOA,MAAW,QA+FX,IAAMC,EAAOD,EAAM,WACxB,CACE,CACE,KAAAE,EACA,OAAAC,EACA,IAAAC,EACA,SAAAC,EACA,OAAAC,EACA,SAAAC,EAAW,GACX,SAAAC,EACA,QAAAC,EACA,cAAAC,EACA,GAAGC,CACL,EACAC,IACG,CAWH,IAAMC,EAAcb,EAAM,QAAQ,IAAM,CACtC,GAAIG,IAAW,SAAU,CAEvB,IAAMW,EAAiB,IAAI,IAAI,CAAC,WAAY,YAAY,CAAC,EAGzD,OAAIP,GACFO,EAAe,IAAI,UAAU,EAI3BV,GACFA,EAAI,MAAM,KAAK,EAAE,QAASW,GAAU,CAC9BA,GAAOD,EAAe,IAAIC,CAAK,CACrC,CAAC,EAGI,MAAM,KAAKD,CAAc,EAAE,KAAK,GAAG,CAC5C,CAGA,OAAOV,CACT,EAAG,CAACD,EAAQC,EAAKG,CAAQ,CAAC,EAE1B,OACEP,EAAA,cAACgB,EAAA,CACC,GAAG,IACH,IAAKJ,EACL,KAAMV,EACN,OAAQC,EACR,IAAKU,EACL,OAAQP,EACR,WAAUE,EACV,QAASC,EACT,cAAeC,EACd,GAAGC,GAEHN,CACH,CAEJ,CACF,EAEaY,EAAWjB,EAAM,WAC5B,CAAC,CAAE,KAAAE,EAAM,KAAAgB,EAAM,GAAGP,CAAM,EAAGC,IAEvBZ,EAAA,cAACC,EAAA,CAAK,IAAKW,EAAK,KAAMV,EAAO,GAAGS,GAC7BO,CACH,CAGN,EAEaC,EAAanB,EAAM,WAC9B,CAAC,CAAE,KAAAE,EAAM,SAAAG,EAAU,GAAGM,CAAM,EAAGC,IAE3BZ,EAAA,cAACC,EAAA,CAAK,IAAKW,EAAK,KAAMV,EAAO,GAAGS,GAC7BN,CACH,CAGN,EAEAY,EAAS,YAAc,WACvBE,EAAW,YAAc,aACzBlB,EAAK,YAAc,OAEnB,IAAOmB,EAAQnB","sourcesContent":["import React from \"react\";\nimport UI from \"../ui\";\nimport type { LinkProps } from \"./link.types\";\n\n/**\n * Link - A semantic, accessible anchor component with enhanced security and styling.\n *\n * The Link component renders accessible `<a>` elements with automatic security\n * attributes for external links, customizable styling variants, and full WCAG 2.1\n * AA compliance. It supports traditional text links, button-styled links, and\n * programmatic focus management via ref forwarding.\n *\n * ## Features\n *\n * - 🔒 **Automatic Security**: External links get `rel=\"noopener noreferrer\"`\n * - ♿ **WCAG 2.1 AA Compliant**: Accessible focus indicators and semantic HTML\n * - 🎨 **Flexible Styling**: Text links, button links, and pill variants\n * - ⚡ **Performance**: Optional prefetch hints for faster navigation\n * - 🎯 **Ref Forwarding**: Direct DOM access for focus management and scroll\n * - 🧪 **Type-Safe**: Full TypeScript support with comprehensive prop types\n *\n * ## Accessibility\n *\n * - ✅ Semantic `<a>` element for proper keyboard navigation\n * - ✅ Focus indicators meet WCAG 2.4.7 (3:1 contrast ratio)\n * - ✅ Screen readers announce link purpose and destination\n * - ✅ External links include security attributes automatically\n * - ✅ Supports `aria-label` for icon-only or ambiguous links\n * - ✅ Ref forwarding enables skip-link patterns\n *\n * @example\n * // Basic internal link\n * <Link href=\"/about\">About Us</Link>\n *\n * @example\n * // External link with automatic security\n * <Link href=\"https://example.com\" target=\"_blank\">\n * Visit Example\n * </Link>\n *\n * @example\n * // Button-styled call-to-action link\n * <Link href=\"/signup\">\n * <b>Get Started</b>\n * </Link>\n *\n * @example\n * // Icon-only link with accessible label\n * <Link href=\"/settings\" aria-label=\"Open settings\">\n * <SettingsIcon aria-hidden=\"true\" />\n * </Link>\n *\n * @example\n * // Analytics tracking with onClick (includes keyboard users)\n * <Link\n * href=\"/products\"\n * onClick={(e) => trackEvent('link_click', { href: '/products' })}\n * >\n * Browse Products\n * </Link>\n *\n * @example\n * // Skip link with ref forwarding for focus management\n * const mainRef = useRef<HTMLAnchorElement>(null);\n *\n * <Link ref={mainRef} href=\"#main-content\">\n * Skip to main content\n * </Link>\n *\n * @example\n * // Custom styled link with CSS variables\n * <Link\n * href=\"/products\"\n * styles={{\n * '--link-color': '#0066cc',\n * '--link-decoration': 'underline',\n * }}\n * >\n * Browse Products\n * </Link>\n *\n * @example\n * // ✅ GOOD: Descriptive link text\n * <Link href=\"/docs/installation\">\n * Read installation guide\n * </Link>\n *\n * @example\n * // ❌ BAD: Generic link text (poor for screen readers)\n * <Link href=\"/docs/installation\">\n * Click here\n * </Link>\n *\n * @see {@link LinkProps} for complete prop documentation\n */\nexport const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(\n (\n {\n href,\n target,\n rel,\n children,\n styles,\n prefetch = false,\n btnStyle,\n onClick,\n onPointerDown,\n ...props\n },\n ref\n ) => {\n /**\n * Compute the final `rel` attribute value with security defaults.\n *\n * For external links (target=\"_blank\"), we merge user-provided `rel` values\n * with security defaults `noopener noreferrer` to prevent:\n * - window.opener exploitation (noopener)\n * - Referrer header leakage (noreferrer)\n *\n * If prefetch is enabled, we also add the `prefetch` hint.\n */\n const computedRel = React.useMemo(() => {\n if (target === \"_blank\") {\n // Start with security defaults\n const securityTokens = new Set([\"noopener\", \"noreferrer\"]);\n\n // Add prefetch if enabled\n if (prefetch) {\n securityTokens.add(\"prefetch\");\n }\n\n // Merge with user-provided rel tokens (if any)\n if (rel) {\n rel.split(/\\s+/).forEach((token) => {\n if (token) securityTokens.add(token);\n });\n }\n\n return Array.from(securityTokens).join(\" \");\n }\n\n // For non-external links, use provided rel as-is\n return rel;\n }, [target, rel, prefetch]);\n\n return (\n <UI\n as=\"a\"\n ref={ref}\n href={href}\n target={target}\n rel={computedRel}\n styles={styles}\n data-btn={btnStyle}\n onClick={onClick}\n onPointerDown={onPointerDown}\n {...props}\n >\n {children}\n </UI>\n );\n }\n);\n\nexport const IconLink = React.forwardRef<HTMLAnchorElement, LinkProps>(\n ({ href, icon, ...props }, ref) => {\n return (\n <Link ref={ref} href={href} {...props}>\n {icon}\n </Link>\n );\n }\n);\n\nexport const LinkButton = React.forwardRef<HTMLAnchorElement, LinkProps>(\n ({ href, children, ...props }, ref) => {\n return (\n <Link ref={ref} href={href} {...props}>\n {children}\n </Link>\n );\n }\n);\n\nIconLink.displayName = \"IconLink\";\nLinkButton.displayName = \"LinkButton\";\nLink.displayName = \"Link\";\n\nexport default Link;\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkPNWIRCG3_cjs = require('./chunk-PNWIRCG3.cjs');
|
|
4
|
+
var chunkTON2YGMD_cjs = require('./chunk-TON2YGMD.cjs');
|
|
5
|
+
var chunkENTCUJ3A_cjs = require('./chunk-ENTCUJ3A.cjs');
|
|
6
|
+
var D = require('react');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var D__default = /*#__PURE__*/_interopDefault(D);
|
|
11
|
+
|
|
12
|
+
var r=({type:a="button",children:n,styles:p,disabled:i,isDisabled:l,classes:m,onPointerDown:u,onPointerOver:d,onPointerLeave:b,onClick:P,onKeyDown:c,...y})=>{let B=chunkPNWIRCG3_cjs.a(i,l),{disabledProps:t,handlers:f}=chunkTON2YGMD_cjs.a(B,{handlers:{onClick:P,onPointerDown:u,onKeyDown:c},className:m});return D__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"button",type:a,"aria-disabled":t["aria-disabled"],onPointerOver:d,onPointerLeave:b,style:p,className:t.className,...f,...y},n)};var h=r;r.displayName="Button";
|
|
13
|
+
|
|
14
|
+
exports.a = r;
|
|
15
|
+
exports.b = h;
|
|
16
|
+
//# sourceMappingURL=out.js.map
|
|
17
|
+
//# sourceMappingURL=chunk-E4OSROCA.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/buttons/button.tsx"],"names":["React","Button","type","children","styles","disabled","isDisabled","classes","onPointerDown","onPointerOver","onPointerLeave","onClick","onKeyDown","props","isActuallyDisabled","resolveDisabledState","disabledProps","handlers","useDisabledState","ui_default","button_default"],"mappings":"2HACA,OAAOA,MAAW,QAmEX,IAAMC,EAAS,CAAC,CACrB,KAAAC,EAAO,SACP,SAAAC,EACA,OAAAC,EACA,SAAAC,EACA,WAAAC,EACA,QAAAC,EACA,cAAAC,EACA,cAAAC,EACA,eAAAC,EACA,QAAAC,EACA,UAAAC,EACA,GAAGC,CACL,IAAmB,CAEjB,IAAMC,EAAqBC,EAAqBV,EAAUC,CAAU,EAG9D,CAAE,cAAAU,EAAe,SAAAC,CAAS,EAAIC,EAClCJ,EACA,CACE,SAAU,CACR,QAAAH,EACA,cAAAH,EACA,UAAAI,CACF,EAEA,UAAWL,CAGb,CACF,EAGA,OACEP,EAAA,cAACmB,EAAA,CACC,GAAG,SACH,KAAMjB,EACN,gBAAec,EAAc,eAAe,EAC5C,cAAeP,EACf,eAAgBC,EAChB,MAAON,EACP,UAAWY,EAAc,UACxB,GAAGC,EACH,GAAGJ,GAEHV,CACH,CAEJ,EAMA,IAAOiB,EAAQnB,EACfA,EAAO,YAAc","sourcesContent":["import UI from \"../ui\";\nimport React from \"react\";\nimport { useDisabledState } from \"../../hooks/use-disabled-state\";\nimport { resolveDisabledState } from \"../../utils/accessibility\";\nimport type { DisabledStateProps } from \"../../types/shared\";\n\nexport type ButtonProps = Partial<React.ComponentProps<typeof UI>> &\n DisabledStateProps & {\n /**\n * The button type\n * Required - 'button' | 'submit' | 'reset'\n */\n type: \"button\" | \"submit\" | \"reset\";\n };\n\n/**\n * Accessible Button component with WCAG 2.1 Level AA compliant disabled state.\n *\n * **Key Accessibility Features:**\n * - Uses `aria-disabled` pattern instead of native `disabled` attribute\n * - Maintains keyboard focusability when disabled (stays in tab order)\n * - Prevents all interactions when disabled via optimized `useDisabledState` hook\n * - Automatic className merging for seamless styling\n * - Supports both modern `disabled` and legacy `isDisabled` props\n *\n * **Why aria-disabled?**\n * - Elements remain in keyboard tab order (WCAG 2.1.1 - Keyboard)\n * - Screen readers can discover and announce disabled state (WCAG 4.1.2)\n * - Enables tooltips and help text on disabled buttons\n * - Better visual styling control for WCAG AA contrast compliance\n *\n * **Performance:**\n * - Uses optimized `useDisabledState` hook with stable references\n * - Automatic className merging eliminates boilerplate\n * - ~90% reduction in unnecessary re-renders compared to previous implementation\n *\n * @example\n * // Basic usage\n * <Button type=\"button\" onClick={handleClick}>\n * Click me\n * </Button>\n *\n * @example\n * // Disabled state (prevents all interactions but stays focusable)\n * <Button type=\"button\" disabled={true} onClick={handleClick}>\n * Cannot click (but can focus for screen readers)\n * </Button>\n *\n * @example\n * // With custom classes (automatic merging with .is-disabled)\n * <Button\n * type=\"button\"\n * disabled={true}\n * classes=\"my-custom-btn\"\n * >\n * Custom disabled button\n * </Button>\n *\n * @example\n * // Legacy isDisabled prop (still supported)\n * <Button type=\"button\" isDisabled={true} onClick={handleClick}>\n * Legacy disabled\n * </Button>\n *\n * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/keyboard WCAG 2.1.1 - Keyboard}\n * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/name-role-value WCAG 4.1.2 - Name, Role, Value}\n * @see {@link file://./../../hooks/useDisabledState.md useDisabledState Hook Documentation}\n */\nexport const Button = ({\n type = \"button\",\n children,\n styles,\n disabled,\n isDisabled,\n classes,\n onPointerDown,\n onPointerOver,\n onPointerLeave,\n onClick,\n onKeyDown,\n ...props\n}: ButtonProps) => {\n // Resolve disabled state from both props (disabled takes precedence)\n const isActuallyDisabled = resolveDisabledState(disabled, isDisabled);\n\n // Use the disabled state hook with enhanced API for automatic className merging\n const { disabledProps, handlers } = useDisabledState<HTMLButtonElement>(\n isActuallyDisabled,\n {\n handlers: {\n onClick,\n onPointerDown,\n onKeyDown,\n },\n // Automatic className merging - hook combines disabled class with user classes\n className: classes,\n // Note: onPointerOver and onPointerLeave are intentionally NOT wrapped\n // to allow hover effects on disabled buttons for visual feedback\n }\n );\n\n /* Returning a button element with accessible disabled state */\n return (\n <UI\n as=\"button\"\n type={type}\n aria-disabled={disabledProps[\"aria-disabled\"]}\n onPointerOver={onPointerOver}\n onPointerLeave={onPointerLeave}\n style={styles}\n className={disabledProps.className}\n {...handlers}\n {...props}\n >\n {children}\n </UI>\n );\n};\n\nexport const IconButton = ({ icon, ...props }: ButtonProps) => {\n return <Button {...props}>{icon}</Button>;\n};\n\nexport default Button;\nButton.displayName = \"Button\";\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { a } from './chunk-BFK62VX5.js';
|
|
2
|
+
import { a as a$1 } from './chunk-75QHTLFO.js';
|
|
3
|
+
import { a as a$2 } from './chunk-HHLNOC5T.js';
|
|
4
|
+
import D from 'react';
|
|
5
|
+
|
|
6
|
+
var r=({type:a$3="button",children:n,styles:p,disabled:i,isDisabled:l,classes:m,onPointerDown:u,onPointerOver:d,onPointerLeave:b,onClick:P,onKeyDown:c,...y})=>{let B=a(i,l),{disabledProps:t,handlers:f}=a$1(B,{handlers:{onClick:P,onPointerDown:u,onKeyDown:c},className:m});return D.createElement(a$2,{as:"button",type:a$3,"aria-disabled":t["aria-disabled"],onPointerOver:d,onPointerLeave:b,style:p,className:t.className,...f,...y},n)};var h=r;r.displayName="Button";
|
|
7
|
+
|
|
8
|
+
export { r as a, h as b };
|
|
9
|
+
//# sourceMappingURL=out.js.map
|
|
10
|
+
//# sourceMappingURL=chunk-KVKQLRJG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/buttons/button.tsx"],"names":["React","Button","type","children","styles","disabled","isDisabled","classes","onPointerDown","onPointerOver","onPointerLeave","onClick","onKeyDown","props","isActuallyDisabled","resolveDisabledState","disabledProps","handlers","useDisabledState","ui_default","button_default"],"mappings":"wHACA,OAAOA,MAAW,QAmEX,IAAMC,EAAS,CAAC,CACrB,KAAAC,EAAO,SACP,SAAAC,EACA,OAAAC,EACA,SAAAC,EACA,WAAAC,EACA,QAAAC,EACA,cAAAC,EACA,cAAAC,EACA,eAAAC,EACA,QAAAC,EACA,UAAAC,EACA,GAAGC,CACL,IAAmB,CAEjB,IAAMC,EAAqBC,EAAqBV,EAAUC,CAAU,EAG9D,CAAE,cAAAU,EAAe,SAAAC,CAAS,EAAIC,EAClCJ,EACA,CACE,SAAU,CACR,QAAAH,EACA,cAAAH,EACA,UAAAI,CACF,EAEA,UAAWL,CAGb,CACF,EAGA,OACEP,EAAA,cAACmB,EAAA,CACC,GAAG,SACH,KAAMjB,EACN,gBAAec,EAAc,eAAe,EAC5C,cAAeP,EACf,eAAgBC,EAChB,MAAON,EACP,UAAWY,EAAc,UACxB,GAAGC,EACH,GAAGJ,GAEHV,CACH,CAEJ,EAMA,IAAOiB,EAAQnB,EACfA,EAAO,YAAc","sourcesContent":["import UI from \"../ui\";\nimport React from \"react\";\nimport { useDisabledState } from \"../../hooks/use-disabled-state\";\nimport { resolveDisabledState } from \"../../utils/accessibility\";\nimport type { DisabledStateProps } from \"../../types/shared\";\n\nexport type ButtonProps = Partial<React.ComponentProps<typeof UI>> &\n DisabledStateProps & {\n /**\n * The button type\n * Required - 'button' | 'submit' | 'reset'\n */\n type: \"button\" | \"submit\" | \"reset\";\n };\n\n/**\n * Accessible Button component with WCAG 2.1 Level AA compliant disabled state.\n *\n * **Key Accessibility Features:**\n * - Uses `aria-disabled` pattern instead of native `disabled` attribute\n * - Maintains keyboard focusability when disabled (stays in tab order)\n * - Prevents all interactions when disabled via optimized `useDisabledState` hook\n * - Automatic className merging for seamless styling\n * - Supports both modern `disabled` and legacy `isDisabled` props\n *\n * **Why aria-disabled?**\n * - Elements remain in keyboard tab order (WCAG 2.1.1 - Keyboard)\n * - Screen readers can discover and announce disabled state (WCAG 4.1.2)\n * - Enables tooltips and help text on disabled buttons\n * - Better visual styling control for WCAG AA contrast compliance\n *\n * **Performance:**\n * - Uses optimized `useDisabledState` hook with stable references\n * - Automatic className merging eliminates boilerplate\n * - ~90% reduction in unnecessary re-renders compared to previous implementation\n *\n * @example\n * // Basic usage\n * <Button type=\"button\" onClick={handleClick}>\n * Click me\n * </Button>\n *\n * @example\n * // Disabled state (prevents all interactions but stays focusable)\n * <Button type=\"button\" disabled={true} onClick={handleClick}>\n * Cannot click (but can focus for screen readers)\n * </Button>\n *\n * @example\n * // With custom classes (automatic merging with .is-disabled)\n * <Button\n * type=\"button\"\n * disabled={true}\n * classes=\"my-custom-btn\"\n * >\n * Custom disabled button\n * </Button>\n *\n * @example\n * // Legacy isDisabled prop (still supported)\n * <Button type=\"button\" isDisabled={true} onClick={handleClick}>\n * Legacy disabled\n * </Button>\n *\n * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/keyboard WCAG 2.1.1 - Keyboard}\n * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/name-role-value WCAG 4.1.2 - Name, Role, Value}\n * @see {@link file://./../../hooks/useDisabledState.md useDisabledState Hook Documentation}\n */\nexport const Button = ({\n type = \"button\",\n children,\n styles,\n disabled,\n isDisabled,\n classes,\n onPointerDown,\n onPointerOver,\n onPointerLeave,\n onClick,\n onKeyDown,\n ...props\n}: ButtonProps) => {\n // Resolve disabled state from both props (disabled takes precedence)\n const isActuallyDisabled = resolveDisabledState(disabled, isDisabled);\n\n // Use the disabled state hook with enhanced API for automatic className merging\n const { disabledProps, handlers } = useDisabledState<HTMLButtonElement>(\n isActuallyDisabled,\n {\n handlers: {\n onClick,\n onPointerDown,\n onKeyDown,\n },\n // Automatic className merging - hook combines disabled class with user classes\n className: classes,\n // Note: onPointerOver and onPointerLeave are intentionally NOT wrapped\n // to allow hover effects on disabled buttons for visual feedback\n }\n );\n\n /* Returning a button element with accessible disabled state */\n return (\n <UI\n as=\"button\"\n type={type}\n aria-disabled={disabledProps[\"aria-disabled\"]}\n onPointerOver={onPointerOver}\n onPointerLeave={onPointerLeave}\n style={styles}\n className={disabledProps.className}\n {...handlers}\n {...props}\n >\n {children}\n </UI>\n );\n};\n\nexport const IconButton = ({ icon, ...props }: ButtonProps) => {\n return <Button {...props}>{icon}</Button>;\n};\n\nexport default Button;\nButton.displayName = \"Button\";\n"]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var chunk2NRIP6RB_cjs = require('./chunk-2NRIP6RB.cjs');
|
|
4
4
|
var chunk6WTC4JXH_cjs = require('./chunk-6WTC4JXH.cjs');
|
|
5
|
-
var
|
|
5
|
+
var chunkE4OSROCA_cjs = require('./chunk-E4OSROCA.cjs');
|
|
6
6
|
var chunkENTCUJ3A_cjs = require('./chunk-ENTCUJ3A.cjs');
|
|
7
7
|
var s = require('react');
|
|
8
8
|
|
|
@@ -10,9 +10,9 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
10
10
|
|
|
11
11
|
var s__default = /*#__PURE__*/_interopDefault(s);
|
|
12
12
|
|
|
13
|
-
var y=({dialogTitle:t,onClick:o,id:l,type:e="h3"})=>{let a=s.useCallback(()=>{o();},[o]);return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"div",classes:"dialog-header"},s__default.default.createElement(chunk2NRIP6RB_cjs.b,{type:e,className:"dialog-title",id:l},t||"Dialog"),s__default.default.createElement(
|
|
13
|
+
var y=({dialogTitle:t,onClick:o,id:l,type:e="h3"})=>{let a=s.useCallback(()=>{o();},[o]);return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"div",classes:"dialog-header"},s__default.default.createElement(chunk2NRIP6RB_cjs.b,{type:e,className:"dialog-title",id:l},t||"Dialog"),s__default.default.createElement(chunkE4OSROCA_cjs.b,{type:"button",onClick:a,className:"dialog-close","aria-label":"Close dialog","data-btn":"icon"},s__default.default.createElement(chunk6WTC4JXH_cjs.b,null,s__default.default.createElement(chunk6WTC4JXH_cjs.b.Remove,{size:16}))))},D=s__default.default.memo(y);y.displayName="DialogHeader";var H=({onClose:t,onConfirm:o,confirmLabel:l,cancelLabel:e})=>{let a=s.useCallback(()=>{t();},[t]),m=s.useCallback(()=>{o&&o();},[o]);return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"section",className:"dialog-footer"},e&&s__default.default.createElement(chunkE4OSROCA_cjs.b,{type:"button",onClick:a,className:"dialog-button button-secondary","data-btn":"sm"},e),o&&s__default.default.createElement(chunkE4OSROCA_cjs.b,{type:"button",onClick:m,className:"dialog-button button-primary","data-btn":"sm"},l))};H.displayName="DialogFooter";var I=s__default.default.memo(H);var h=(t,o)=>s.useCallback(e=>{let a=t.current?.getBoundingClientRect();a&&(e.clientY<a.top||e.clientY>a.bottom||e.clientX<a.left||e.clientX>a.right)&&o();},[t,o]);var F=({isOpen:t,onOpenChange:o,isAlertDialog:l=!1,onClose:e,dialogTitle:a,dialogLabel:m,children:P,onConfirm:U,confirmLabel:B="Confirm",cancelLabel:E="Cancel",className:M="",hideFooter:v=!1,styles:x})=>{let f=s.useRef(null),u=s.useId();s.useEffect(()=>{let r=f.current;r&&(t?l?r.show():r.showModal():r.close());},[t,l]);let c=s.useCallback(()=>{o(!1),e&&e();},[o,e]),L=h(f,c),b=s.useId();return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"dialog",role:l?"alertdialog":"dialog",ref:f,onClose:c,onClick:L,"aria-modal":t&&!l?"true":void 0,"aria-labelledby":u,"aria-describedby":b,"aria-label":m,className:`dialog-modal ${M}`.trim(),style:x},s__default.default.createElement(D,{dialogTitle:a,onClick:c,id:u}),s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"section",id:b,className:"dialog-content",onClick:r=>r.stopPropagation()},P,!v&&s__default.default.createElement(I,{onClose:c,onConfirm:U,confirmLabel:B,cancelLabel:E})))};F.displayName="Dialog";var ao=s__default.default.memo(F);
|
|
14
14
|
|
|
15
15
|
exports.a = F;
|
|
16
16
|
exports.b = ao;
|
|
17
17
|
//# sourceMappingURL=out.js.map
|
|
18
|
-
//# sourceMappingURL=chunk-
|
|
18
|
+
//# sourceMappingURL=chunk-MGPWZRBX.cjs.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { a } from './chunk-HHLNOC5T.js';
|
|
2
|
+
import o from 'react';
|
|
3
|
+
|
|
4
|
+
var i=o.forwardRef(({href:e,target:n,rel:r,children:t,styles:k,prefetch:s=!1,btnStyle:m,onClick:L,onPointerDown:d,...c},l)=>{let u=o.useMemo(()=>{if(n==="_blank"){let f=new Set(["noopener","noreferrer"]);return s&&f.add("prefetch"),r&&r.split(/\s+/).forEach(p=>{p&&f.add(p);}),Array.from(f).join(" ")}return r},[n,r,s]);return o.createElement(a,{as:"a",ref:l,href:e,target:n,rel:u,styles:k,"data-btn":m,onClick:L,onPointerDown:d,...c},t)}),y=o.forwardRef(({href:e,icon:n,...r},t)=>o.createElement(i,{ref:t,href:e,...r},n)),h=o.forwardRef(({href:e,children:n,...r},t)=>o.createElement(i,{ref:t,href:e,...r},n));y.displayName="IconLink";h.displayName="LinkButton";i.displayName="Link";var x=i;
|
|
5
|
+
|
|
6
|
+
export { i as a, y as b, h as c, x as d };
|
|
7
|
+
//# sourceMappingURL=out.js.map
|
|
8
|
+
//# sourceMappingURL=chunk-NNTBIHSD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/link/link.tsx"],"names":["React","Link","href","target","rel","children","styles","prefetch","btnStyle","onClick","onPointerDown","props","ref","computedRel","securityTokens","token","ui_default","IconLink","icon","LinkButton","link_default"],"mappings":"mCAAA,OAAOA,MAAW,QA+FX,IAAMC,EAAOD,EAAM,WACxB,CACE,CACE,KAAAE,EACA,OAAAC,EACA,IAAAC,EACA,SAAAC,EACA,OAAAC,EACA,SAAAC,EAAW,GACX,SAAAC,EACA,QAAAC,EACA,cAAAC,EACA,GAAGC,CACL,EACAC,IACG,CAWH,IAAMC,EAAcb,EAAM,QAAQ,IAAM,CACtC,GAAIG,IAAW,SAAU,CAEvB,IAAMW,EAAiB,IAAI,IAAI,CAAC,WAAY,YAAY,CAAC,EAGzD,OAAIP,GACFO,EAAe,IAAI,UAAU,EAI3BV,GACFA,EAAI,MAAM,KAAK,EAAE,QAASW,GAAU,CAC9BA,GAAOD,EAAe,IAAIC,CAAK,CACrC,CAAC,EAGI,MAAM,KAAKD,CAAc,EAAE,KAAK,GAAG,CAC5C,CAGA,OAAOV,CACT,EAAG,CAACD,EAAQC,EAAKG,CAAQ,CAAC,EAE1B,OACEP,EAAA,cAACgB,EAAA,CACC,GAAG,IACH,IAAKJ,EACL,KAAMV,EACN,OAAQC,EACR,IAAKU,EACL,OAAQP,EACR,WAAUE,EACV,QAASC,EACT,cAAeC,EACd,GAAGC,GAEHN,CACH,CAEJ,CACF,EAEaY,EAAWjB,EAAM,WAC5B,CAAC,CAAE,KAAAE,EAAM,KAAAgB,EAAM,GAAGP,CAAM,EAAGC,IAEvBZ,EAAA,cAACC,EAAA,CAAK,IAAKW,EAAK,KAAMV,EAAO,GAAGS,GAC7BO,CACH,CAGN,EAEaC,EAAanB,EAAM,WAC9B,CAAC,CAAE,KAAAE,EAAM,SAAAG,EAAU,GAAGM,CAAM,EAAGC,IAE3BZ,EAAA,cAACC,EAAA,CAAK,IAAKW,EAAK,KAAMV,EAAO,GAAGS,GAC7BN,CACH,CAGN,EAEAY,EAAS,YAAc,WACvBE,EAAW,YAAc,aACzBlB,EAAK,YAAc,OAEnB,IAAOmB,EAAQnB","sourcesContent":["import React from \"react\";\nimport UI from \"../ui\";\nimport type { LinkProps } from \"./link.types\";\n\n/**\n * Link - A semantic, accessible anchor component with enhanced security and styling.\n *\n * The Link component renders accessible `<a>` elements with automatic security\n * attributes for external links, customizable styling variants, and full WCAG 2.1\n * AA compliance. It supports traditional text links, button-styled links, and\n * programmatic focus management via ref forwarding.\n *\n * ## Features\n *\n * - 🔒 **Automatic Security**: External links get `rel=\"noopener noreferrer\"`\n * - ♿ **WCAG 2.1 AA Compliant**: Accessible focus indicators and semantic HTML\n * - 🎨 **Flexible Styling**: Text links, button links, and pill variants\n * - ⚡ **Performance**: Optional prefetch hints for faster navigation\n * - 🎯 **Ref Forwarding**: Direct DOM access for focus management and scroll\n * - 🧪 **Type-Safe**: Full TypeScript support with comprehensive prop types\n *\n * ## Accessibility\n *\n * - ✅ Semantic `<a>` element for proper keyboard navigation\n * - ✅ Focus indicators meet WCAG 2.4.7 (3:1 contrast ratio)\n * - ✅ Screen readers announce link purpose and destination\n * - ✅ External links include security attributes automatically\n * - ✅ Supports `aria-label` for icon-only or ambiguous links\n * - ✅ Ref forwarding enables skip-link patterns\n *\n * @example\n * // Basic internal link\n * <Link href=\"/about\">About Us</Link>\n *\n * @example\n * // External link with automatic security\n * <Link href=\"https://example.com\" target=\"_blank\">\n * Visit Example\n * </Link>\n *\n * @example\n * // Button-styled call-to-action link\n * <Link href=\"/signup\">\n * <b>Get Started</b>\n * </Link>\n *\n * @example\n * // Icon-only link with accessible label\n * <Link href=\"/settings\" aria-label=\"Open settings\">\n * <SettingsIcon aria-hidden=\"true\" />\n * </Link>\n *\n * @example\n * // Analytics tracking with onClick (includes keyboard users)\n * <Link\n * href=\"/products\"\n * onClick={(e) => trackEvent('link_click', { href: '/products' })}\n * >\n * Browse Products\n * </Link>\n *\n * @example\n * // Skip link with ref forwarding for focus management\n * const mainRef = useRef<HTMLAnchorElement>(null);\n *\n * <Link ref={mainRef} href=\"#main-content\">\n * Skip to main content\n * </Link>\n *\n * @example\n * // Custom styled link with CSS variables\n * <Link\n * href=\"/products\"\n * styles={{\n * '--link-color': '#0066cc',\n * '--link-decoration': 'underline',\n * }}\n * >\n * Browse Products\n * </Link>\n *\n * @example\n * // ✅ GOOD: Descriptive link text\n * <Link href=\"/docs/installation\">\n * Read installation guide\n * </Link>\n *\n * @example\n * // ❌ BAD: Generic link text (poor for screen readers)\n * <Link href=\"/docs/installation\">\n * Click here\n * </Link>\n *\n * @see {@link LinkProps} for complete prop documentation\n */\nexport const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(\n (\n {\n href,\n target,\n rel,\n children,\n styles,\n prefetch = false,\n btnStyle,\n onClick,\n onPointerDown,\n ...props\n },\n ref\n ) => {\n /**\n * Compute the final `rel` attribute value with security defaults.\n *\n * For external links (target=\"_blank\"), we merge user-provided `rel` values\n * with security defaults `noopener noreferrer` to prevent:\n * - window.opener exploitation (noopener)\n * - Referrer header leakage (noreferrer)\n *\n * If prefetch is enabled, we also add the `prefetch` hint.\n */\n const computedRel = React.useMemo(() => {\n if (target === \"_blank\") {\n // Start with security defaults\n const securityTokens = new Set([\"noopener\", \"noreferrer\"]);\n\n // Add prefetch if enabled\n if (prefetch) {\n securityTokens.add(\"prefetch\");\n }\n\n // Merge with user-provided rel tokens (if any)\n if (rel) {\n rel.split(/\\s+/).forEach((token) => {\n if (token) securityTokens.add(token);\n });\n }\n\n return Array.from(securityTokens).join(\" \");\n }\n\n // For non-external links, use provided rel as-is\n return rel;\n }, [target, rel, prefetch]);\n\n return (\n <UI\n as=\"a\"\n ref={ref}\n href={href}\n target={target}\n rel={computedRel}\n styles={styles}\n data-btn={btnStyle}\n onClick={onClick}\n onPointerDown={onPointerDown}\n {...props}\n >\n {children}\n </UI>\n );\n }\n);\n\nexport const IconLink = React.forwardRef<HTMLAnchorElement, LinkProps>(\n ({ href, icon, ...props }, ref) => {\n return (\n <Link ref={ref} href={href} {...props}>\n {icon}\n </Link>\n );\n }\n);\n\nexport const LinkButton = React.forwardRef<HTMLAnchorElement, LinkProps>(\n ({ href, children, ...props }, ref) => {\n return (\n <Link ref={ref} href={href} {...props}>\n {children}\n </Link>\n );\n }\n);\n\nIconLink.displayName = \"IconLink\";\nLinkButton.displayName = \"LinkButton\";\nLink.displayName = \"Link\";\n\nexport default Link;\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { b } from './chunk-ZFJ4U45S.js';
|
|
2
2
|
import { b as b$2 } from './chunk-5QD3DWFI.js';
|
|
3
|
-
import { b as b$1 } from './chunk-
|
|
3
|
+
import { b as b$1 } from './chunk-KVKQLRJG.js';
|
|
4
4
|
import { a } from './chunk-HHLNOC5T.js';
|
|
5
5
|
import s, { useCallback, useRef, useId, useEffect } from 'react';
|
|
6
6
|
|
|
@@ -8,4 +8,4 @@ var y=({dialogTitle:t,onClick:o,id:l,type:e="h3"})=>{let a$1=useCallback(()=>{o(
|
|
|
8
8
|
|
|
9
9
|
export { F as a, ao as b };
|
|
10
10
|
//# sourceMappingURL=out.js.map
|
|
11
|
-
//# sourceMappingURL=chunk-
|
|
11
|
+
//# sourceMappingURL=chunk-QKHPHMG2.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunk5PJYLVFY_cjs = require('./chunk-5PJYLVFY.cjs');
|
|
4
4
|
var chunkENTCUJ3A_cjs = require('./chunk-ENTCUJ3A.cjs');
|
|
5
5
|
var e = require('react');
|
|
6
6
|
|
|
@@ -8,10 +8,10 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
8
8
|
|
|
9
9
|
var e__default = /*#__PURE__*/_interopDefault(e);
|
|
10
10
|
|
|
11
|
-
var U=(r,t=15)=>r.length>t?`${r.slice(0,t)}...`:r;var i=e__default.default.memo(({children:r,id:t,styles:s,classes:a,...m})=>{let{renderStyles:n,defaultStyles:o,as:d,ref:I,...p}=m;return e__default.default.createElement("li",{id:t,style:s,className:a,"data-list":"unstyled inline",...p},r)});i.displayName="BreadcrumbItem";var y=e__default.default.memo(({children:r,...t})=>e__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"ol","data-list":"unstyled inline",...t},r));y.displayName="BreadcrumbList";var B=e__default.default.memo(({styles:r,id:t,classes:s,children:a,...m})=>e__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"nav",id:t,styles:r,className:s,...m},e__default.default.createElement(y,null,a)));B.displayName="BreadcrumbNav";function w(r,t){let s=e__default.default.useMemo(()=>r?r.split("/").filter(n=>n):[],[r]),a=e__default.default.useCallback(n=>{let o=t?.find(d=>d.path===n);return {path:o?.path||n,name:o?.name||n,url:o?.url||n}},[t]),m=e__default.default.useMemo(()=>s.map((n,o)=>({...a(n),isLast:o===s.length-1,index:o})),[s,a]);return {segments:m,hasSegments:m.length>0}}var l=({startRoute:r="Home",startRouteUrl:t="/",currentRoute:s,spacer:a=e__default.default.createElement(e__default.default.Fragment,null,"/"),routes:m,styles:n,id:o,classes:d,ariaLabel:I="Breadcrumb",truncateLength:p=15,linkProps:N,...k})=>{let{segments:L,hasSegments:v}=w(s,m),b=e__default.default.useId();return !s?.length||!v?null:e__default.default.createElement(B,{id:o,styles:n,className:d,"aria-label":I,...k},e__default.default.createElement(i,{key:`start-${b}`},e__default.default.createElement(
|
|
11
|
+
var U=(r,t=15)=>r.length>t?`${r.slice(0,t)}...`:r;var i=e__default.default.memo(({children:r,id:t,styles:s,classes:a,...m})=>{let{renderStyles:n,defaultStyles:o,as:d,ref:I,...p}=m;return e__default.default.createElement("li",{id:t,style:s,className:a,"data-list":"unstyled inline",...p},r)});i.displayName="BreadcrumbItem";var y=e__default.default.memo(({children:r,...t})=>e__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"ol","data-list":"unstyled inline",...t},r));y.displayName="BreadcrumbList";var B=e__default.default.memo(({styles:r,id:t,classes:s,children:a,...m})=>e__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"nav",id:t,styles:r,className:s,...m},e__default.default.createElement(y,null,a)));B.displayName="BreadcrumbNav";function w(r,t){let s=e__default.default.useMemo(()=>r?r.split("/").filter(n=>n):[],[r]),a=e__default.default.useCallback(n=>{let o=t?.find(d=>d.path===n);return {path:o?.path||n,name:o?.name||n,url:o?.url||n}},[t]),m=e__default.default.useMemo(()=>s.map((n,o)=>({...a(n),isLast:o===s.length-1,index:o})),[s,a]);return {segments:m,hasSegments:m.length>0}}var l=({startRoute:r="Home",startRouteUrl:t="/",currentRoute:s,spacer:a=e__default.default.createElement(e__default.default.Fragment,null,"/"),routes:m,styles:n,id:o,classes:d,ariaLabel:I="Breadcrumb",truncateLength:p=15,linkProps:N,...k})=>{let{segments:L,hasSegments:v}=w(s,m),b=e__default.default.useId();return !s?.length||!v?null:e__default.default.createElement(B,{id:o,styles:n,className:d,"aria-label":I,...k},e__default.default.createElement(i,{key:`start-${b}`},e__default.default.createElement(chunk5PJYLVFY_cjs.d,{href:t,...N},r)),L.map(({name:$,url:x,path:u,isLast:S,index:h})=>{let c=decodeURIComponent($),C=U(c,p),P=c.length>p;if(S){let M=h>0?L[h-1].path:null;return !u||u.length<=3||u===M?null:e__default.default.createElement(i,{key:`${u}-${b}`},e__default.default.createElement("span",{"aria-hidden":"true"},a),e__default.default.createElement("span",{"aria-current":"page","aria-label":P?c:void 0},C))}return e__default.default.createElement(i,{key:`${u}-${b}`},e__default.default.createElement("span",{"aria-hidden":"true"},a),e__default.default.createElement(chunk5PJYLVFY_cjs.d,{href:x,"aria-label":P?c:void 0,...N},C))}))},H=l;l.displayName="Breadcrumb";l.Nav=B;l.List=y;l.Item=i;
|
|
12
12
|
|
|
13
13
|
exports.a = w;
|
|
14
14
|
exports.b = l;
|
|
15
15
|
exports.c = H;
|
|
16
16
|
//# sourceMappingURL=out.js.map
|
|
17
|
-
//# sourceMappingURL=chunk-
|
|
17
|
+
//# sourceMappingURL=chunk-R7NLLZU2.cjs.map
|