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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "accessible-kit",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Lightweight, accessible UI component library with full ARIA support. Zero dependencies, vanilla JavaScript.",
5
5
  "main": "src/js/index.js",
6
6
  "type": "module",
@@ -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.button.setAttribute("aria-haspopup", "true");
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", "menu");
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
- item.setAttribute("role", "menuitem");
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
- this.close();
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",