aria-ease 3.0.3 → 4.0.1

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.
Files changed (43) hide show
  1. package/README.md +207 -87
  2. package/bin/cli.cjs +32 -0
  3. package/bin/cli.js +1 -1
  4. package/bin/{contractTestRunnerPlaywright-3VJUZSYK.js → contractTestRunnerPlaywright-EZLNNJV5.js} +32 -0
  5. package/bin/{test-D374H2ZS.js → test-45KMD4F4.js} +1 -1
  6. package/dist/{contractTestRunnerPlaywright-4UOHWGWD.js → contractTestRunnerPlaywright-UQQI5MYS.js} +32 -0
  7. package/dist/index.cjs +624 -1
  8. package/dist/index.d.cts +83 -2
  9. package/dist/index.d.ts +83 -2
  10. package/dist/index.js +589 -2
  11. package/dist/src/{Types.d-uG0Hm1yK.d.ts → Types.d-BrHSyS03.d.cts} +17 -0
  12. package/dist/src/{Types.d-uG0Hm1yK.d.cts → Types.d-BrHSyS03.d.ts} +17 -0
  13. package/dist/src/accordion/index.cjs +159 -0
  14. package/dist/src/accordion/index.d.cts +19 -2
  15. package/dist/src/accordion/index.d.ts +19 -2
  16. package/dist/src/accordion/index.js +159 -1
  17. package/dist/src/block/index.cjs +1 -1
  18. package/dist/src/block/index.d.cts +6 -2
  19. package/dist/src/block/index.d.ts +6 -2
  20. package/dist/src/block/index.js +1 -1
  21. package/dist/src/checkbox/index.cjs +129 -0
  22. package/dist/src/checkbox/index.d.cts +15 -2
  23. package/dist/src/checkbox/index.d.ts +15 -2
  24. package/dist/src/checkbox/index.js +129 -1
  25. package/dist/src/combobox/index.d.cts +1 -1
  26. package/dist/src/combobox/index.d.ts +1 -1
  27. package/dist/src/menu/index.cjs +32 -0
  28. package/dist/src/menu/index.d.cts +1 -1
  29. package/dist/src/menu/index.d.ts +1 -1
  30. package/dist/src/menu/index.js +32 -0
  31. package/dist/src/radio/index.cjs +122 -0
  32. package/dist/src/radio/index.d.cts +17 -2
  33. package/dist/src/radio/index.d.ts +17 -2
  34. package/dist/src/radio/index.js +122 -1
  35. package/dist/src/toggle/index.cjs +145 -0
  36. package/dist/src/toggle/index.d.cts +17 -2
  37. package/dist/src/toggle/index.d.ts +17 -2
  38. package/dist/src/toggle/index.js +145 -1
  39. package/dist/src/utils/test/{contractTestRunnerPlaywright-4UOHWGWD.js → contractTestRunnerPlaywright-UQQI5MYS.js} +32 -0
  40. package/dist/src/utils/test/contracts/MenuContract.json +0 -1
  41. package/dist/src/utils/test/index.cjs +32 -0
  42. package/dist/src/utils/test/index.js +1 -1
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -156,136 +156,255 @@ menu.refresh();
156
156
 
157
157
  ### 🪗 Accordion
158
158
 
159
- Updates `aria-expanded` attributes for accordion panels.
159
+ Creates accessible accordions with keyboard navigation and automatic state management.
160
+
161
+ **Features:**
162
+
163
+ - Arrow key navigation between triggers
164
+ - Automatic ARIA attribute management
165
+ - Single or multiple panel expansion
166
+ - Enter/Space to toggle panels
167
+ - Home/End key support
160
168
 
161
169
  ```javascript
162
- import { updateAccordionTriggerAriaAttributes } from "aria-ease/accordion";
170
+ import { makeAccordionAccessible } from "aria-ease/accordion";
171
+
172
+ // React Example
173
+ useEffect(() => {
174
+ const accordion = makeAccordionAccessible({
175
+ accordionId: "accordion-container",
176
+ triggersClass: "accordion-trigger",
177
+ panelsClass: "accordion-panel",
178
+ allowMultiple: false, // Only one panel open at a time (default)
179
+ });
163
180
 
164
- const accordionStates = [
165
- { expanded: true },
166
- { expanded: false },
167
- { expanded: false },
168
- ];
181
+ return () => accordion.cleanup();
182
+ }, []);
169
183
 
170
- // Call when accordion state changes
171
- updateAccordionTriggerAriaAttributes(
172
- "accordion-container", // Container ID
173
- "accordion-trigger", // Shared class for triggers
174
- accordionStates, // State array
175
- 0, // Index of trigger that changed
176
- );
184
+ // Programmatic control
185
+ accordion.expandItem(0); // Expand first panel
186
+ accordion.collapseItem(1); // Collapse second panel
187
+ accordion.toggleItem(2); // Toggle third panel
177
188
  ```
