@fpkit/acss 1.0.0-beta.1 → 1.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 +32 -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/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/icons/icon.d.cts +32 -32
- package/libs/components/icons/icon.d.ts +32 -32
- package/libs/components/list/list.css +1 -1
- package/libs/components/list/list.min.css +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/package.json +4 -3
- package/src/components/README.mdx +1 -1
- package/src/components/buttons/button.scss +5 -0
- package/src/components/buttons/button.stories.tsx +8 -5
- 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/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 +545 -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
|
@@ -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 +1 @@
|
|
|
1
|
-
button{--btn-size-xs: 0.6875rem;--btn-size-sm: 0.8125rem;--btn-size-md: 0.9375rem;--btn-size-lg: 1.125rem;--btn-pill: 100rem;--btn-fs: var(--btn-size-md);--btn-height: calc(var(--btn-fs) * 2.25);--btn-bg: lightgray;--btn-width: max-content;font-size:var(--btn-fs);font-weight:var(--btn-fw, 500);height:var(--btn-height);place-items:var(--btn-place, center);padding-inline:var(--btn-padding-inline, calc(var(--btn-fs) * 1.5));padding-block:var(--btn-padding-block, calc(var(--btn-fs) * 0.5));border:var(--btn-border, none);border-radius:var(--btn-radius, 0.375rem);text-decoration:var(--btn-deco, none);color:var(--btn-color, currentColor);display:var(--btn-display, inline-flex);gap:var(--btn-gap, 0.2rem);white-space:var(--btn-whitespace, inherit);margin:var(--btn-spacing, 0);transition:var(--btn-transition, var(--tran-all, all 0.3s cubic-bezier(0.4, 0, 0.2, 1)));background-color:var(--btn-bg, var(--btn));outline:none;width:var(--btn-width);display:inline-flex;align-items:center;line-height:0cap}button[type]{background-color:var(--btn-bg, var(--neutral-300));--btn-border: solid var(--btn-sg)}button[type=submit],button[style*=submit]{--btn-bg: var(--primary-500, royal-blue);--btn-color: white}button[disabled],button[aria-disabled=true]{cursor:var(--btn-cursor, not-allowed)}button[disabled]:is(:hover,:focus),button[aria-disabled=true]:is(:hover,:focus){transform:none}button:is(:hover,:focus){filter:var(--btn-hover-filter, brightness(0.85));transform:var(--btn-hover-transform, scale(1.03));outline:var(--btn-hover-outline, thin);outline-offset:var(--line-offset, 1px)}button:is(:hover,:focus)[aria-disabled=true]{transform:none;opacity:var(--btn-opacity, 0.5);filter:none}button[type=reset]{--btn-bg: transparent;--btn-color: gray;--btn-border: gray thin solid}button[type=submit]{--btn-bg: var(--primary-700, blue);--btn-color: #fff;--btn-border: none}button[data-fp-btn~=pill],button[data-btn~=pill],button[data-style~=pill]{border-radius:var(--btn-pill, 100rem)}button[data-btn~=xs],button .btn-xs{--btn-fs: var(--btn-size-xs);text-transform:uppercase}button[data-btn~=sm],button .btn-sm{--btn-fs: var(--btn-size-sm)}button[data-btn~=md],button .btn-md{--btn-fs: var(--btn-size-md)}button[data-btn~=lg],button .btn-lg{--btn-fs: var(--btn-size-lg)}button[data-btn~=icon],button .btn-icon{padding:unset;height:unset;--btn-bg: transparent;min-width:1.5rem;min-height:1.5rem;text-align:center;display:inline-flex;align-items:center;justify-content:center}button[data-btn~=text],button .btn-text{--btn-bg: transparent;--btn-color: currentColor;--btn-border: none;--btn-height: unset;--btn-width: unset;--btn-padding-block: 0.75rem;--btn-padding-inline: 0.75rem}button[data-btn~=text]:is(:hover,:focus),button .btn-text:is(:hover,:focus){background-color:color-mix(in srgb, var(--btn-color) 10%, transparent);outline:.025rem solid var(--btn-color);outline-offset:0;filter:none}/*# sourceMappingURL=button.css.map */
|
|
1
|
+
button{--btn-size-xs: 0.6875rem;--btn-size-sm: 0.8125rem;--btn-size-md: 0.9375rem;--btn-size-lg: 1.125rem;--btn-pill: 100rem;--btn-fs: var(--btn-size-md);--btn-height: calc(var(--btn-fs) * 2.25);--btn-bg: lightgray;--btn-width: max-content;font-size:var(--btn-fs);font-weight:var(--btn-fw, 500);height:var(--btn-height);place-items:var(--btn-place, center);padding-inline:var(--btn-padding-inline, calc(var(--btn-fs) * 1.5));padding-block:var(--btn-padding-block, calc(var(--btn-fs) * 0.5));border:var(--btn-border, none);border-radius:var(--btn-radius, 0.375rem);text-decoration:var(--btn-deco, none);color:var(--btn-color, currentColor);display:var(--btn-display, inline-flex);gap:var(--btn-gap, 0.2rem);white-space:var(--btn-whitespace, inherit);margin:var(--btn-spacing, 0);transition:var(--btn-transition, var(--tran-all, all 0.3s cubic-bezier(0.4, 0, 0.2, 1)));background-color:var(--btn-bg, var(--btn));outline:none;width:var(--btn-width);display:inline-flex;align-items:center;line-height:0cap}button[type]{background-color:var(--btn-bg, var(--neutral-300));--btn-border: solid var(--btn-sg)}button[type=submit],button[style*=submit]{--btn-bg: var(--primary-500, royal-blue);--btn-color: white}button[disabled],button[aria-disabled=true]{cursor:var(--btn-cursor, not-allowed)}button[disabled]:is(:hover,:focus),button[aria-disabled=true]:is(:hover,:focus){transform:none}button:is(:hover,:focus){filter:var(--btn-hover-filter, brightness(0.85));transform:var(--btn-hover-transform, scale(1.03));outline:var(--btn-hover-outline, thin);outline-offset:var(--line-offset, 1px)}button:is(:hover,:focus)[aria-disabled=true]{transform:none;opacity:var(--btn-opacity, 0.5);filter:none}button:focus-visible{outline:var(--btn-focus-outline, 2px solid currentColor);outline-offset:var(--btn-focus-outline-offset, 1px)}button[type=reset]{--btn-bg: transparent;--btn-color: gray;--btn-border: gray thin solid}button[type=submit]{--btn-bg: var(--primary-700, blue);--btn-color: #fff;--btn-border: none}button[data-fp-btn~=pill],button[data-btn~=pill],button[data-style~=pill]{border-radius:var(--btn-pill, 100rem)}button[data-btn~=xs],button .btn-xs{--btn-fs: var(--btn-size-xs);text-transform:uppercase}button[data-btn~=sm],button .btn-sm{--btn-fs: var(--btn-size-sm)}button[data-btn~=md],button .btn-md{--btn-fs: var(--btn-size-md)}button[data-btn~=lg],button .btn-lg{--btn-fs: var(--btn-size-lg)}button[data-btn~=icon],button .btn-icon{padding:unset;height:unset;--btn-bg: transparent;min-width:1.5rem;min-height:1.5rem;text-align:center;display:inline-flex;align-items:center;justify-content:center}button[data-btn~=text],button .btn-text{--btn-bg: transparent;--btn-color: currentColor;--btn-border: none;--btn-height: unset;--btn-width: unset;--btn-padding-block: 0.75rem;--btn-padding-inline: 0.75rem}button[data-btn~=text]:is(:hover,:focus),button .btn-text:is(:hover,:focus){background-color:color-mix(in srgb, var(--btn-color) 10%, transparent);outline:.025rem solid var(--btn-color);outline-offset:0;filter:none}/*# sourceMappingURL=button.css.map */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sourceRoot":"","sources":["../../../src/components/buttons/button.scss"],"names":[],"mappings":"AAAA,OAEE,yBACA,yBACA,yBACA,wBACA,mBACA,6BACA,yCACA,oBACA,yBAEA,wBACA,+BACA,yBACA,qCACA,oEACA,kEACA,+BACA,0CACA,sCACA,qCACA,wCACA,2BACA,2CACA,6BACA,yFAIA,2CACA,aACA,uBACA,oBACA,mBACA,iBAEA,aACE,mDACA,kCAGF,0CAEE,yCACA,mBAGF,4CAEE,sCAGA,gFACE,eAOJ,yBAEE,iDACA,kDACA,uCACA,uCAGA,6CACE,eACA,gCACA,YAIJ,mBACE,sBACA,kBACA,8BAGF,oBACE,mCACA,kBACA,mBAGF,0EAGE,sCAGF,oCAEE,6BACA,yBAGF,oCAEE,6BAGF,oCAEE,6BAGF,oCAEE,6BAGF,wCAEE,cACA,aACA,sBACA,iBACA,kBACA,kBACA,oBACA,mBACA,uBAGF,wCAEE,sBACA,0BACA,mBACA,oBACA,mBACA,6BACA,8BACA,4EACE,uEACA,uCACA,iBACA","file":"button.css"}
|
|
1
|
+
{"version":3,"sourceRoot":"","sources":["../../../src/components/buttons/button.scss"],"names":[],"mappings":"AAAA,OAEE,yBACA,yBACA,yBACA,wBACA,mBACA,6BACA,yCACA,oBACA,yBAEA,wBACA,+BACA,yBACA,qCACA,oEACA,kEACA,+BACA,0CACA,sCACA,qCACA,wCACA,2BACA,2CACA,6BACA,yFAIA,2CACA,aACA,uBACA,oBACA,mBACA,iBAEA,aACE,mDACA,kCAGF,0CAEE,yCACA,mBAGF,4CAEE,sCAGA,gFACE,eAOJ,yBAEE,iDACA,kDACA,uCACA,uCAGA,6CACE,eACA,gCACA,YAIJ,qBACE,yDACA,oDAGF,mBACE,sBACA,kBACA,8BAGF,oBACE,mCACA,kBACA,mBAGF,0EAGE,sCAGF,oCAEE,6BACA,yBAGF,oCAEE,6BAGF,oCAEE,6BAGF,oCAEE,6BAGF,wCAEE,cACA,aACA,sBACA,iBACA,kBACA,kBACA,oBACA,mBACA,uBAGF,wCAEE,sBACA,0BACA,mBACA,oBACA,mBACA,6BACA,8BACA,4EACE,uEACA,uCACA,iBACA","file":"button.css"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
button{--btn-size-xs: 0.6875rem;--btn-size-sm: 0.8125rem;--btn-size-md: 0.9375rem;--btn-size-lg: 1.125rem;--btn-pill: 100rem;--btn-fs: var(--btn-size-md);--btn-height: calc(var(--btn-fs) * 2.25);--btn-bg: lightgray;--btn-width: max-content;font-size:var(--btn-fs);font-weight:var(--btn-fw, 500);height:var(--btn-height);place-items:var(--btn-place, center);padding-inline:var(--btn-padding-inline, calc(var(--btn-fs) * 1.5));padding-block:var(--btn-padding-block, calc(var(--btn-fs) * 0.5));border:var(--btn-border, none);border-radius:var(--btn-radius, 0.375rem);-webkit-text-decoration:var(--btn-deco, none);text-decoration:var(--btn-deco, none);color:var(--btn-color, currentColor);display:var(--btn-display, inline-flex);gap:var(--btn-gap, 0.2rem);white-space:var(--btn-whitespace, inherit);margin:var(--btn-spacing, 0);transition:var(--btn-transition, var(--tran-all, all 0.3s cubic-bezier(0.4, 0, 0.2, 1)));background-color:var(--btn-bg, var(--btn));outline:none;width:var(--btn-width);display:inline-flex;align-items:center;line-height:0cap}button[type]{background-color:var(--btn-bg, var(--neutral-300));--btn-border: solid var(--btn-sg)}button[type=submit],button[style*=submit]{--btn-bg: var(--primary-500, royal-blue);--btn-color: white}button[disabled],button[aria-disabled=true]{cursor:var(--btn-cursor, not-allowed)}button[disabled]:is(:hover,:focus),button[aria-disabled=true]:is(:hover,:focus){transform:none}button:is(:hover,:focus){filter:var(--btn-hover-filter, brightness(0.85));transform:var(--btn-hover-transform, scale(1.03));outline:var(--btn-hover-outline, thin);outline-offset:var(--line-offset, 1px)}button:is(:hover,:focus)[aria-disabled=true]{transform:none;opacity:var(--btn-opacity, 0.5);filter:none}button[type=reset]{--btn-bg: transparent;--btn-color: gray;--btn-border: gray thin solid}button[type=submit]{--btn-bg: var(--primary-700, blue);--btn-color: #fff;--btn-border: none}button[data-fp-btn~=pill],button[data-btn~=pill],button[data-style~=pill]{border-radius:var(--btn-pill, 100rem)}button[data-btn~=xs],button .btn-xs{--btn-fs: var(--btn-size-xs);text-transform:uppercase}button[data-btn~=sm],button .btn-sm{--btn-fs: var(--btn-size-sm)}button[data-btn~=md],button .btn-md{--btn-fs: var(--btn-size-md)}button[data-btn~=lg],button .btn-lg{--btn-fs: var(--btn-size-lg)}button[data-btn~=icon],button .btn-icon{padding:unset;height:unset;--btn-bg: transparent;min-width:1.5rem;min-height:1.5rem;text-align:center;display:inline-flex;align-items:center;justify-content:center}button[data-btn~=text],button .btn-text{--btn-bg: transparent;--btn-color: currentColor;--btn-border: none;--btn-height: unset;--btn-width: unset;--btn-padding-block: 0.75rem;--btn-padding-inline: 0.75rem}button[data-btn~=text]:is(:hover,:focus),button .btn-text:is(:hover,:focus){background-color:color-mix(in srgb, var(--btn-color) 10%, transparent);outline:.025rem solid var(--btn-color);outline-offset:0;filter:none}
|
|
1
|
+
button{--btn-size-xs: 0.6875rem;--btn-size-sm: 0.8125rem;--btn-size-md: 0.9375rem;--btn-size-lg: 1.125rem;--btn-pill: 100rem;--btn-fs: var(--btn-size-md);--btn-height: calc(var(--btn-fs) * 2.25);--btn-bg: lightgray;--btn-width: max-content;font-size:var(--btn-fs);font-weight:var(--btn-fw, 500);height:var(--btn-height);place-items:var(--btn-place, center);padding-inline:var(--btn-padding-inline, calc(var(--btn-fs) * 1.5));padding-block:var(--btn-padding-block, calc(var(--btn-fs) * 0.5));border:var(--btn-border, none);border-radius:var(--btn-radius, 0.375rem);-webkit-text-decoration:var(--btn-deco, none);text-decoration:var(--btn-deco, none);color:var(--btn-color, currentColor);display:var(--btn-display, inline-flex);gap:var(--btn-gap, 0.2rem);white-space:var(--btn-whitespace, inherit);margin:var(--btn-spacing, 0);transition:var(--btn-transition, var(--tran-all, all 0.3s cubic-bezier(0.4, 0, 0.2, 1)));background-color:var(--btn-bg, var(--btn));outline:none;width:var(--btn-width);display:inline-flex;align-items:center;line-height:0cap}button[type]{background-color:var(--btn-bg, var(--neutral-300));--btn-border: solid var(--btn-sg)}button[type=submit],button[style*=submit]{--btn-bg: var(--primary-500, royal-blue);--btn-color: white}button[disabled],button[aria-disabled=true]{cursor:var(--btn-cursor, not-allowed)}button[disabled]:is(:hover,:focus),button[aria-disabled=true]:is(:hover,:focus){transform:none}button:is(:hover,:focus){filter:var(--btn-hover-filter, brightness(0.85));transform:var(--btn-hover-transform, scale(1.03));outline:var(--btn-hover-outline, thin);outline-offset:var(--line-offset, 1px)}button:is(:hover,:focus)[aria-disabled=true]{transform:none;opacity:var(--btn-opacity, 0.5);filter:none}button:focus-visible{outline:var(--btn-focus-outline, 2px solid currentColor);outline-offset:var(--btn-focus-outline-offset, 1px)}button[type=reset]{--btn-bg: transparent;--btn-color: gray;--btn-border: gray thin solid}button[type=submit]{--btn-bg: var(--primary-700, blue);--btn-color: #fff;--btn-border: none}button[data-fp-btn~=pill],button[data-btn~=pill],button[data-style~=pill]{border-radius:var(--btn-pill, 100rem)}button[data-btn~=xs],button .btn-xs{--btn-fs: var(--btn-size-xs);text-transform:uppercase}button[data-btn~=sm],button .btn-sm{--btn-fs: var(--btn-size-sm)}button[data-btn~=md],button .btn-md{--btn-fs: var(--btn-size-md)}button[data-btn~=lg],button .btn-lg{--btn-fs: var(--btn-size-lg)}button[data-btn~=icon],button .btn-icon{padding:unset;height:unset;--btn-bg: transparent;min-width:1.5rem;min-height:1.5rem;text-align:center;display:inline-flex;align-items:center;justify-content:center}button[data-btn~=text],button .btn-text{--btn-bg: transparent;--btn-color: currentColor;--btn-border: none;--btn-height: unset;--btn-width: unset;--btn-padding-block: 0.75rem;--btn-padding-inline: 0.75rem}button[data-btn~=text]:is(:hover,:focus),button .btn-text:is(:hover,:focus){background-color:color-mix(in srgb, var(--btn-color) 10%, transparent);outline:.025rem solid var(--btn-color);outline-offset:0;filter:none}
|
|
2
2
|
|
|
3
|
-
/*# sourceMappingURL=data:application/json;base64,
|
|
3
|
+
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL2J1dHRvbnMvYnV0dG9uLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FFRSx3QkFBQSxDQUNBLHdCQUFBLENBQ0Esd0JBQUEsQ0FDQSx1QkFBQSxDQUNBLGtCQUFBLENBQ0EsNEJBQUEsQ0FDQSx3Q0FBQSxDQUNBLG1CQUFBLENBQ0Esd0JBQUEsQ0FFQSx1QkFBQSxDQUNBLDhCQUFBLENBQ0Esd0JBQUEsQ0FDQSxvQ0FBQSxDQUNBLG1FQUFBLENBQ0EsaUVBQUEsQ0FDQSw4QkFBQSxDQUNBLHlDQUFBLENBQ0EsNkNBQUEsQ0FBQSxxQ0FBQSxDQUNBLG9DQUFBLENBQ0EsdUNBQUEsQ0FDQSwwQkFBQSxDQUNBLDBDQUFBLENBQ0EsNEJBQUEsQ0FDQSx3RkFBQSxDQUlBLDBDQUFBLENBQ0EsWUFBQSxDQUNBLHNCQUFBLENBQ0EsbUJBQUEsQ0FDQSxrQkFBQSxDQUNBLGdCQUFBLENBRUEsYUFDRSxrREFBQSxDQUNBLGlDQUFBLENBR0YsMENBRUUsd0NBQUEsQ0FDQSxrQkFBQSxDQUdGLDRDQUVFLHFDQUFBLENBR0EsZ0ZBQ0UsY0FBQSxDQU9KLHlCQUVFLGdEQUFBLENBQ0EsaURBQUEsQ0FDQSxzQ0FBQSxDQUNBLHNDQUFBLENBR0EsNkNBQ0UsY0FBQSxDQUNBLCtCQUFBLENBQ0EsV0FBQSxDQUlKLHFCQUNFLHdEQUFBLENBQ0EsbURBQUEsQ0FHRixtQkFDRSxxQkFBQSxDQUNBLGlCQUFBLENBQ0EsNkJBQUEsQ0FHRixvQkFDRSxrQ0FBQSxDQUNBLGlCQUFBLENBQ0Esa0JBQUEsQ0FHRiwwRUFHRSxxQ0FBQSxDQUdGLG9DQUVFLDRCQUFBLENBQ0Esd0JBQUEsQ0FHRixvQ0FFRSw0QkFBQSxDQUdGLG9DQUVFLDRCQUFBLENBR0Ysb0NBRUUsNEJBQUEsQ0FHRix3Q0FFRSxhQUFBLENBQ0EsWUFBQSxDQUNBLHFCQUFBLENBQ0EsZ0JBQUEsQ0FDQSxpQkFBQSxDQUNBLGlCQUFBLENBQ0EsbUJBQUEsQ0FDQSxrQkFBQSxDQUNBLHNCQUFBLENBR0Ysd0NBRUUscUJBQUEsQ0FDQSx5QkFBQSxDQUNBLGtCQUFBLENBQ0EsbUJBQUEsQ0FDQSxrQkFBQSxDQUNBLDRCQUFBLENBQ0EsNkJBQUEsQ0FDQSw0RUFDRSxzRUFBQSxDQUNBLHNDQUFBLENBQ0EsZ0JBQUEsQ0FDQSxXQUFBIiwiZmlsZSI6ImJ1dHRvbi5taW4uY3NzIn0= */
|