accessible-kit 1.0.5 → 1.0.7
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/CHANGELOG.md +39 -0
- package/README.md +71 -2
- package/package.json +1 -1
- package/src/css/a11y-accordion.theme.css +4 -4
- package/src/css/a11y-dropdown.core.css +0 -18
- package/src/css/a11y-dropdown.theme.css +8 -5
- package/src/css/a11y-modal.core.css +0 -5
- package/src/css/a11y-modal.theme.css +6 -2
- package/src/css/a11y-offcanvas.core.css +0 -5
- package/src/css/a11y-offcanvas.theme.css +5 -0
- package/src/css/a11y-tabs.core.css +0 -9
- package/src/css/a11y-tabs.theme.css +9 -4
- package/src/js/a11y-dropdown.js +63 -23
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.7] - 2026-01-26
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **All Components - Focus Indicator Improvements**: Migrated from `:focus` to `:focus-visible` for better keyboard navigation UX
|
|
12
|
+
- **Breaking Change for Custom Themes**: If you have custom theme files, update `:focus` selectors to `:focus-visible`
|
|
13
|
+
- **Complete separation of concerns**: ALL visual styling (including `outline` properties) moved from `*.core.css` to `*.theme.css` files
|
|
14
|
+
- Core CSS files now contain ZERO visual styling - only structural layout and behavior
|
|
15
|
+
- Theme CSS files contain all focus styling including `outline`, `outline-offset`, and `outline-color`
|
|
16
|
+
- **Benefits**:
|
|
17
|
+
- Focus indicators only appear for keyboard navigation, not on mouse clicks
|
|
18
|
+
- Perfect separation between core functionality and visual design
|
|
19
|
+
- Complete customization freedom - modify or remove all focus styling in theme files
|
|
20
|
+
- No hardcoded visual defaults in core files
|
|
21
|
+
- **Affected components**: Dropdown, Modal, Offcanvas, Tabs, Accordion
|
|
22
|
+
- **Migration guide**:
|
|
23
|
+
- Update your custom theme `:focus` selectors to `:focus-visible`
|
|
24
|
+
- All `outline` customization should now be done in `*.theme.css` files only
|
|
25
|
+
- Updated documentation in README with focus-visible information
|
|
26
|
+
|
|
27
|
+
## [1.0.6] - 2026-01-15
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **Dropdown - Multiple ARIA Pattern Support**: Added `data-dropdown-role` attribute to support dialog and listbox patterns in addition to the default menu pattern
|
|
31
|
+
- **New attribute**: `data-dropdown-role` accepts `"menu"` (default), `"dialog"`, or `"listbox"`
|
|
32
|
+
- Automatically sets appropriate `aria-haspopup` value based on role type:
|
|
33
|
+
- `role="menu"` → `aria-haspopup="true"`
|
|
34
|
+
- `role="dialog"` → `aria-haspopup="dialog"`
|
|
35
|
+
- `role="listbox"` → `aria-haspopup="listbox"`
|
|
36
|
+
- Conditionally applies correct item roles (`menuitem`, none for dialog, `option` for listbox)
|
|
37
|
+
- Pattern-specific keyboard navigation:
|
|
38
|
+
- Menu/listbox: Arrow keys, Home/End, Tab closes dropdown
|
|
39
|
+
- Dialog: Arrow keys, Tab navigates within (doesn't close)
|
|
40
|
+
- **Use case example**: Radio playlists, media controls, or complex interactive widgets that aren't semantic "menus"
|
|
41
|
+
- Added comprehensive documentation and live example (radio playlist) to demo page
|
|
42
|
+
- Updated README with ARIA pattern guidance and when to use each pattern
|
|
43
|
+
- **Backwards compatible**: Defaults to `"menu"` pattern, existing implementations unchanged
|
|
44
|
+
|
|
8
45
|
## [1.0.5] - 2025-12-21
|
|
9
46
|
|
|
10
47
|
### Fixed
|
|
@@ -134,6 +171,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
134
171
|
- Zero dependencies
|
|
135
172
|
- Full TypeScript-ready exports
|
|
136
173
|
|
|
174
|
+
[1.0.7]: https://github.com/5ulo/accessible-kit/compare/v1.0.6...v1.0.7
|
|
175
|
+
[1.0.6]: https://github.com/5ulo/accessible-kit/compare/v1.0.5...v1.0.6
|
|
137
176
|
[1.0.5]: https://github.com/5ulo/accessible-kit/compare/v1.0.4...v1.0.5
|
|
138
177
|
[1.0.4]: https://github.com/5ulo/accessible-kit/compare/v1.0.3...v1.0.4
|
|
139
178
|
[1.0.3]: https://github.com/5ulo/accessible-kit/compare/v1.0.2...v1.0.3
|
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ A collection of fully accessible UI components for modern web applications. Buil
|
|
|
19
19
|
- **♿ Inclusive** - High contrast mode and reduced motion support
|
|
20
20
|
- **🔧 Framework Agnostic** - Works with any framework or vanilla JS
|
|
21
21
|
- **📦 Tree-shakeable** - Import only what you need
|
|
22
|
+
- **⌨️ Focus Visible** - Uses `:focus-visible` for better keyboard navigation UX
|
|
22
23
|
|
|
23
24
|
## 📚 Table of Contents
|
|
24
25
|
|
|
@@ -168,11 +169,12 @@ const dropdown = new AccessibleDropdown(element, {
|
|
|
168
169
|
|
|
169
170
|
## Dropdown
|
|
170
171
|
|
|
171
|
-
Fully accessible dropdown component with keyboard navigation and ARIA support.
|
|
172
|
+
Fully accessible dropdown component with keyboard navigation and ARIA support. Supports multiple ARIA patterns (menu, dialog, listbox) for different use cases.
|
|
172
173
|
|
|
173
174
|
### Features
|
|
174
175
|
|
|
175
|
-
- ✅ Full ARIA attributes support
|
|
176
|
+
- ✅ Full ARIA attributes support (menu, dialog, and listbox patterns)
|
|
177
|
+
- ✅ Flexible role patterns via `data-dropdown-role` attribute
|
|
176
178
|
- ✅ Keyboard navigation (arrows, Enter, Space, Esc, Home, End)
|
|
177
179
|
- ✅ Focus management
|
|
178
180
|
- ✅ Auto-close on outside click
|
|
@@ -213,6 +215,57 @@ Fully accessible dropdown component with keyboard navigation and ARIA support.
|
|
|
213
215
|
</div>
|
|
214
216
|
```
|
|
215
217
|
|
|
218
|
+
### ARIA Patterns
|
|
219
|
+
|
|
220
|
+
The dropdown component supports multiple ARIA patterns to match different use cases. By default, it uses the **menu pattern**, but you can change this using the `data-dropdown-role` attribute.
|
|
221
|
+
|
|
222
|
+
#### When to Use Each Pattern
|
|
223
|
+
|
|
224
|
+
**Menu Pattern (default)** - Use `role="menu"` for:
|
|
225
|
+
- Navigation menus
|
|
226
|
+
- Action menus (Edit, Delete, etc.)
|
|
227
|
+
- Language/region selectors
|
|
228
|
+
- User profile menus
|
|
229
|
+
- Context menus
|
|
230
|
+
|
|
231
|
+
**Dialog Pattern** - Use `data-dropdown-role="dialog"` for:
|
|
232
|
+
- Media player playlists/controls
|
|
233
|
+
- Complex interactive widgets
|
|
234
|
+
- Custom form controls
|
|
235
|
+
- Rich content with multiple focusable elements
|
|
236
|
+
- Non-menu disclosure patterns
|
|
237
|
+
|
|
238
|
+
**Example: Dialog Pattern (Radio Playlist)**
|
|
239
|
+
|
|
240
|
+
```html
|
|
241
|
+
<!-- Use data-dropdown-role="dialog" for playlists, controls, or interactive widgets -->
|
|
242
|
+
<div data-dropdown data-dropdown-role="dialog">
|
|
243
|
+
<button data-dropdown-button>
|
|
244
|
+
🎵 Playlist
|
|
245
|
+
<span data-dropdown-arrow></span>
|
|
246
|
+
</button>
|
|
247
|
+
<div data-dropdown-menu>
|
|
248
|
+
<div>
|
|
249
|
+
<div>
|
|
250
|
+
<button data-dropdown-item>Station 1</button>
|
|
251
|
+
<button data-dropdown-item>Station 2</button>
|
|
252
|
+
<button data-dropdown-item>Station 3</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<!-- This automatically sets:
|
|
259
|
+
- Button: aria-haspopup="dialog" (instead of "true")
|
|
260
|
+
- Menu: role="dialog" (instead of "menu")
|
|
261
|
+
- Items: No role attribute (instead of "menuitem")
|
|
262
|
+
- Keyboard: Tab allowed within dropdown (menu pattern closes on Tab)
|
|
263
|
+
-->
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Why use dialog pattern?**
|
|
267
|
+
When your dropdown contains complex interactive content (like a media player playlist), it's not semantically a "menu" of commands. The dialog pattern better represents this content structure and provides more appropriate keyboard behavior. Tab/Shift+Tab cycles through items within the dropdown (instead of closing it like menu pattern does).
|
|
268
|
+
|
|
216
269
|
### JavaScript Initialization
|
|
217
270
|
|
|
218
271
|
```javascript
|
|
@@ -225,6 +278,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
225
278
|
|
|
226
279
|
// Or manual initialization with options
|
|
227
280
|
const dropdown = new Dropdown(element, {
|
|
281
|
+
dropdownRole: 'menu', // 'menu' (default), 'dialog', or 'listbox'
|
|
228
282
|
closeOnSelect: true,
|
|
229
283
|
closeOnOutsideClick: true,
|
|
230
284
|
closeOnEscape: true,
|
|
@@ -269,6 +323,9 @@ Dropdowns have **CSS Grid animations enabled by default**. The animation automat
|
|
|
269
323
|
**Data attributes:**
|
|
270
324
|
|
|
271
325
|
```html
|
|
326
|
+
<!-- Use dialog pattern for non-menu content -->
|
|
327
|
+
<div data-dropdown data-dropdown-role="dialog">
|
|
328
|
+
|
|
272
329
|
<!-- Keep dropdown open after selection -->
|
|
273
330
|
<div data-dropdown data-close-on-select="false">
|
|
274
331
|
|
|
@@ -283,6 +340,7 @@ Dropdowns have **CSS Grid animations enabled by default**. The animation automat
|
|
|
283
340
|
|
|
284
341
|
```javascript
|
|
285
342
|
{
|
|
343
|
+
dropdownRole: 'menu', // ARIA pattern: 'menu', 'dialog', or 'listbox'
|
|
286
344
|
closeOnSelect: true, // Close after selecting item
|
|
287
345
|
closeOnOutsideClick: true, // Close on outside click
|
|
288
346
|
closeOnEscape: true, // Close on Escape key
|
|
@@ -295,12 +353,21 @@ Dropdowns have **CSS Grid animations enabled by default**. The animation automat
|
|
|
295
353
|
|
|
296
354
|
### Keyboard Navigation
|
|
297
355
|
|
|
356
|
+
**Menu Pattern (default):**
|
|
357
|
+
|
|
298
358
|
- **Enter** / **Space** - Open/close menu or select item
|
|
299
359
|
- **↓** / **↑** - Navigate between items
|
|
300
360
|
- **Home** / **End** - Jump to first/last item
|
|
301
361
|
- **Esc** - Close menu and return focus to button
|
|
302
362
|
- **Tab** - Close menu and move focus
|
|
303
363
|
|
|
364
|
+
**Dialog Pattern:**
|
|
365
|
+
|
|
366
|
+
- **Enter** / **Space** - Open/close or select item
|
|
367
|
+
- **↓** / **↑** - Navigate between items
|
|
368
|
+
- **Esc** - Close and return focus to button
|
|
369
|
+
- **Tab** / **Shift+Tab** - Cycle through items within dialog (does not close)
|
|
370
|
+
|
|
304
371
|
### Variants
|
|
305
372
|
|
|
306
373
|
**Navigation menu:**
|
|
@@ -989,6 +1056,7 @@ Contains only logic, positioning, layout, and behavior:
|
|
|
989
1056
|
- Visibility states
|
|
990
1057
|
- Animations
|
|
991
1058
|
- Responsive breakpoints
|
|
1059
|
+
- **NO visual styling** - completely theme-agnostic
|
|
992
1060
|
|
|
993
1061
|
**Do not modify** unless changing component functionality.
|
|
994
1062
|
|
|
@@ -1000,6 +1068,7 @@ Contains all visual styling:
|
|
|
1000
1068
|
- Typography
|
|
1001
1069
|
- Borders and border-radius
|
|
1002
1070
|
- Shadows
|
|
1071
|
+
- **All focus indicators** (`:focus-visible` with `outline`, `outline-offset`, etc.)
|
|
1003
1072
|
- Dark mode
|
|
1004
1073
|
- High contrast mode
|
|
1005
1074
|
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
background: #f9fafb;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
[data-accordion-trigger]:focus {
|
|
34
|
+
[data-accordion-trigger]:focus-visible {
|
|
35
35
|
outline: 2px solid #3b82f6;
|
|
36
36
|
outline-offset: -2px;
|
|
37
37
|
}
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
background: #2563eb;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
[data-accordion-toggle]:focus {
|
|
81
|
+
[data-accordion-toggle]:focus-visible {
|
|
82
82
|
outline: 2px solid #3b82f6;
|
|
83
83
|
outline-offset: 2px;
|
|
84
84
|
}
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
border-color: #d1d5db;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
[data-accordion-toggle-all]:focus {
|
|
115
|
+
[data-accordion-toggle-all]:focus-visible {
|
|
116
116
|
outline: 2px solid #3b82f6;
|
|
117
117
|
outline-offset: 2px;
|
|
118
118
|
}
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
border-width: 2px;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
[data-accordion-trigger]:focus {
|
|
217
|
+
[data-accordion-trigger]:focus-visible {
|
|
218
218
|
outline-width: 3px;
|
|
219
219
|
}
|
|
220
220
|
}
|
|
@@ -16,10 +16,6 @@
|
|
|
16
16
|
cursor: pointer;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
[data-dropdown-button]:focus {
|
|
20
|
-
outline-offset: 2px;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
/* Button icon/arrow */
|
|
24
20
|
[data-dropdown-arrow] {
|
|
25
21
|
display: inline-block;
|
|
@@ -99,10 +95,6 @@
|
|
|
99
95
|
text-decoration: none;
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
[data-dropdown-item]:focus {
|
|
103
|
-
outline: none;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
98
|
/* Divider */
|
|
107
99
|
[data-dropdown-divider] {
|
|
108
100
|
height: 1px;
|
|
@@ -130,9 +122,6 @@
|
|
|
130
122
|
}
|
|
131
123
|
|
|
132
124
|
/* Navigation variant */
|
|
133
|
-
[data-dropdown][data-variant="nav"] [data-dropdown-button]:focus {
|
|
134
|
-
outline-offset: 2px;
|
|
135
|
-
}
|
|
136
125
|
|
|
137
126
|
/* Language switcher variant */
|
|
138
127
|
[data-dropdown][data-variant="language"] [data-dropdown-item] {
|
|
@@ -153,13 +142,6 @@
|
|
|
153
142
|
}
|
|
154
143
|
}
|
|
155
144
|
|
|
156
|
-
/* High contrast mode support */
|
|
157
|
-
@media (prefers-contrast: high) {
|
|
158
|
-
[data-dropdown-item]:focus {
|
|
159
|
-
outline-offset: -2px;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
145
|
/* Reduced motion support - disable animations */
|
|
164
146
|
@media (prefers-reduced-motion: reduce) {
|
|
165
147
|
[data-dropdown-menu]:not([data-no-animation]),
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
border-color: #9ca3af;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
[data-dropdown-button]:focus {
|
|
23
|
+
[data-dropdown-button]:focus-visible {
|
|
24
24
|
outline: 2px solid #3b82f6;
|
|
25
25
|
border-color: #3b82f6;
|
|
26
26
|
}
|
|
@@ -61,9 +61,10 @@
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
[data-dropdown-item]:hover,
|
|
64
|
-
[data-dropdown-item]:focus {
|
|
64
|
+
[data-dropdown-item]:focus-visible {
|
|
65
65
|
background: #f3f4f6;
|
|
66
66
|
color: #111827;
|
|
67
|
+
outline: none;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
[data-dropdown-item]:active {
|
|
@@ -98,7 +99,7 @@
|
|
|
98
99
|
background: rgba(0, 0, 0, 0.05);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
[data-dropdown][data-variant="nav"] [data-dropdown-button]:focus {
|
|
102
|
+
[data-dropdown][data-variant="nav"] [data-dropdown-button]:focus-visible {
|
|
102
103
|
outline: 2px solid currentColor;
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -122,8 +123,9 @@
|
|
|
122
123
|
border-width: 2px;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
[data-dropdown-item]:focus {
|
|
126
|
+
[data-dropdown-item]:focus-visible {
|
|
126
127
|
outline: 2px solid currentColor;
|
|
128
|
+
outline-offset: -2px;
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -160,9 +162,10 @@
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
[data-dropdown-item]:hover,
|
|
163
|
-
[data-dropdown-item]:focus {
|
|
165
|
+
[data-dropdown-item]:focus-visible {
|
|
164
166
|
background: #374151;
|
|
165
167
|
color: #fff;
|
|
168
|
+
outline: none;
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
[data-dropdown-item]:active {
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
[data-modal-dialog]:focus-visible {
|
|
20
|
+
outline: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
/* Modal header */
|
|
20
24
|
[data-modal-header] {
|
|
21
25
|
padding: 1.5rem;
|
|
@@ -66,7 +70,7 @@
|
|
|
66
70
|
color: #111827;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
[data-modal-close]:focus {
|
|
73
|
+
[data-modal-close]:focus-visible {
|
|
70
74
|
outline: 2px solid #3b82f6;
|
|
71
75
|
outline-offset: 2px;
|
|
72
76
|
}
|
|
@@ -200,7 +204,7 @@
|
|
|
200
204
|
border: 2px solid currentColor;
|
|
201
205
|
}
|
|
202
206
|
|
|
203
|
-
[data-modal-close]:focus {
|
|
207
|
+
[data-modal-close]:focus-visible {
|
|
204
208
|
outline-width: 3px;
|
|
205
209
|
}
|
|
206
210
|
}
|
|
@@ -112,11 +112,6 @@ body.offcanvas-open {
|
|
|
112
112
|
overflow: hidden;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/* Focus trap */
|
|
116
|
-
[data-offcanvas-panel]:focus {
|
|
117
|
-
outline: none;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
115
|
/* Reduced motion support */
|
|
121
116
|
@media (prefers-reduced-motion: reduce) {
|
|
122
117
|
[data-offcanvas-panel],
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
/* Obsahuje vizualne nastavenia: farby, velkosti, bordery, radiusy, spacing */
|
|
3
3
|
/* Pre zmenu vzhladu upravte tento subor */
|
|
4
4
|
|
|
5
|
+
/* Focus visible for the panel itself (focus trap) */
|
|
6
|
+
[data-offcanvas-panel]:focus-visible {
|
|
7
|
+
outline: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
/* Focus visible for all focusable elements inside offcanvas */
|
|
6
11
|
[data-offcanvas-panel] :focus-visible {
|
|
7
12
|
outline: 2px solid #3b82f6;
|
|
@@ -46,10 +46,6 @@
|
|
|
46
46
|
pointer-events: none;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
[data-tabs-tab]:focus {
|
|
50
|
-
outline-offset: -2px;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
49
|
/* Tab panels container */
|
|
54
50
|
[data-tabs-panels] {
|
|
55
51
|
position: relative;
|
|
@@ -64,11 +60,6 @@
|
|
|
64
60
|
display: block;
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
/* Focus within panel */
|
|
68
|
-
[data-tabs-panel]:focus {
|
|
69
|
-
outline: none;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
63
|
/* View Transitions API uses default cross-fade animation */
|
|
73
64
|
/* Customization via pseudo-elements */
|
|
74
65
|
::view-transition-old(root),
|
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
background: #f9fafb;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
[data-tabs-tab]:focus {
|
|
38
|
+
[data-tabs-tab]:focus-visible {
|
|
39
39
|
outline: 2px solid #3b82f6;
|
|
40
|
+
outline-offset: -2px;
|
|
40
41
|
color: #374151;
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -80,6 +81,10 @@
|
|
|
80
81
|
line-height: 1.6;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
[data-tabs-panel]:focus-visible {
|
|
85
|
+
outline: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
/* Boxed variant */
|
|
84
89
|
[data-tabs][data-tabs][data-variant="boxed"] [data-tabs-list] {
|
|
85
90
|
border-bottom: 1px solid #e5e7eb;
|
|
@@ -136,7 +141,7 @@
|
|
|
136
141
|
color: #fff;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
[data-tabs][data-tabs][data-variant="pills"] [data-tabs-tab]:focus {
|
|
144
|
+
[data-tabs][data-tabs][data-variant="pills"] [data-tabs-tab]:focus-visible {
|
|
140
145
|
outline-color: #3b82f6;
|
|
141
146
|
}
|
|
142
147
|
|
|
@@ -222,7 +227,7 @@
|
|
|
222
227
|
border-width: 2px;
|
|
223
228
|
}
|
|
224
229
|
|
|
225
|
-
[data-tabs-tab]:focus {
|
|
230
|
+
[data-tabs-tab]:focus-visible {
|
|
226
231
|
outline-width: 3px;
|
|
227
232
|
}
|
|
228
233
|
}
|
|
@@ -246,7 +251,7 @@
|
|
|
246
251
|
background: #1f2937;
|
|
247
252
|
}
|
|
248
253
|
|
|
249
|
-
[data-tabs-tab]:focus {
|
|
254
|
+
[data-tabs-tab]:focus-visible {
|
|
250
255
|
color: #f9fafb;
|
|
251
256
|
}
|
|
252
257
|
|
package/src/js/a11y-dropdown.js
CHANGED
|
@@ -18,6 +18,7 @@ class AccessibleDropdown {
|
|
|
18
18
|
|
|
19
19
|
// Options
|
|
20
20
|
this.options = {
|
|
21
|
+
dropdownRole: options.dropdownRole || "menu",
|
|
21
22
|
closeOnSelect: options.closeOnSelect !== false,
|
|
22
23
|
closeOnOutsideClick: options.closeOnOutsideClick !== false,
|
|
23
24
|
closeOnEscape: options.closeOnEscape !== false,
|
|
@@ -91,18 +92,24 @@ class AccessibleDropdown {
|
|
|
91
92
|
.substr(2, 9)}`;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
// Button ARIA attributes
|
|
95
|
-
this.
|
|
95
|
+
// Button ARIA attributes - set aria-haspopup based on dropdown role
|
|
96
|
+
const ariaHaspopupValue = this.options.dropdownRole === "menu" ? "true" : this.options.dropdownRole;
|
|
97
|
+
this.button.setAttribute("aria-haspopup", ariaHaspopupValue);
|
|
96
98
|
this.button.setAttribute("aria-expanded", "false");
|
|
97
99
|
this.button.setAttribute("aria-controls", this.menu.id);
|
|
98
100
|
|
|
99
|
-
// Menu ARIA attributes
|
|
100
|
-
this.menu.setAttribute("role",
|
|
101
|
+
// Menu ARIA attributes - set role based on dropdown role
|
|
102
|
+
this.menu.setAttribute("role", this.options.dropdownRole);
|
|
101
103
|
this.menu.setAttribute("aria-labelledby", this.button.id);
|
|
102
104
|
|
|
103
|
-
// Menu items ARIA attributes
|
|
105
|
+
// Menu items ARIA attributes - set item role based on dropdown role
|
|
106
|
+
const itemRole = this.options.dropdownRole === "menu" ? "menuitem" :
|
|
107
|
+
this.options.dropdownRole === "listbox" ? "option" : null;
|
|
108
|
+
|
|
104
109
|
this.items.forEach((item) => {
|
|
105
|
-
|
|
110
|
+
if (itemRole) {
|
|
111
|
+
item.setAttribute("role", itemRole);
|
|
112
|
+
}
|
|
106
113
|
if (!item.hasAttribute("tabindex")) {
|
|
107
114
|
item.setAttribute("tabindex", "-1");
|
|
108
115
|
}
|
|
@@ -184,32 +191,64 @@ class AccessibleDropdown {
|
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
handleItemKeydown(e, index) {
|
|
194
|
+
// Common navigation for all patterns
|
|
187
195
|
switch (e.key) {
|
|
188
196
|
case "Enter":
|
|
189
197
|
case " ":
|
|
190
198
|
e.preventDefault();
|
|
191
199
|
this.selectItem(index);
|
|
192
200
|
break;
|
|
193
|
-
case "ArrowDown":
|
|
194
|
-
e.preventDefault();
|
|
195
|
-
this.focusNextItem();
|
|
196
|
-
break;
|
|
197
|
-
case "ArrowUp":
|
|
198
|
-
e.preventDefault();
|
|
199
|
-
this.focusPreviousItem();
|
|
200
|
-
break;
|
|
201
|
-
case "Home":
|
|
202
|
-
e.preventDefault();
|
|
203
|
-
this.setFocusedItem(0);
|
|
204
|
-
break;
|
|
205
|
-
case "End":
|
|
206
|
-
e.preventDefault();
|
|
207
|
-
this.setFocusedItem(this.items.length - 1);
|
|
208
|
-
break;
|
|
209
201
|
case "Tab":
|
|
210
|
-
|
|
202
|
+
// For dialog pattern, Tab should cycle within items
|
|
203
|
+
// For menu and listbox, close on Tab
|
|
204
|
+
if (this.options.dropdownRole === "dialog") {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
// Tab forward or backward through items
|
|
207
|
+
if (e.shiftKey) {
|
|
208
|
+
this.focusPreviousItem();
|
|
209
|
+
} else {
|
|
210
|
+
this.focusNextItem();
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
// Menu and listbox patterns close on Tab
|
|
214
|
+
this.close();
|
|
215
|
+
}
|
|
211
216
|
break;
|
|
212
217
|
}
|
|
218
|
+
|
|
219
|
+
// Pattern-specific navigation
|
|
220
|
+
if (this.options.dropdownRole === "menu" || this.options.dropdownRole === "listbox") {
|
|
221
|
+
switch (e.key) {
|
|
222
|
+
case "ArrowDown":
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
this.focusNextItem();
|
|
225
|
+
break;
|
|
226
|
+
case "ArrowUp":
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
this.focusPreviousItem();
|
|
229
|
+
break;
|
|
230
|
+
case "Home":
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
this.setFocusedItem(0);
|
|
233
|
+
break;
|
|
234
|
+
case "End":
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
this.setFocusedItem(this.items.length - 1);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
} else if (this.options.dropdownRole === "dialog") {
|
|
240
|
+
// Dialog pattern: Arrow keys for navigation (less strict)
|
|
241
|
+
switch (e.key) {
|
|
242
|
+
case "ArrowDown":
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
this.focusNextItem();
|
|
245
|
+
break;
|
|
246
|
+
case "ArrowUp":
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
this.focusPreviousItem();
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
213
252
|
}
|
|
214
253
|
|
|
215
254
|
handleItemClick(e, index) {
|
|
@@ -320,6 +359,7 @@ function initDropdowns() {
|
|
|
320
359
|
|
|
321
360
|
dropdowns.forEach((dropdown) => {
|
|
322
361
|
const options = {
|
|
362
|
+
dropdownRole: dropdown.dataset.dropdownRole || "menu",
|
|
323
363
|
closeOnSelect: dropdown.dataset.closeOnSelect !== "false",
|
|
324
364
|
closeOnOutsideClick:
|
|
325
365
|
dropdown.dataset.closeOnOutsideClick !== "false",
|