178
189
 
179
190
  **HTML structure:**
180
191
 
181
192
  ```html
182
193
  <div id="accordion-container">
183
- <button
184
- class="accordion-trigger"
185
- aria-expanded="false"
186
- aria-controls="panel-1"
187
- >
188
- Section 1
189
- </button>
190
- <div id="panel-1">Content 1</div>
191
-
192
- <button
193
- class="accordion-trigger"
194
- aria-expanded="false"
195
- aria-controls="panel-2"
196
- >
197
- Section 2
198
- </button>
199
- <div id="panel-2">Content 2</div>
194
+ <button class="accordion-trigger">Section 1</button>
195
+ <div class="accordion-panel">Content 1</div>
196
+
197
+ <button class="accordion-trigger">Section 2</button>
198
+ <div class="accordion-panel">Content 2</div>
199
+
200
+ <button class="accordion-trigger">Section 3</button>
201
+ <div class="accordion-panel">Content 3</div>
200
202
  </div>
201
203
  ```
202
204
 
205
+ <details>
206
+ <summary>📌 Legacy API (Still Supported)</summary>
207
+
208
+ ```javascript
209
+ import { updateAccordionTriggerAriaAttributes } from "aria-ease/accordion";
210
+
211
+ const accordionStates = [{ display: true }, { display: false }];
212
+
213
+ updateAccordionTriggerAriaAttributes(
214
+ "accordion-container",
215
+ "accordion-trigger",
216
+ accordionStates,
217
+ 0,
218
+ );
219
+ ```
220
+
221
+ </details>
222
+
203
223
  ---
204
224
 
205
225
  ### ✅ Checkbox
206
226
 
207
- Updates `aria-checked` attributes for custom checkboxes.
227
+ Creates accessible checkbox groups with keyboard navigation and state management.
228
+
229
+ **Features:**
230
+
231
+ - Arrow key navigation
232
+ - Space to toggle
233
+ - Independent state tracking
234
+ - Home/End key support
235
+ - Query checked states
208
236
 
209
237
  ```javascript
210
- import { updateCheckboxAriaAttributes } from "aria-ease/checkbox";
238
+ import { makeCheckboxAccessible } from "aria-ease/checkbox";
211
239
 
212
- const checkboxStates = [
213
- { checked: true },
214
- { checked: false },
215
- { checked: true },
216
- ];
217
-
218
- // Call when checkbox is toggled
219
- function handleCheckboxClick(index) {
220
- checkboxStates[index].checked = !checkboxStates[index].checked;
221
-
222
- updateCheckboxAriaAttributes(
223
- "checkbox-group",
224
- "custom-checkbox",
225
- checkboxStates,
226
- index,
227
- );
228
- }
240
+ // React Example
241
+ useEffect(() => {
242
+ const checkboxGroup = makeCheckboxAccessible({
243
+ checkboxGroupId: "checkbox-group",
244
+ checkboxesClass: "custom-checkbox",
245
+ });
246
+
247
+ return () => checkboxGroup.cleanup();
248
+ }, []);
249
+
250
+ // Programmatic control
251
+ checkboxGroup.toggleCheckbox(0); // Toggle first checkbox
252
+ checkboxGroup.setCheckboxState(1, true); // Check second checkbox
253
+ const states = checkboxGroup.getCheckedStates(); // [true, false, true]
254
+ const indices = checkboxGroup.getCheckedIndices(); // [0, 2]
229
255
  ```
230
256
 
231
257
  **HTML structure:**
232
258
 
