accessible-kit 1.0.5 → 1.0.6
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 +19 -0
- package/README.md +68 -2
- package/package.json +1 -1
- package/src/js/a11y-dropdown.js +63 -23
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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.6] - 2026-01-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Dropdown - Multiple ARIA Pattern Support**: Added `data-dropdown-role` attribute to support dialog and listbox patterns in addition to the default menu pattern
|
|
12
|
+
- **New attribute**: `data-dropdown-role` accepts `"menu"` (default), `"dialog"`, or `"listbox"`
|
|
13
|
+
- Automatically sets appropriate `aria-haspopup` value based on role type:
|
|
14
|
+
- `role="menu"` → `aria-haspopup="true"`
|
|
15
|
+
- `role="dialog"` → `aria-haspopup="dialog"`
|
|
16
|
+
- `role="listbox"` → `aria-haspopup="listbox"`
|
|
17
|
+
- Conditionally applies correct item roles (`menuitem`, none for dialog, `option` for listbox)
|
|
18
|
+
- Pattern-specific keyboard navigation:
|
|
19
|
+
- Menu/listbox: Arrow keys, Home/End, Tab closes dropdown
|
|
20
|
+
- Dialog: Arrow keys, Tab navigates within (doesn't close)
|
|
21
|
+
- **Use case example**: Radio playlists, media controls, or complex interactive widgets that aren't semantic "menus"
|
|
22
|
+
- Added comprehensive documentation and live example (radio playlist) to demo page
|
|
23
|
+
- Updated README with ARIA pattern guidance and when to use each pattern
|
|
24
|
+
- **Backwards compatible**: Defaults to `"menu"` pattern, existing implementations unchanged
|
|
25
|
+
|
|
8
26
|
## [1.0.5] - 2025-12-21
|
|
9
27
|
|
|
10
28
|
### Fixed
|
|
@@ -134,6 +152,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
134
152
|
- Zero dependencies
|
|
135
153
|
- Full TypeScript-ready exports
|
|
136
154
|
|
|
155
|
+
[1.0.6]: https://github.com/5ulo/accessible-kit/compare/v1.0.5...v1.0.6
|
|
137
156
|
[1.0.5]: https://github.com/5ulo/accessible-kit/compare/v1.0.4...v1.0.5
|
|
138
157
|
[1.0.4]: https://github.com/5ulo/accessible-kit/compare/v1.0.3...v1.0.4
|
|
139
158
|
[1.0.3]: https://github.com/5ulo/accessible-kit/compare/v1.0.2...v1.0.3
|
package/README.md
CHANGED
|
@@ -168,11 +168,12 @@ const dropdown = new AccessibleDropdown(element, {
|
|
|
168
168
|
|
|
169
169
|
## Dropdown
|
|
170
170
|
|
|
171
|
-
Fully accessible dropdown component with keyboard navigation and ARIA support.
|
|
171
|
+
Fully accessible dropdown component with keyboard navigation and ARIA support. Supports multiple ARIA patterns (menu, dialog, listbox) for different use cases.
|
|
172
172
|
|
|
173
173
|
### Features
|
|
174
174
|
|
|
175
|
-
- ✅ Full ARIA attributes support
|
|
175
|
+
- ✅ Full ARIA attributes support (menu, dialog, and listbox patterns)
|
|
176
|
+
- ✅ Flexible role patterns via `data-dropdown-role` attribute
|
|
176
177
|
- ✅ Keyboard navigation (arrows, Enter, Space, Esc, Home, End)
|
|
177
178
|
- ✅ Focus management
|
|
178
179
|
- ✅ Auto-close on outside click
|
|
@@ -213,6 +214,57 @@ Fully accessible dropdown component with keyboard navigation and ARIA support.
|
|
|
213
214
|
</div>
|
|
214
215
|
```
|
|
215
216
|
|
|
217
|
+
### ARIA Patterns
|
|
218
|
+
|
|
219
|
+
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.
|
|
220
|
+
|
|
221
|
+
#### When to Use Each Pattern
|
|
222
|
+
|
|
223
|
+
**Menu Pattern (default)** - Use `role="menu"` for:
|
|
224
|
+
- Navigation menus
|
|
225
|
+
- Action menus (Edit, Delete, etc.)
|
|
226
|
+
- Language/region selectors
|
|
227
|
+
- User profile menus
|
|
228
|
+
- Context menus
|
|
229
|
+
|
|
230
|
+
**Dialog Pattern** - Use `data-dropdown-role="dialog"` for:
|
|
231
|
+
- Media player playlists/controls
|
|
232
|
+
- Complex interactive widgets
|
|
233
|
+
- Custom form controls
|
|
234
|
+
- Rich content with multiple focusable elements
|
|
235
|
+
- Non-menu disclosure patterns
|
|
236
|
+
|
|
237
|
+
**Example: Dialog Pattern (Radio Playlist)**
|
|
238
|
+
|
|
239
|
+
```html
|
|
240
|
+
<!-- Use data-dropdown-role="dialog" for playlists, controls, or interactive widgets -->
|
|
241
|
+
<div data-dropdown data-dropdown-role="dialog">
|
|
242
|
+
<button data-dropdown-button>
|
|
243
|
+
🎵 Playlist
|
|
244
|
+
<span data-dropdown-arrow></span>
|
|
245
|
+
</button>
|
|
246
|
+
<div data-dropdown-menu>
|
|
247
|
+
<div>
|
|
248
|
+
<div>
|
|
249
|
+
<button data-dropdown-item>Station 1</button>
|
|
250
|
+
<button data-dropdown-item>Station 2</button>
|
|
251
|
+
<button data-dropdown-item>Station 3</button>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<!-- This automatically sets:
|
|
258
|
+
- Button: aria-haspopup="dialog" (instead of "true")
|
|
259
|
+
- Menu: role="dialog" (instead of "menu")
|
|
260
|
+
- Items: No role attribute (instead of "menuitem")
|
|
261
|
+
- Keyboard: Tab allowed within dropdown (menu pattern closes on Tab)
|
|
262
|
+
-->
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Why use dialog pattern?**
|
|
266
|
+
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).
|
|
267
|
+
|
|
216
268
|
### JavaScript Initialization
|
|
217
269
|
|
|
218
270
|
```javascript
|
|
@@ -225,6 +277,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
225
277
|
|
|
226
278
|
// Or manual initialization with options
|
|
227
279
|
const dropdown = new Dropdown(element, {
|
|
280
|
+
dropdownRole: 'menu', // 'menu' (default), 'dialog', or 'listbox'
|
|
228
281
|
closeOnSelect: true,
|
|
229
282
|
closeOnOutsideClick: true,
|
|
230
283
|
closeOnEscape: true,
|
|
@@ -269,6 +322,9 @@ Dropdowns have **CSS Grid animations enabled by default**. The animation automat
|
|
|
269
322
|
**Data attributes:**
|
|
270
323
|
|
|
271
324
|
```html
|
|
325
|
+
<!-- Use dialog pattern for non-menu content -->
|
|
326
|
+
<div data-dropdown data-dropdown-role="dialog">
|
|
327
|
+
|
|
272
328
|
<!-- Keep dropdown open after selection -->
|
|
273
329
|
<div data-dropdown data-close-on-select="false">
|
|
274
330
|
|
|
@@ -283,6 +339,7 @@ Dropdowns have **CSS Grid animations enabled by default**. The animation automat
|
|
|
283
339
|
|
|
284
340
|
```javascript
|
|
285
341
|
{
|
|
342
|
+
dropdownRole: 'menu', // ARIA pattern: 'menu', 'dialog', or 'listbox'
|
|
286
343
|
closeOnSelect: true, // Close after selecting item
|
|
287
344
|
closeOnOutsideClick: true, // Close on outside click
|
|
288
345
|
closeOnEscape: true, // Close on Escape key
|
|
@@ -295,12 +352,21 @@ Dropdowns have **CSS Grid animations enabled by default**. The animation automat
|
|
|
295
352
|
|
|
296
353
|
### Keyboard Navigation
|
|
297
354
|
|
|
355
|
+
**Menu Pattern (default):**
|
|
356
|
+
|
|
298
357
|
- **Enter** / **Space** - Open/close menu or select item
|
|
299
358
|
- **↓** / **↑** - Navigate between items
|
|
300
359
|
- **Home** / **End** - Jump to first/last item
|
|
301
360
|
- **Esc** - Close menu and return focus to button
|
|
302
361
|
- **Tab** - Close menu and move focus
|
|
303
362
|
|
|
363
|
+
**Dialog Pattern:**
|
|
364
|
+
|
|
365
|
+
- **Enter** / **Space** - Open/close or select item
|
|
366
|
+
- **↓** / **↑** - Navigate between items
|
|
367
|
+
- **Esc** - Close and return focus to button
|
|
368
|
+
- **Tab** / **Shift+Tab** - Cycle through items within dialog (does not close)
|
|
369
|
+
|
|
304
370
|
### Variants
|
|
305
371
|
|
|
306
372
|
**Navigation menu:**
|
package/package.json
CHANGED
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",
|