aria-ease 6.5.1 → 6.7.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 +88 -24
- package/bin/{chunk-AUJAN4RK.js → chunk-2TOYEY5L.js} +87 -40
- package/bin/chunk-VPBHLMAS.js +127 -0
- package/bin/cli.cjs +403 -237
- package/bin/cli.js +8 -123
- package/bin/configLoader-XRF6VM4J.js +7 -0
- package/{dist/contractTestRunnerPlaywright-7F756CFB.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +121 -60
- package/bin/{test-C3CMRHSI.js → test-WRIJHN6H.js} +65 -24
- package/dist/{chunk-AUJAN4RK.js → chunk-2TOYEY5L.js} +87 -40
- package/dist/configLoader-IT4PWCJB.js +128 -0
- package/{bin/contractTestRunnerPlaywright-7F756CFB.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +121 -60
- package/dist/index.cjs +471 -137
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +127 -35
- package/dist/src/menu/index.cjs +62 -11
- package/dist/src/menu/index.js +62 -11
- package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +8 -8
- package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +4 -4
- package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +172 -34
- package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +10 -10
- package/dist/src/utils/test/{chunk-AUJAN4RK.js → chunk-2TOYEY5L.js} +85 -41
- package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-HL73FADJ.js → contractTestRunnerPlaywright-IRJOAEMT.js} +117 -59
- package/dist/src/utils/test/index.cjs +403 -125
- package/dist/src/utils/test/index.d.cts +7 -1
- package/dist/src/utils/test/index.d.ts +7 -1
- package/dist/src/utils/test/index.js +61 -23
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -194,6 +194,8 @@ declare function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, lis
|
|
|
194
194
|
|
|
195
195
|
declare function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation, activateOnFocus, callback }: TabsConfig): AccessibilityInstance;
|
|
196
196
|
|
|
197
|
+
type StrictnessMode = 'minimal' | 'balanced' | 'strict' | 'paranoid';
|
|
198
|
+
|
|
197
199
|
/**
|
|
198
200
|
* Runs static and interactions accessibility test on UI components.
|
|
199
201
|
* @param {string} componentName The name of the component contract to test against
|
|
@@ -201,7 +203,10 @@ declare function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orie
|
|
|
201
203
|
* @param {string} url Optional URL to run full Playwright E2E tests. If omitted, uses isolated component testing with page.setContent()
|
|
202
204
|
*/
|
|
203
205
|
|
|
204
|
-
|
|
206
|
+
type TestAuditOptions = {
|
|
207
|
+
strictness?: StrictnessMode;
|
|
208
|
+
};
|
|
209
|
+
declare function testUiComponent(componentName: string, component: HTMLElement | null, url: string | null, options?: TestAuditOptions): Promise<JestAxeResult>;
|
|
205
210
|
/**
|
|
206
211
|
* Cleanup function to close the shared Playwright browser
|
|
207
212
|
* Call this in afterAll() or after all tests complete
|
package/dist/index.d.ts
CHANGED
|
@@ -194,6 +194,8 @@ declare function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, lis
|
|
|
194
194
|
|
|
195
195
|
declare function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation, activateOnFocus, callback }: TabsConfig): AccessibilityInstance;
|
|
196
196
|
|
|
197
|
+
type StrictnessMode = 'minimal' | 'balanced' | 'strict' | 'paranoid';
|
|
198
|
+
|
|
197
199
|
/**
|
|
198
200
|
* Runs static and interactions accessibility test on UI components.
|
|
199
201
|
* @param {string} componentName The name of the component contract to test against
|
|
@@ -201,7 +203,10 @@ declare function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orie
|
|
|
201
203
|
* @param {string} url Optional URL to run full Playwright E2E tests. If omitted, uses isolated component testing with page.setContent()
|
|
202
204
|
*/
|
|
203
205
|
|
|
204
|
-
|
|
206
|
+
type TestAuditOptions = {
|
|
207
|
+
strictness?: StrictnessMode;
|
|
208
|
+
};
|
|
209
|
+
declare function testUiComponent(componentName: string, component: HTMLElement | null, url: string | null, options?: TestAuditOptions): Promise<JestAxeResult>;
|
|
205
210
|
/**
|
|
206
211
|
* Cleanup function to close the shared Playwright browser
|
|
207
212
|
* Call this in afterAll() or after all tests complete
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ContractReporter,
|
|
3
3
|
closeSharedBrowser,
|
|
4
|
-
contract_default
|
|
5
|
-
|
|
4
|
+
contract_default,
|
|
5
|
+
normalizeLevel,
|
|
6
|
+
normalizeStrictness,
|
|
7
|
+
resolveEnforcement
|
|
8
|
+
} from "./chunk-2TOYEY5L.js";
|
|
6
9
|
import "./chunk-I2KLQ2HA.js";
|
|
7
10
|
|
|
8
11
|
// src/accordion/src/makeAccordionAccessible/makeAccordionAccessible.ts
|
|
@@ -464,11 +467,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
464
467
|
for (let i = 0; i < allItems.length; i++) {
|
|
465
468
|
const item = allItems.item(i);
|
|
466
469
|
const isNested = isItemInNestedSubmenu(item);
|
|
470
|
+
const isDisabled = item.getAttribute("aria-disabled") === "true";
|
|
467
471
|
if (!isNested) {
|
|
468
472
|
if (!item.hasAttribute("tabindex")) {
|
|
469
473
|
item.setAttribute("tabindex", "-1");
|
|
470
474
|
}
|
|
471
|
-
|
|
475
|
+
if (!isDisabled) {
|
|
476
|
+
filteredItems.push(item);
|
|
477
|
+
}
|
|
472
478
|
}
|
|
473
479
|
}
|
|
474
480
|
}
|
|
@@ -493,9 +499,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
493
499
|
const items = getItems();
|
|
494
500
|
items.forEach((item) => {
|
|
495
501
|
item.setAttribute("role", "menuitem");
|
|
496
|
-
|
|
502
|
+
const submenuId = item.getAttribute("data-submenu-id") ?? item.getAttribute("aria-controls");
|
|
503
|
+
const hasSubmenuTriggerAttributes = item.hasAttribute("aria-haspopup") && submenuId;
|
|
504
|
+
if (submenuId && (item.hasAttribute("data-submenu-id") || hasSubmenuTriggerAttributes)) {
|
|
497
505
|
item.setAttribute("aria-haspopup", "menu");
|
|
498
|
-
item.setAttribute("aria-controls",
|
|
506
|
+
item.setAttribute("aria-controls", submenuId);
|
|
507
|
+
if (!item.hasAttribute("aria-expanded")) {
|
|
508
|
+
item.setAttribute("aria-expanded", "false");
|
|
509
|
+
}
|
|
499
510
|
}
|
|
500
511
|
});
|
|
501
512
|
}
|
|
@@ -504,24 +515,43 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
504
515
|
const nextIndex = (currentIndex + direction + len) % len;
|
|
505
516
|
elementItems.item(nextIndex).focus();
|
|
506
517
|
}
|
|
518
|
+
function focusItemAtIndex(items, index) {
|
|
519
|
+
if (items.length === 0) return;
|
|
520
|
+
items[index]?.focus();
|
|
521
|
+
}
|
|
507
522
|
function hasSubmenu(menuItem) {
|
|
508
523
|
return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
|
|
509
524
|
}
|
|
525
|
+
function closeAncestorMenusFromTrigger(triggerEl) {
|
|
526
|
+
let currentTrigger = triggerEl;
|
|
527
|
+
while (currentTrigger && currentTrigger.getAttribute("role") === "menuitem") {
|
|
528
|
+
const parentMenu = currentTrigger.closest('[role="menu"]');
|
|
529
|
+
if (!parentMenu) break;
|
|
530
|
+
parentMenu.style.display = "none";
|
|
531
|
+
currentTrigger.setAttribute("aria-expanded", "false");
|
|
532
|
+
const parentTriggerId = parentMenu.getAttribute("aria-labelledby");
|
|
533
|
+
if (!parentTriggerId) break;
|
|
534
|
+
const nextTrigger = document.getElementById(parentTriggerId);
|
|
535
|
+
if (!nextTrigger) break;
|
|
536
|
+
currentTrigger = nextTrigger;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
510
539
|
intializeMenuItems();
|
|
511
540
|
function handleItemsKeydown(event, menuItem, menuItemIndex) {
|
|
512
541
|
switch (event.key) {
|
|
513
|
-
case "ArrowUp":
|
|
514
542
|
case "ArrowLeft": {
|
|
515
543
|
if (event.key === "ArrowLeft" && triggerButton.getAttribute("role") === "menuitem") {
|
|
516
544
|
event.preventDefault();
|
|
517
545
|
closeMenu();
|
|
518
546
|
return;
|
|
519
547
|
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case "ArrowUp": {
|
|
520
551
|
event.preventDefault();
|
|
521
552
|
moveFocus2(toNodeListLike(getFilteredItems()), menuItemIndex, -1);
|
|
522
553
|
break;
|
|
523
554
|
}
|
|
524
|
-
case "ArrowDown":
|
|
525
555
|
case "ArrowRight": {
|
|
526
556
|
if (event.key === "ArrowRight" && hasSubmenu(menuItem)) {
|
|
527
557
|
event.preventDefault();
|
|
@@ -531,10 +561,24 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
531
561
|
return;
|
|
532
562
|
}
|
|
533
563
|
}
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case "ArrowDown": {
|
|
534
567
|
event.preventDefault();
|
|
535
568
|
moveFocus2(toNodeListLike(getFilteredItems()), menuItemIndex, 1);
|
|
536
569
|
break;
|
|
537
570
|
}
|
|
571
|
+
case "Home": {
|
|
572
|
+
event.preventDefault();
|
|
573
|
+
focusItemAtIndex(getFilteredItems(), 0);
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
case "End": {
|
|
577
|
+
event.preventDefault();
|
|
578
|
+
const items = getFilteredItems();
|
|
579
|
+
focusItemAtIndex(items, items.length - 1);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
538
582
|
case "Escape": {
|
|
539
583
|
event.preventDefault();
|
|
540
584
|
closeMenu();
|
|
@@ -547,15 +591,25 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
547
591
|
case "Enter":
|
|
548
592
|
case " ": {
|
|
549
593
|
event.preventDefault();
|
|
594
|
+
if (hasSubmenu(menuItem)) {
|
|
595
|
+
const submenuId = menuItem.getAttribute("aria-controls");
|
|
596
|
+
if (submenuId) {
|
|
597
|
+
openSubmenu(submenuId);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
550
601
|
menuItem.click();
|
|
602
|
+
closeMenu();
|
|
603
|
+
if (onOpenChange) {
|
|
604
|
+
onOpenChange(false);
|
|
605
|
+
}
|
|
551
606
|
break;
|
|
552
607
|
}
|
|
553
608
|
case "Tab": {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
609
|
+
closeMenu();
|
|
610
|
+
closeAncestorMenusFromTrigger(triggerButton);
|
|
611
|
+
if (onOpenChange) {
|
|
612
|
+
onOpenChange(false);
|
|
559
613
|
}
|
|
560
614
|
break;
|
|
561
615
|
}
|
|
@@ -656,6 +710,7 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
656
710
|
}
|
|
657
711
|
}
|
|
658
712
|
function closeMenu() {
|
|
713
|
+
submenuInstances.forEach((instance) => instance.closeMenu());
|
|
659
714
|
setAria(false);
|
|
660
715
|
menuDiv.style.display = "none";
|
|
661
716
|
removeListeners();
|
|
@@ -687,7 +742,6 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
687
742
|
}
|
|
688
743
|
triggerButton.addEventListener("click", handleTriggerClick);
|
|
689
744
|
document.addEventListener("click", handleClickOutside);
|
|
690
|
-
triggerButton.setAttribute("data-menu-initialized", "true");
|
|
691
745
|
function cleanup() {
|
|
692
746
|
removeListeners();
|
|
693
747
|
triggerButton.removeEventListener("click", handleTriggerClick);
|
|
@@ -1445,8 +1499,9 @@ import { axe } from "jest-axe";
|
|
|
1445
1499
|
|
|
1446
1500
|
// src/utils/test/src/contractTestRunner.ts
|
|
1447
1501
|
import fs from "fs/promises";
|
|
1448
|
-
async function runContractTests(componentName, component) {
|
|
1502
|
+
async function runContractTests(componentName, component, strictness) {
|
|
1449
1503
|
const reporter = new ContractReporter(false);
|
|
1504
|
+
const strictnessMode = normalizeStrictness(strictness);
|
|
1450
1505
|
const contractTyped = contract_default;
|
|
1451
1506
|
const contractPath = contractTyped[componentName]?.path;
|
|
1452
1507
|
if (!contractPath) {
|
|
@@ -1460,19 +1515,42 @@ async function runContractTests(componentName, component) {
|
|
|
1460
1515
|
const failures = [];
|
|
1461
1516
|
const passes = [];
|
|
1462
1517
|
const skipped = [];
|
|
1463
|
-
const
|
|
1518
|
+
const warnings = [];
|
|
1519
|
+
const classifyFailure = (message, levelRaw) => {
|
|
1520
|
+
const level = normalizeLevel(levelRaw);
|
|
1521
|
+
const enforcement = resolveEnforcement(level, strictnessMode);
|
|
1522
|
+
if (enforcement === "error") {
|
|
1523
|
+
failures.push(message);
|
|
1524
|
+
return { status: "fail", level, detail: message };
|
|
1525
|
+
}
|
|
1526
|
+
if (enforcement === "warning") {
|
|
1527
|
+
warnings.push(message);
|
|
1528
|
+
return { status: "warn", level, detail: message };
|
|
1529
|
+
}
|
|
1530
|
+
const ignoredMessage = `${message} (ignored by strictness=${strictnessMode}, level=${level})`;
|
|
1531
|
+
skipped.push(ignoredMessage);
|
|
1532
|
+
return { status: "skip", level, detail: ignoredMessage };
|
|
1533
|
+
};
|
|
1534
|
+
let staticPassed = 0;
|
|
1535
|
+
let staticFailed = 0;
|
|
1536
|
+
let staticWarnings = 0;
|
|
1464
1537
|
for (const test of componentContract.static[0].assertions) {
|
|
1465
1538
|
if (test.target !== "relative") {
|
|
1539
|
+
const staticLevel = normalizeLevel(test.level);
|
|
1466
1540
|
const selector = componentContract.selectors[test.target];
|
|
1467
1541
|
if (!selector) {
|
|
1468
|
-
|
|
1469
|
-
|
|
1542
|
+
const outcome = classifyFailure(`Selector for target ${test.target} not found.`, test.level);
|
|
1543
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1544
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1545
|
+
reporter.reportStaticTest(`${test.target} has required ARIA attributes`, outcome.status, outcome.detail, outcome.level);
|
|
1470
1546
|
continue;
|
|
1471
1547
|
}
|
|
1472
1548
|
const target = component.querySelector(selector);
|
|
1473
1549
|
if (!target) {
|
|
1474
|
-
|
|
1475
|
-
|
|
1550
|
+
const outcome = classifyFailure(`Target ${test.target} not found.`, test.level);
|
|
1551
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1552
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1553
|
+
reporter.reportStaticTest(`${test.target} has required ARIA attributes`, outcome.status, outcome.detail, outcome.level);
|
|
1476
1554
|
continue;
|
|
1477
1555
|
}
|
|
1478
1556
|
const attributeValue = target.getAttribute(test.attribute);
|
|
@@ -1480,35 +1558,38 @@ async function runContractTests(componentName, component) {
|
|
|
1480
1558
|
const attributes = test.attribute.split(" | ");
|
|
1481
1559
|
const hasAnyAttribute = attributes.some((attr) => target.hasAttribute(attr));
|
|
1482
1560
|
if (!hasAnyAttribute) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1561
|
+
const outcome = classifyFailure(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`, test.level);
|
|
1562
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1563
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1564
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}`, outcome.status, outcome.detail, outcome.level);
|
|
1485
1565
|
} else {
|
|
1486
1566
|
passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
|
|
1487
|
-
|
|
1567
|
+
staticPassed += 1;
|
|
1568
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}`, "pass", void 0, staticLevel);
|
|
1488
1569
|
}
|
|
1489
1570
|
} else if (!attributeValue || !test.expectedValue.split(" | ").includes(attributeValue)) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1571
|
+
const outcome = classifyFailure(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`, test.level);
|
|
1572
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1573
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1574
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}="${test.expectedValue}"`, outcome.status, outcome.detail, outcome.level);
|
|
1492
1575
|
} else {
|
|
1493
1576
|
passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
|
|
1494
|
-
|
|
1577
|
+
staticPassed += 1;
|
|
1578
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}="${attributeValue}"`, "pass", void 0, staticLevel);
|
|
1495
1579
|
}
|
|
1496
1580
|
}
|
|
1497
1581
|
}
|
|
1498
1582
|
for (const dynamicTest of componentContract.dynamic) {
|
|
1499
1583
|
skipped.push(dynamicTest.description);
|
|
1500
|
-
reporter.reportTest(dynamicTest, "skip");
|
|
1584
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicTest.level }, "skip");
|
|
1501
1585
|
}
|
|
1502
|
-
|
|
1503
|
-
const staticFailed = failures.length - failuresBeforeStatic;
|
|
1504
|
-
const staticPassed = Math.max(0, staticTotal - staticFailed);
|
|
1505
|
-
reporter.reportStatic(staticPassed, staticFailed);
|
|
1586
|
+
reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
|
|
1506
1587
|
reporter.summary(failures);
|
|
1507
|
-
return { passes, failures, skipped };
|
|
1588
|
+
return { passes, failures, skipped, warnings };
|
|
1508
1589
|
}
|
|
1509
1590
|
|
|
1510
1591
|
// src/utils/test/src/test.ts
|
|
1511
|
-
async function testUiComponent(componentName, component, url) {
|
|
1592
|
+
async function testUiComponent(componentName, component, url, options = {}) {
|
|
1512
1593
|
if (!componentName || typeof componentName !== "string") {
|
|
1513
1594
|
throw new Error("\u274C testUiComponent requires a valid componentName (string)");
|
|
1514
1595
|
}
|
|
@@ -1545,14 +1626,25 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
1545
1626
|
}
|
|
1546
1627
|
return null;
|
|
1547
1628
|
}
|
|
1629
|
+
let strictness = normalizeStrictness(options.strictness);
|
|
1630
|
+
if (options.strictness === void 0 && typeof window === "undefined") {
|
|
1631
|
+
try {
|
|
1632
|
+
const { loadConfig } = await import("./configLoader-IT4PWCJB.js");
|
|
1633
|
+
const { config } = await loadConfig(process.cwd());
|
|
1634
|
+
const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
|
|
1635
|
+
strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
|
|
1636
|
+
} catch {
|
|
1637
|
+
strictness = "balanced";
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1548
1640
|
let contract;
|
|
1549
1641
|
try {
|
|
1550
1642
|
if (url) {
|
|
1551
1643
|
const devServerUrl = await checkDevServer(url);
|
|
1552
1644
|
if (devServerUrl) {
|
|
1553
1645
|
console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
|
|
1554
|
-
const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-
|
|
1555
|
-
contract = await runContractTestsPlaywright(componentName, devServerUrl);
|
|
1646
|
+
const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-UAOFNS7Z.js");
|
|
1647
|
+
contract = await runContractTestsPlaywright(componentName, devServerUrl, strictness);
|
|
1556
1648
|
} else {
|
|
1557
1649
|
throw new Error(
|
|
1558
1650
|
`\u274C Dev server not running at ${url}
|
|
@@ -1561,7 +1653,7 @@ Please start your dev server and try again.`
|
|
|
1561
1653
|
}
|
|
1562
1654
|
} else if (component) {
|
|
1563
1655
|
console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
|
|
1564
|
-
contract = await runContractTests(componentName, component);
|
|
1656
|
+
contract = await runContractTests(componentName, component, strictness);
|
|
1565
1657
|
} else {
|
|
1566
1658
|
throw new Error("\u274C Either component or URL must be provided");
|
|
1567
1659
|
}
|
package/dist/src/menu/index.cjs
CHANGED
|
@@ -47,11 +47,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
47
47
|
for (let i = 0; i < allItems.length; i++) {
|
|
48
48
|
const item = allItems.item(i);
|
|
49
49
|
const isNested = isItemInNestedSubmenu(item);
|
|
50
|
+
const isDisabled = item.getAttribute("aria-disabled") === "true";
|
|
50
51
|
if (!isNested) {
|
|
51
52
|
if (!item.hasAttribute("tabindex")) {
|
|
52
53
|
item.setAttribute("tabindex", "-1");
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
if (!isDisabled) {
|
|
56
|
+
filteredItems.push(item);
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
}
|
|
@@ -76,9 +79,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
76
79
|
const items = getItems();
|
|
77
80
|
items.forEach((item) => {
|
|
78
81
|
item.setAttribute("role", "menuitem");
|
|
79
|
-
|
|
82
|
+
const submenuId = item.getAttribute("data-submenu-id") ?? item.getAttribute("aria-controls");
|
|
83
|
+
const hasSubmenuTriggerAttributes = item.hasAttribute("aria-haspopup") && submenuId;
|
|
84
|
+
if (submenuId && (item.hasAttribute("data-submenu-id") || hasSubmenuTriggerAttributes)) {
|
|
80
85
|
item.setAttribute("aria-haspopup", "menu");
|
|
81
|
-
item.setAttribute("aria-controls",
|
|
86
|
+
item.setAttribute("aria-controls", submenuId);
|
|
87
|
+
if (!item.hasAttribute("aria-expanded")) {
|
|
88
|
+
item.setAttribute("aria-expanded", "false");
|
|
89
|
+
}
|
|
82
90
|
}
|
|
83
91
|
});
|
|
84
92
|
}
|
|
@@ -87,24 +95,43 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
87
95
|
const nextIndex = (currentIndex + direction + len) % len;
|
|
88
96
|
elementItems.item(nextIndex).focus();
|
|
89
97
|
}
|
|
98
|
+
function focusItemAtIndex(items, index) {
|
|
99
|
+
if (items.length === 0) return;
|
|
100
|
+
items[index]?.focus();
|
|
101
|
+
}
|
|
90
102
|
function hasSubmenu(menuItem) {
|
|
91
103
|
return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
|
|
92
104
|
}
|
|
105
|
+
function closeAncestorMenusFromTrigger(triggerEl) {
|
|
106
|
+
let currentTrigger = triggerEl;
|
|
107
|
+
while (currentTrigger && currentTrigger.getAttribute("role") === "menuitem") {
|
|
108
|
+
const parentMenu = currentTrigger.closest('[role="menu"]');
|
|
109
|
+
if (!parentMenu) break;
|
|
110
|
+
parentMenu.style.display = "none";
|
|
111
|
+
currentTrigger.setAttribute("aria-expanded", "false");
|
|
112
|
+
const parentTriggerId = parentMenu.getAttribute("aria-labelledby");
|
|
113
|
+
if (!parentTriggerId) break;
|
|
114
|
+
const nextTrigger = document.getElementById(parentTriggerId);
|
|
115
|
+
if (!nextTrigger) break;
|
|
116
|
+
currentTrigger = nextTrigger;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
93
119
|
intializeMenuItems();
|
|
94
120
|
function handleItemsKeydown(event, menuItem, menuItemIndex) {
|
|
95
121
|
switch (event.key) {
|
|
96
|
-
case "ArrowUp":
|
|
97
122
|
case "ArrowLeft": {
|
|
98
123
|
if (event.key === "ArrowLeft" && triggerButton.getAttribute("role") === "menuitem") {
|
|
99
124
|
event.preventDefault();
|
|
100
125
|
closeMenu();
|
|
101
126
|
return;
|
|
102
127
|
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case "ArrowUp": {
|
|
103
131
|
event.preventDefault();
|
|
104
132
|
moveFocus(toNodeListLike(getFilteredItems()), menuItemIndex, -1);
|
|
105
133
|
break;
|
|
106
134
|
}
|
|
107
|
-
case "ArrowDown":
|
|
108
135
|
case "ArrowRight": {
|
|
109
136
|
if (event.key === "ArrowRight" && hasSubmenu(menuItem)) {
|
|
110
137
|
event.preventDefault();
|
|
@@ -114,10 +141,24 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
114
141
|
return;
|
|
115
142
|
}
|
|
116
143
|
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case "ArrowDown": {
|
|
117
147
|
event.preventDefault();
|
|
118
148
|
moveFocus(toNodeListLike(getFilteredItems()), menuItemIndex, 1);
|
|
119
149
|
break;
|
|
120
150
|
}
|
|
151
|
+
case "Home": {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
focusItemAtIndex(getFilteredItems(), 0);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "End": {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
const items = getFilteredItems();
|
|
159
|
+
focusItemAtIndex(items, items.length - 1);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
121
162
|
case "Escape": {
|
|
122
163
|
event.preventDefault();
|
|
123
164
|
closeMenu();
|
|
@@ -130,15 +171,25 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
130
171
|
case "Enter":
|
|
131
172
|
case " ": {
|
|
132
173
|
event.preventDefault();
|
|
174
|
+
if (hasSubmenu(menuItem)) {
|
|
175
|
+
const submenuId = menuItem.getAttribute("aria-controls");
|
|
176
|
+
if (submenuId) {
|
|
177
|
+
openSubmenu(submenuId);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
133
181
|
menuItem.click();
|
|
182
|
+
closeMenu();
|
|
183
|
+
if (onOpenChange) {
|
|
184
|
+
onOpenChange(false);
|
|
185
|
+
}
|
|
134
186
|
break;
|
|
135
187
|
}
|
|
136
188
|
case "Tab": {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
189
|
+
closeMenu();
|
|
190
|
+
closeAncestorMenusFromTrigger(triggerButton);
|
|
191
|
+
if (onOpenChange) {
|
|
192
|
+
onOpenChange(false);
|
|
142
193
|
}
|
|
143
194
|
break;
|
|
144
195
|
}
|
|
@@ -237,6 +288,7 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
237
288
|
}
|
|
238
289
|
}
|
|
239
290
|
function closeMenu() {
|
|
291
|
+
submenuInstances.forEach((instance) => instance.closeMenu());
|
|
240
292
|
setAria(false);
|
|
241
293
|
menuDiv.style.display = "none";
|
|
242
294
|
removeListeners();
|
|
@@ -268,7 +320,6 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
268
320
|
}
|
|
269
321
|
triggerButton.addEventListener("click", handleTriggerClick);
|
|
270
322
|
document.addEventListener("click", handleClickOutside);
|
|
271
|
-
triggerButton.setAttribute("data-menu-initialized", "true");
|
|
272
323
|
function cleanup() {
|
|
273
324
|
removeListeners();
|
|
274
325
|
triggerButton.removeEventListener("click", handleTriggerClick);
|
package/dist/src/menu/index.js
CHANGED
|
@@ -45,11 +45,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
45
45
|
for (let i = 0; i < allItems.length; i++) {
|
|
46
46
|
const item = allItems.item(i);
|
|
47
47
|
const isNested = isItemInNestedSubmenu(item);
|
|
48
|
+
const isDisabled = item.getAttribute("aria-disabled") === "true";
|
|
48
49
|
if (!isNested) {
|
|
49
50
|
if (!item.hasAttribute("tabindex")) {
|
|
50
51
|
item.setAttribute("tabindex", "-1");
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
if (!isDisabled) {
|
|
54
|
+
filteredItems.push(item);
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -74,9 +77,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
74
77
|
const items = getItems();
|
|
75
78
|
items.forEach((item) => {
|
|
76
79
|
item.setAttribute("role", "menuitem");
|
|
77
|
-
|
|
80
|
+
const submenuId = item.getAttribute("data-submenu-id") ?? item.getAttribute("aria-controls");
|
|
81
|
+
const hasSubmenuTriggerAttributes = item.hasAttribute("aria-haspopup") && submenuId;
|
|
82
|
+
if (submenuId && (item.hasAttribute("data-submenu-id") || hasSubmenuTriggerAttributes)) {
|
|
78
83
|
item.setAttribute("aria-haspopup", "menu");
|
|
79
|
-
item.setAttribute("aria-controls",
|
|
84
|
+
item.setAttribute("aria-controls", submenuId);
|
|
85
|
+
if (!item.hasAttribute("aria-expanded")) {
|
|
86
|
+
item.setAttribute("aria-expanded", "false");
|
|
87
|
+
}
|
|
80
88
|
}
|
|
81
89
|
});
|
|
82
90
|
}
|
|
@@ -85,24 +93,43 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
85
93
|
const nextIndex = (currentIndex + direction + len) % len;
|
|
86
94
|
elementItems.item(nextIndex).focus();
|
|
87
95
|
}
|
|
96
|
+
function focusItemAtIndex(items, index) {
|
|
97
|
+
if (items.length === 0) return;
|
|
98
|
+
items[index]?.focus();
|
|
99
|
+
}
|
|
88
100
|
function hasSubmenu(menuItem) {
|
|
89
101
|
return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
|
|
90
102
|
}
|
|
103
|
+
function closeAncestorMenusFromTrigger(triggerEl) {
|
|
104
|
+
let currentTrigger = triggerEl;
|
|
105
|
+
while (currentTrigger && currentTrigger.getAttribute("role") === "menuitem") {
|
|
106
|
+
const parentMenu = currentTrigger.closest('[role="menu"]');
|
|
107
|
+
if (!parentMenu) break;
|
|
108
|
+
parentMenu.style.display = "none";
|
|
109
|
+
currentTrigger.setAttribute("aria-expanded", "false");
|
|
110
|
+
const parentTriggerId = parentMenu.getAttribute("aria-labelledby");
|
|
111
|
+
if (!parentTriggerId) break;
|
|
112
|
+
const nextTrigger = document.getElementById(parentTriggerId);
|
|
113
|
+
if (!nextTrigger) break;
|
|
114
|
+
currentTrigger = nextTrigger;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
91
117
|
intializeMenuItems();
|
|
92
118
|
function handleItemsKeydown(event, menuItem, menuItemIndex) {
|
|
93
119
|
switch (event.key) {
|
|
94
|
-
case "ArrowUp":
|
|
95
120
|
case "ArrowLeft": {
|
|
96
121
|
if (event.key === "ArrowLeft" && triggerButton.getAttribute("role") === "menuitem") {
|
|
97
122
|
event.preventDefault();
|
|
98
123
|
closeMenu();
|
|
99
124
|
return;
|
|
100
125
|
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "ArrowUp": {
|
|
101
129
|
event.preventDefault();
|
|
102
130
|
moveFocus(toNodeListLike(getFilteredItems()), menuItemIndex, -1);
|
|
103
131
|
break;
|
|
104
132
|
}
|
|
105
|
-
case "ArrowDown":
|
|
106
133
|
case "ArrowRight": {
|
|
107
134
|
if (event.key === "ArrowRight" && hasSubmenu(menuItem)) {
|
|
108
135
|
event.preventDefault();
|
|
@@ -112,10 +139,24 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
112
139
|
return;
|
|
113
140
|
}
|
|
114
141
|
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "ArrowDown": {
|
|
115
145
|
event.preventDefault();
|
|
116
146
|
moveFocus(toNodeListLike(getFilteredItems()), menuItemIndex, 1);
|
|
117
147
|
break;
|
|
118
148
|
}
|
|
149
|
+
case "Home": {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
focusItemAtIndex(getFilteredItems(), 0);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "End": {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
const items = getFilteredItems();
|
|
157
|
+
focusItemAtIndex(items, items.length - 1);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
119
160
|
case "Escape": {
|
|
120
161
|
event.preventDefault();
|
|
121
162
|
closeMenu();
|
|
@@ -128,15 +169,25 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
128
169
|
case "Enter":
|
|
129
170
|
case " ": {
|
|
130
171
|
event.preventDefault();
|
|
172
|
+
if (hasSubmenu(menuItem)) {
|
|
173
|
+
const submenuId = menuItem.getAttribute("aria-controls");
|
|
174
|
+
if (submenuId) {
|
|
175
|
+
openSubmenu(submenuId);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
131
179
|
menuItem.click();
|
|
180
|
+
closeMenu();
|
|
181
|
+
if (onOpenChange) {
|
|
182
|
+
onOpenChange(false);
|
|
183
|
+
}
|
|
132
184
|
break;
|
|
133
185
|
}
|
|
134
186
|
case "Tab": {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
187
|
+
closeMenu();
|
|
188
|
+
closeAncestorMenusFromTrigger(triggerButton);
|
|
189
|
+
if (onOpenChange) {
|
|
190
|
+
onOpenChange(false);
|
|
140
191
|
}
|
|
141
192
|
break;
|
|
142
193
|
}
|
|
@@ -235,6 +286,7 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
235
286
|
}
|
|
236
287
|
}
|
|
237
288
|
function closeMenu() {
|
|
289
|
+
submenuInstances.forEach((instance) => instance.closeMenu());
|
|
238
290
|
setAria(false);
|
|
239
291
|
menuDiv.style.display = "none";
|
|
240
292
|
removeListeners();
|
|
@@ -266,7 +318,6 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
266
318
|
}
|
|
267
319
|
triggerButton.addEventListener("click", handleTriggerClick);
|
|
268
320
|
document.addEventListener("click", handleClickOutside);
|
|
269
|
-
triggerButton.setAttribute("data-menu-initialized", "true");
|
|
270
321
|
function cleanup() {
|
|
271
322
|
removeListeners();
|
|
272
323
|
triggerButton.removeEventListener("click", handleTriggerClick);
|