233
259
  ```html
234
260
  <div id="checkbox-group">
235
- <div
236
- class="custom-checkbox"
237
- role="checkbox"
238
- aria-checked="false"
239
- aria-label="Option 1"
240
- ></div>
241
- <div
242
- class="custom-checkbox"
243
- role="checkbox"
244
- aria-checked="false"
245
- aria-label="Option 2"
246
- ></div>
261
+ <div class="custom-checkbox" aria-label="Option 1"></div>
262
+ <div class="custom-checkbox" aria-label="Option 2"></div>
263
+ <div class="custom-checkbox" aria-label="Option 3"></div>
247
264
  </div>
248
265
  ```
249
266
 
267
+ <details>
268
+ <summary>📌 Legacy API (Still Supported)</summary>
269
+
270
+ ```javascript
271
+ import { updateCheckboxAriaAttributes } from "aria-ease/checkbox";
272
+
273
+ const checkboxStates = [{ checked: true }, { checked: false }];
274
+
275
+ updateCheckboxAriaAttributes(
276
+ "checkbox-group",
277
+ "custom-checkbox",
278
+ checkboxStates,
279
+ 0,
280
+ );
281
+ ```
282
+
283
+ </details>
284
+
250
285
  ---
251
286
 
252
287
  ### 🔘 Radio Button
253
288
 
254
- Updates `aria-checked` attributes for custom radio buttons.
289
+ Creates accessible radio groups with keyboard navigation and automatic selection management.
255
290
 
256
- ```javascript
257
- import { updateRadioAriaAttributes } from "aria-ease/radio";
291
+ **Features:**
292
+
293
+ - Arrow key navigation (all directions)
294
+ - Automatic unchecking of other radios
295
+ - Space to select
296
+ - Home/End key support
297
+ - Single selection enforcement
258
298
 
259
- const radioStates = [{ checked: true }, { checked: false }, { checked: false }];
299
+ ```javascript
300
+ import { makeRadioAccessible } from "aria-ease/radio";
260
301
 
