aria-ease 6.6.0 → 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 +75 -15
- package/bin/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
- package/bin/chunk-VPBHLMAS.js +127 -0
- package/bin/cli.cjs +380 -231
- package/bin/cli.js +8 -123
- package/bin/configLoader-XRF6VM4J.js +7 -0
- package/{dist/contractTestRunnerPlaywright-PC6JOYYV.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
- package/bin/{test-LP723IXM.js → test-WRIJHN6H.js} +65 -24
- package/dist/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
- package/dist/configLoader-IT4PWCJB.js +128 -0
- package/{bin/contractTestRunnerPlaywright-PC6JOYYV.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
- package/dist/index.cjs +404 -125
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +83 -29
- package/dist/src/menu/index.cjs +18 -5
- package/dist/src/menu/index.js +18 -5
- 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 +44 -19
- package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +3 -3
- package/dist/src/utils/test/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +85 -36
- package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-RGKMGXND.js → contractTestRunnerPlaywright-IRJOAEMT.js} +94 -58
- package/dist/src/utils/test/index.cjs +380 -119
- 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
|
|
@@ -519,6 +522,20 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
519
522
|
function hasSubmenu(menuItem) {
|
|
520
523
|
return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
|
|
521
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
|
+
}
|
|
522
539
|
intializeMenuItems();
|
|
523
540
|
function handleItemsKeydown(event, menuItem, menuItemIndex) {
|
|
524
541
|
switch (event.key) {
|
|
@@ -589,11 +606,10 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
589
606
|
break;
|
|
590
607
|
}
|
|
591
608
|
case "Tab": {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
609
|
+
closeMenu();
|
|
610
|
+
closeAncestorMenusFromTrigger(triggerButton);
|
|
611
|
+
if (onOpenChange) {
|
|
612
|
+
onOpenChange(false);
|
|
597
613
|
}
|
|
598
614
|
break;
|
|
599
615
|
}
|
|
@@ -1483,8 +1499,9 @@ import { axe } from "jest-axe";
|
|
|
1483
1499
|
|
|
1484
1500
|
// src/utils/test/src/contractTestRunner.ts
|
|
1485
1501
|
import fs from "fs/promises";
|
|
1486
|
-
async function runContractTests(componentName, component) {
|
|
1502
|
+
async function runContractTests(componentName, component, strictness) {
|
|
1487
1503
|
const reporter = new ContractReporter(false);
|
|
1504
|
+
const strictnessMode = normalizeStrictness(strictness);
|
|
1488
1505
|
const contractTyped = contract_default;
|
|
1489
1506
|
const contractPath = contractTyped[componentName]?.path;
|
|
1490
1507
|
if (!contractPath) {
|
|
@@ -1498,19 +1515,42 @@ async function runContractTests(componentName, component) {
|
|
|
1498
1515
|
const failures = [];
|
|
1499
1516
|
const passes = [];
|
|
1500
1517
|
const skipped = [];
|
|
1501
|
-
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;
|
|
1502
1537
|
for (const test of componentContract.static[0].assertions) {
|
|
1503
1538
|
if (test.target !== "relative") {
|
|
1539
|
+
const staticLevel = normalizeLevel(test.level);
|
|
1504
1540
|
const selector = componentContract.selectors[test.target];
|
|
1505
1541
|
if (!selector) {
|
|
1506
|
-
|
|
1507
|
-
|
|
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);
|
|
1508
1546
|
continue;
|
|
1509
1547
|
}
|
|
1510
1548
|
const target = component.querySelector(selector);
|
|
1511
1549
|
if (!target) {
|
|
1512
|
-
|
|
1513
|
-
|
|
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);
|
|
1514
1554
|
continue;
|
|
1515
1555
|
}
|
|
1516
1556
|
const attributeValue = target.getAttribute(test.attribute);
|
|
@@ -1518,35 +1558,38 @@ async function runContractTests(componentName, component) {
|
|
|
1518
1558
|
const attributes = test.attribute.split(" | ");
|
|
1519
1559
|
const hasAnyAttribute = attributes.some((attr) => target.hasAttribute(attr));
|
|
1520
1560
|
if (!hasAnyAttribute) {
|
|
1521
|
-
|
|
1522
|
-
|
|
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);
|
|
1523
1565
|
} else {
|
|
1524
1566
|
passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
|
|
1525
|
-
|
|
1567
|
+
staticPassed += 1;
|
|
1568
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}`, "pass", void 0, staticLevel);
|
|
1526
1569
|
}
|
|
1527
1570
|
} else if (!attributeValue || !test.expectedValue.split(" | ").includes(attributeValue)) {
|
|
1528
|
-
|
|
1529
|
-
|
|
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);
|
|
1530
1575
|
} else {
|
|
1531
1576
|
passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
|
|
1532
|
-
|
|
1577
|
+
staticPassed += 1;
|
|
1578
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}="${attributeValue}"`, "pass", void 0, staticLevel);
|
|
1533
1579
|
}
|
|
1534
1580
|
}
|
|
1535
1581
|
}
|
|
1536
1582
|
for (const dynamicTest of componentContract.dynamic) {
|
|
1537
1583
|
skipped.push(dynamicTest.description);
|
|
1538
|
-
reporter.reportTest(dynamicTest, "skip");
|
|
1584
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicTest.level }, "skip");
|
|
1539
1585
|
}
|
|
1540
|
-
|
|
1541
|
-
const staticFailed = failures.length - failuresBeforeStatic;
|
|
1542
|
-
const staticPassed = Math.max(0, staticTotal - staticFailed);
|
|
1543
|
-
reporter.reportStatic(staticPassed, staticFailed);
|
|
1586
|
+
reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
|
|
1544
1587
|
reporter.summary(failures);
|
|
1545
|
-
return { passes, failures, skipped };
|
|
1588
|
+
return { passes, failures, skipped, warnings };
|
|
1546
1589
|
}
|
|
1547
1590
|
|
|
1548
1591
|
// src/utils/test/src/test.ts
|
|
1549
|
-
async function testUiComponent(componentName, component, url) {
|
|
1592
|
+
async function testUiComponent(componentName, component, url, options = {}) {
|
|
1550
1593
|
if (!componentName || typeof componentName !== "string") {
|
|
1551
1594
|
throw new Error("\u274C testUiComponent requires a valid componentName (string)");
|
|
1552
1595
|
}
|
|
@@ -1583,14 +1626,25 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
1583
1626
|
}
|
|
1584
1627
|
return null;
|
|
1585
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
|
+
}
|
|
1586
1640
|
let contract;
|
|
1587
1641
|
try {
|
|
1588
1642
|
if (url) {
|
|
1589
1643
|
const devServerUrl = await checkDevServer(url);
|
|
1590
1644
|
if (devServerUrl) {
|
|
1591
1645
|
console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
|
|
1592
|
-
const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-
|
|
1593
|
-
contract = await runContractTestsPlaywright(componentName, devServerUrl);
|
|
1646
|
+
const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-UAOFNS7Z.js");
|
|
1647
|
+
contract = await runContractTestsPlaywright(componentName, devServerUrl, strictness);
|
|
1594
1648
|
} else {
|
|
1595
1649
|
throw new Error(
|
|
1596
1650
|
`\u274C Dev server not running at ${url}
|
|
@@ -1599,7 +1653,7 @@ Please start your dev server and try again.`
|
|
|
1599
1653
|
}
|
|
1600
1654
|
} else if (component) {
|
|
1601
1655
|
console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
|
|
1602
|
-
contract = await runContractTests(componentName, component);
|
|
1656
|
+
contract = await runContractTests(componentName, component, strictness);
|
|
1603
1657
|
} else {
|
|
1604
1658
|
throw new Error("\u274C Either component or URL must be provided");
|
|
1605
1659
|
}
|
package/dist/src/menu/index.cjs
CHANGED
|
@@ -102,6 +102,20 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
102
102
|
function hasSubmenu(menuItem) {
|
|
103
103
|
return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
|
|
104
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
|
+
}
|
|
105
119
|
intializeMenuItems();
|
|
106
120
|
function handleItemsKeydown(event, menuItem, menuItemIndex) {
|
|
107
121
|
switch (event.key) {
|
|
@@ -172,11 +186,10 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
172
186
|
break;
|
|
173
187
|
}
|
|
174
188
|
case "Tab": {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
189
|
+
closeMenu();
|
|
190
|
+
closeAncestorMenusFromTrigger(triggerButton);
|
|
191
|
+
if (onOpenChange) {
|
|
192
|
+
onOpenChange(false);
|
|
180
193
|
}
|
|
181
194
|
break;
|
|
182
195
|
}
|
package/dist/src/menu/index.js
CHANGED
|
@@ -100,6 +100,20 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
100
100
|
function hasSubmenu(menuItem) {
|
|
101
101
|
return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
|
|
102
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
|
+
}
|
|
103
117
|
intializeMenuItems();
|
|
104
118
|
function handleItemsKeydown(event, menuItem, menuItemIndex) {
|
|
105
119
|
switch (event.key) {
|
|
@@ -170,11 +184,10 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
170
184
|
break;
|
|
171
185
|
}
|
|
172
186
|
case "Tab": {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
187
|
+
closeMenu();
|
|
188
|
+
closeAncestorMenusFromTrigger(triggerButton);
|
|
189
|
+
if (onOpenChange) {
|
|
190
|
+
onOpenChange(false);
|
|
178
191
|
}
|
|
179
192
|
break;
|
|
180
193
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"id": "aria-ease.accordion",
|
|
3
|
+
"id": "aria-ease.contract.accordion",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"created": "09-02-2026",
|
|
6
|
-
"lastUpdated": "
|
|
6
|
+
"lastUpdated": "19-03-2026",
|
|
7
7
|
"description": "ARIA Accordion interaction contract. Validates the ARIA and interaction contract for a custom accordion component following the ARIA Authoring Practices Guide accordion with show/hide pattern.",
|
|
8
8
|
"source": {
|
|
9
9
|
"apg": "https://www.w3.org/WAI/ARIA/apg/patterns/accordion/",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
{
|
|
200
200
|
"description": "Down Arrow moves focus to next accordion trigger.",
|
|
201
201
|
"isMultiple": true,
|
|
202
|
-
"
|
|
202
|
+
"level": "optional",
|
|
203
203
|
"action": [
|
|
204
204
|
{ "type": "keypress", "target": "focusable", "key": "ArrowDown" }
|
|
205
205
|
],
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
{
|
|
216
216
|
"description": "Up Arrow moves focus to previous accordion trigger.",
|
|
217
217
|
"isMultiple": true,
|
|
218
|
-
"
|
|
218
|
+
"level": "optional",
|
|
219
219
|
"action": [
|
|
220
220
|
{ "type": "keypress", "target": "focusable", "key": "ArrowUp" }
|
|
221
221
|
],
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
{
|
|
232
232
|
"description": "Home moves focus to first accordion trigger.",
|
|
233
233
|
"isMultiple": true,
|
|
234
|
-
"
|
|
234
|
+
"level": "optional",
|
|
235
235
|
"action": [
|
|
236
236
|
{ "type": "keypress", "target": "focusable", "key": "Home" }
|
|
237
237
|
],
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
{
|
|
248
248
|
"description": "End moves focus to last accordion trigger.",
|
|
249
249
|
"isMultiple": true,
|
|
250
|
-
"
|
|
250
|
+
"level": "optional",
|
|
251
251
|
"action": [
|
|
252
252
|
{ "type": "keypress", "target": "focusable", "key": "End" }
|
|
253
253
|
],
|
|
@@ -263,7 +263,7 @@
|
|
|
263
263
|
{
|
|
264
264
|
"description": "Down Arrow wraps focus from last trigger to first trigger.",
|
|
265
265
|
"isMultiple": true,
|
|
266
|
-
"
|
|
266
|
+
"level": "optional",
|
|
267
267
|
"action": [
|
|
268
268
|
{ "type": "keypress", "target": "focusable", "key": "ArrowDown" }
|
|
269
269
|
],
|
|
@@ -279,7 +279,7 @@
|
|
|
279
279
|
{
|
|
280
280
|
"description": "Up Arrow wraps focus from first trigger to last trigger.",
|
|
281
281
|
"isMultiple": true,
|
|
282
|
-
"
|
|
282
|
+
"level": "optional",
|
|
283
283
|
"action": [
|
|
284
284
|
{ "type": "keypress", "target": "focusable", "key": "ArrowUp" }
|
|
285
285
|
],
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"id": "aria-ease.combobox.listbox",
|
|
3
|
+
"id": "aria-ease.contract.combobox.listbox",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"created": "11-02-2026",
|
|
6
|
-
"lastUpdated": "
|
|
6
|
+
"lastUpdated": "19-03-2026",
|
|
7
7
|
"description": "ARIA Combobox with Listbox popup interaction contract. Validates the ARIA and interaction contract for a custom combobox with listbox component following the ARIA Authoring Practices Guide combobox with listbox popup pattern.",
|
|
8
8
|
"source": {
|
|
9
9
|
"apg": "https://www.w3.org/WAI/ARIA/apg/patterns/combobox/",
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
199
|
"description": "Home navigates to first listbox option.",
|
|
200
|
-
"
|
|
200
|
+
"level": "optional",
|
|
201
201
|
"action": [
|
|
202
202
|
{ "type": "keypress", "target": "input", "key": "ArrowDown" },
|
|
203
203
|
{ "type": "keypress", "target": "input", "key": "End" },
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
"description": "End navigates to last listbox option.",
|
|
219
|
-
"
|
|
219
|
+
"level": "optional",
|
|
220
220
|
"action": [
|
|
221
221
|
{ "type": "keypress", "target": "input", "key": "ArrowDown" },
|
|
222
222
|
{ "type": "keypress", "target": "input", "key": "End" }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"id": "aria-ease.menu",
|
|
3
|
+
"id": "aria-ease.contract.menu",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"created": "11-02-2026",
|
|
6
|
-
"lastUpdated": "
|
|
6
|
+
"lastUpdated": "19-03-2026",
|
|
7
7
|
"description": "ARIA Menu interaction contract. Validates the ARIA and interaction contract for a custom menu component following the ARIA Authoring Practices Guide menu with popup pattern",
|
|
8
8
|
"source": {
|
|
9
9
|
"apg": "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"from": "container",
|
|
45
45
|
"attribute": "aria-labelledby",
|
|
46
46
|
"to": "trigger",
|
|
47
|
-
"
|
|
47
|
+
"level": "optional",
|
|
48
48
|
"failureMessage": "The APG 1.2 recommends a menu container to have aria-labelledby attribute that references the trigger that controls the menu."
|
|
49
49
|
},
|
|
50
50
|
{
|
|
@@ -390,7 +390,7 @@
|
|
|
390
390
|
]
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
|
-
"description": "Right Arrow
|
|
393
|
+
"description": "Right Arrow from a submenu item opens the submenu, updates ARIA expanded, and focuses first item.",
|
|
394
394
|
"action": [
|
|
395
395
|
{ "type": "click", "target": "trigger" },
|
|
396
396
|
{ "type": "keypress", "target": "submenuTrigger", "key": "ArrowRight" }
|
|
@@ -425,7 +425,7 @@
|
|
|
425
425
|
{
|
|
426
426
|
"target": "container",
|
|
427
427
|
"assertion": "notToBeVisible",
|
|
428
|
-
"failureMessage": "Menu should close after
|
|
428
|
+
"failureMessage": "Menu should close after pressing Tab from a menuitem."
|
|
429
429
|
},
|
|
430
430
|
{
|
|
431
431
|
"target": "trigger",
|
|
@@ -433,51 +433,76 @@
|
|
|
433
433
|
"attribute": "aria-expanded",
|
|
434
434
|
"expectedValue": "false",
|
|
435
435
|
"failureMessage": "Trigger's aria-expanded should be false after Tab closes the menu."
|
|
436
|
-
}
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
"description": "Shift+Tab on a menuitem closes the menu and update aria-expanded.",
|
|
441
|
+
"action": [
|
|
442
|
+
{ "type": "click", "target": "trigger" },
|
|
443
|
+
{ "type": "keypress", "target": "focusable", "key": "Shift+Tab" }
|
|
444
|
+
],
|
|
445
|
+
"assertions": [
|
|
437
446
|
{
|
|
438
|
-
"target": "
|
|
447
|
+
"target": "container",
|
|
439
448
|
"assertion": "notToBeVisible",
|
|
440
|
-
"failureMessage": "
|
|
449
|
+
"failureMessage": "Menu should close after pressing Shift+Tab from a menuitem."
|
|
441
450
|
},
|
|
442
451
|
{
|
|
443
|
-
"target": "
|
|
452
|
+
"target": "trigger",
|
|
444
453
|
"assertion": "toHaveAttribute",
|
|
445
454
|
"attribute": "aria-expanded",
|
|
446
455
|
"expectedValue": "false",
|
|
447
|
-
"failureMessage": "
|
|
456
|
+
"failureMessage": "Trigger's aria-expanded should be false after Shift+Tab closes the menu."
|
|
448
457
|
}
|
|
449
458
|
]
|
|
450
459
|
},
|
|
451
460
|
{
|
|
452
|
-
"description": "
|
|
461
|
+
"description": "Tab from a submenu item closes submenu and updates submenu trigger expanded state.",
|
|
453
462
|
"action": [
|
|
454
463
|
{ "type": "click", "target": "trigger" },
|
|
455
|
-
{ "type": "keypress", "target": "
|
|
464
|
+
{ "type": "keypress", "target": "submenuTrigger", "key": "ArrowRight" },
|
|
465
|
+
{ "type": "keypress", "target": "submenuItems", "key": "Tab" }
|
|
456
466
|
],
|
|
457
467
|
"assertions": [
|
|
458
468
|
{
|
|
459
|
-
"target": "
|
|
469
|
+
"target": "submenu",
|
|
460
470
|
"assertion": "notToBeVisible",
|
|
461
|
-
"failureMessage": "
|
|
471
|
+
"failureMessage": "Tab should close submenu when focus leaves submenu items."
|
|
462
472
|
},
|
|
463
473
|
{
|
|
464
|
-
"target": "
|
|
474
|
+
"target": "submenuTrigger",
|
|
465
475
|
"assertion": "toHaveAttribute",
|
|
466
476
|
"attribute": "aria-expanded",
|
|
467
477
|
"expectedValue": "false",
|
|
468
|
-
"failureMessage": "
|
|
469
|
-
}
|
|
478
|
+
"failureMessage": "Submenu trigger's aria-expanded should be false after Tab closes submenu."
|
|
479
|
+
}
|
|
480
|
+
]
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
"description": "Shift+Tab from a submenu item closes submenu and updates submenu trigger expanded state.",
|
|
484
|
+
"action": [
|
|
485
|
+
{ "type": "click", "target": "trigger" },
|
|
486
|
+
{ "type": "keypress", "target": "submenuTrigger", "key": "ArrowRight" },
|
|
487
|
+
{ "type": "keypress", "target": "submenuItems", "key": "Shift+Tab" }
|
|
488
|
+
],
|
|
489
|
+
"assertions": [
|
|
470
490
|
{
|
|
471
491
|
"target": "submenu",
|
|
472
492
|
"assertion": "notToBeVisible",
|
|
473
|
-
"failureMessage": "Shift+Tab should close
|
|
493
|
+
"failureMessage": "Shift+Tab should close submenu when focus leaves submenu items."
|
|
474
494
|
},
|
|
475
495
|
{
|
|
476
496
|
"target": "submenuTrigger",
|
|
477
497
|
"assertion": "toHaveAttribute",
|
|
478
498
|
"attribute": "aria-expanded",
|
|
479
499
|
"expectedValue": "false",
|
|
480
|
-
"failureMessage": "Submenu trigger's aria-expanded should be false after Shift+Tab closes
|
|
500
|
+
"failureMessage": "Submenu trigger's aria-expanded should be false after Shift+Tab closes submenu."
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
"target": "container",
|
|
504
|
+
"assertion": "notToBeVisible",
|
|
505
|
+
"failureMessage": "Shift+Tab should also close top-level menu after submenu closes."
|
|
481
506
|
}
|
|
482
507
|
]
|
|
483
508
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "aria-ease.tabs",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"created": "28-02-2026",
|
|
6
|
-
"lastUpdated": "
|
|
6
|
+
"lastUpdated": "19-03-2026",
|
|
7
7
|
"description": "ARIA tabs interaction contract. Validates the ARIA and interaction contract for a custom tabs component following the ARIA Authoring Practices Guide pattern.",
|
|
8
8
|
"source": {
|
|
9
9
|
"apg": "https://www.w3.org/WAI/ARIA/apg/patterns/tabs/",
|
|
@@ -226,7 +226,7 @@
|
|
|
226
226
|
},
|
|
227
227
|
{
|
|
228
228
|
"description": "Home moves focus to first tab and activates it.",
|
|
229
|
-
"
|
|
229
|
+
"level": "optional",
|
|
230
230
|
"action": [
|
|
231
231
|
{ "type": "keypress", "target": "focusable", "key": "ArrowRight" },
|
|
232
232
|
{ "type": "keypress", "target": "focusable", "key": "Home" }
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
},
|
|
251
251
|
{
|
|
252
252
|
"description": "End moves focus to last tab and activates it.",
|
|
253
|
-
"
|
|
253
|
+
"level": "optional",
|
|
254
254
|
"action": [
|
|
255
255
|
{ "type": "keypress", "target": "focusable", "key": "End" }
|
|
256
256
|
],
|