261
- function handleRadioSelect(index) {
262
- // Uncheck all, check selected
263
- radioStates.forEach((state, i) => {
264
- state.checked = i === index;
302
+ // React Example
303
+ useEffect(() => {
304
+ const radioGroup = makeRadioAccessible({
305
+ radioGroupId: "radio-group",
306
+ radiosClass: "custom-radio",
307
+ defaultSelectedIndex: 0, // Initially selected (optional)
265
308
  });
266
309
 
267
- updateRadioAriaAttributes("radio-group", "custom-radio", radioStates);
268
- }
310
+ return () => radioGroup.cleanup();
311
+ }, []);
312
+
313
+ // Programmatic control
314
+ radioGroup.selectRadio(2); // Select third radio
315
+ const selected = radioGroup.getSelectedIndex(); // Get current selection
269
316
  ```
270
317
 
318
+ **HTML structure:**
319
+
320
+ ```html
321
+ <div id="radio-group">
322
+ <div class="custom-radio" aria-label="Option 1"></div>
323
+ <div class="custom-radio" aria-label="Option 2"></div>
324
+ <div class="custom-radio" aria-label="Option 3"></div>
325
+ </div>
326
+ ```
327
+
328
+ <details>
329
+ <summary>📌 Legacy API (Still Supported)</summary>
330
+
331
+ ```javascript
332
+ import { updateRadioAriaAttributes } from "aria-ease/radio";
333
+
334
+ const radioStates = [{ checked: true }, { checked: false }];
335
+
336
+ updateRadioAriaAttributes("radio-group", "custom-radio", radioStates, 0);
337
+ ```
338
+
339
+ </details>
340
+
271
341
  ---
272
342
 
273
343
  ### 🔀 Toggle Button
274
344
 
275
- Updates `aria-pressed` attributes for toggle buttons.
345
+ Creates accessible toggle buttons with keyboard interactions and state management.
346
+
347
+ **Features:**
348
+
349
+ - Enter/Space to toggle
350
+ - Single toggle or toggle groups
351
+ - Arrow key navigation (groups)
352
+ - Home/End support (groups)
353
+ - Query pressed states
276
354
 
277
355
  ```javascript
278
- import { updateToggleAriaAttribute } from "aria-ease/toggle";
356
+ import { makeToggleAccessible } from "aria-ease/toggle";
279
357
 
280
- const toggleStates = [{ pressed: false }, { pressed: true }];
358
+ // Single toggle button
359
+ const toggle = makeToggleAccessible({
360
+ toggleId: "mute-button",
361
+ isSingleToggle: true,
362
+ });
281
363
 
282
- function handleToggle(index) {
283
- toggleStates[index].pressed = !toggleStates[index].pressed;
364
+ // Toggle button group
365
+ const toggleGroup = makeToggleAccessible({
366
+ toggleId: "toolbar",
367
+ togglesClass: "toggle-btn",
368
+ isSingleToggle: false,
369
+ });
284
370
 
285
- updateToggleAriaAttribute("toggle-container", "toggle-btn", toggleStates);
286
- }
371
+ // Programmatic control
372
+ toggle.toggleButton(0); // Toggle the button
373
+ toggle.setPressed(0, true); // Set pressed state
374
+ const states = toggleGroup.getPressedStates(); // [false, true, false]
375
+ const indices = toggleGroup.getPressedIndices(); // [1]
376
+
377
+ // Cleanup
378
+ toggle.cleanup();
379
+ ```
380
+
381
+ **HTML structure:**
382
+
383
+ ```html
384
+ <!-- Single toggle -->
385
+ <button id="mute-button">Mute</button>
386
+
387
+ <!-- Toggle group -->
388
+ <div id="toolbar">
389
+ <button class="toggle-btn">Bold</button>
390
+ <button class="toggle-btn">Italic</button>
391
+ <button class="toggle-btn">Underline</button>
392
+ </div>
287
393
  ```
288
394
 
395
+ <details>
396
+ <summary>📌 Legacy API (Still Supported)</summary>
397
+
398
+ ```javascript
399
+ import { updateToggleAriaAttribute } from "aria-ease/toggle";
400
+
401
+ const toggleStates = [{ pressed: false }];
402
+
403
+ updateToggleAriaAttribute("toggle-container", "toggle-btn", toggleStates, 0);
404
+ ```
405
+
406
+ </details>
407
+
289
408
  ---
290
409
 
291
410
  ### 🧱 Block (Generic Focusable Groups)
@@ -336,23 +455,24 @@ Aria-Ease is designed to be lightweight and tree-shakable:
336
455
 
337
456
  | Import | Size (ESM) |
338
457
  | ---------------------------- | --------------------- |
339
- | `aria-ease/accordion` | ~1.5KB |
340
- | `aria-ease/checkbox` | ~1.6KB |
341
- | `aria-ease/radio` | ~1.6KB |
342
- | `aria-ease/toggle` | ~1.4KB |
343
- | `aria-ease/menu` | ~3.7KB |
458
+ | `aria-ease/accordion` | ~6.5KB |
459
+ | `aria-ease/checkbox` | ~6.0KB |
460
+ | `aria-ease/radio` | ~5.5KB |
461
+ | `aria-ease/toggle` | ~6.0KB |
462
+ | `aria-ease/menu` | ~6.7KB |
344
463
  | `aria-ease/block` | ~1.7KB |
345
- | Full bundle (all components) | ~416KB (uncompressed) |
464
+ | `aria-ease/combobox` | ~8.1KB |
465
+ | Full bundle (all components) | ~459KB (uncompressed) |
346
466
 
347
467
  **💡 Tip:** Always import individual components for optimal bundle size:
348
468
 
349
469
  ```javascript
350
- // ✅ Good - only imports menu code (~3.7KB)
470
+ // ✅ Good - only imports menu code (~6.7KB)
351
471
  import { makeMenuAccessible } from "aria-ease/menu";
352
472
  //or
353
473
  import * as Block from "aria-ease/block";
354
474
 
355
- // ❌ Avoid - imports everything (~416KB)
475
+ // ❌ Avoid - imports everything (~459KB)
356
476
  import { makeMenuAccessible } from "aria-ease";
357
477
  ```
358
478
 
package/bin/cli.cjs CHANGED
@@ -13425,6 +13425,38 @@ async function runContractTestsPlaywright(componentName, url) {
13425
13425
  }
13426
13426
  }
13427
13427
  }
13428
+ let shouldSkipTest = false;
13429
+ for (const act of action) {
13430
+ if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
13431
+ const submenuSelector = componentContract.selectors[act.target];
13432
+ if (submenuSelector) {
13433
+ const submenuCount = await page.locator(submenuSelector).count();
13434
+ if (submenuCount === 0) {
13435
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - ${act.target} element not found (optional submenu test)`);
13436
+ shouldSkipTest = true;
13437
+ break;
13438
+ }
13439
+ }
13440
+ }
13441
+ }
13442
+ if (!shouldSkipTest) {
13443
+ for (const assertion of assertions) {
13444
+ if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
13445
+ const submenuSelector = componentContract.selectors[assertion.target];
13446
+ if (submenuSelector) {
13447
+ const submenuCount = await page.locator(submenuSelector).count();
13448
+ if (submenuCount === 0) {
13449
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - ${assertion.target} element not found (optional submenu test)`);
13450
+ shouldSkipTest = true;
13451
+ break;
13452
+ }
13453
+ }
13454
+ }
13455
+ }
13456
+ }
13457
+ if (shouldSkipTest) {
13458
+ continue;
13459
+ }
13428
13460
  for (const act of action) {
13429
13461
  if (act.type === "focus") {
13430
13462
  const focusSelector = componentContract.selectors[act.target];
package/bin/cli.js CHANGED
@@ -204,7 +204,7 @@ program.command("audit").description("Run axe-core powered accessibility audit o
204
204
  console.log(chalk.green("\n\u{1F389} All audits completed."));
205
205
  });
206
206
  program.command("test").description("Run core a11y accessibility standard tests on UI components").action(async () => {
207
- const { runTest } = await import("./test-D374H2ZS.js");
207
+ const { runTest } = await import("./test-45KMD4F4.js");
208
208
  runTest();
209
209
  });
210
210
  program.command("help").description("Display help information").action(() => {
@@ -152,6 +152,38 @@ async function runContractTestsPlaywright(componentName, url) {
152
152
  }
153
153
  }
154
154
  }
155
+ let shouldSkipTest = false;
156
+ for (const act of action) {
157
+ if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
158
+ const submenuSelector = componentContract.selectors[act.target];
159
+ if (submenuSelector) {
160
+ const submenuCount = await page.locator(submenuSelector).count();
161
+ if (submenuCount === 0) {
162
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - ${act.target} element not found (optional submenu test)`);
163
+ shouldSkipTest = true;
164
+ break;
165
+ }
166
+ }
167
+ }
168
+ }
169
+ if (!shouldSkipTest) {
170
+ for (const assertion of assertions) {
171
+ if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
172
+ const submenuSelector = componentContract.selectors[assertion.target];
173
+ if (submenuSelector) {
174
+ const submenuCount = await page.locator(submenuSelector).count();
175
+ if (submenuCount === 0) {
176
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - ${assertion.target} element not found (optional submenu test)`);
177
+ shouldSkipTest = true;
178
+ break;
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ if (shouldSkipTest) {
185
+ continue;
186
+ }
155
187
  for (const act of action) {
156
188
  if (act.type === "focus") {
157
189
  const focusSelector = componentContract.selectors[act.target];
@@ -12776,7 +12776,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
12776
12776
  URL must include protocol (e.g., "http://localhost:5173/test")`
12777
12777
  );
12778
12778
  }
12779
- const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-3VJUZSYK.js");
12779
+ const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-EZLNNJV5.js");
12780
12780
  contract = await runContractTestsPlaywright(componentName, url);
12781
12781
  } else {
12782
12782
  console.log(`\u{1F9EA} Running jsdom tests (limited event handling)`);
@@ -150,6 +150,38 @@ async function runContractTestsPlaywright(componentName, url) {
150
150
  }
151
151
  }
152
152
  }
153
+ let shouldSkipTest = false;
154
+ for (const act of action) {
155
+ if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
156
+ const submenuSelector = componentContract.selectors[act.target];
157
+ if (submenuSelector) {
158
+ const submenuCount = await page.locator(submenuSelector).count();
159
+ if (submenuCount === 0) {
160
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - ${act.target} element not found (optional submenu test)`);
161
+ shouldSkipTest = true;
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ }
167
+ if (!shouldSkipTest) {
168
+ for (const assertion of assertions) {
169
+ if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
170
+ const submenuSelector = componentContract.selectors[assertion.target];
171
+ if (submenuSelector) {
172
+ const submenuCount = await page.locator(submenuSelector).count();
173
+ if (submenuCount === 0) {
174
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - ${assertion.target} element not found (optional submenu test)`);
175
+ shouldSkipTest = true;
176
+ break;
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ if (shouldSkipTest) {
183
+ continue;
184
+ }
153
185
  for (const act of action) {
154
186
  if (act.type === "focus") {
155
187
  const focusSelector = componentContract.selectors[act.target];