aria-ease 6.2.2 → 6.3.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 +91 -12
- package/bin/{chunk-7RMRFSJL.js → chunk-XLG3MIPQ.js} +5 -1
- package/bin/cli.cjs +52 -11
- package/bin/cli.js +1 -1
- package/bin/{contractTestRunnerPlaywright-ACAWN34W.js → contractTestRunnerPlaywright-JXQUUKFO.js} +48 -11
- package/bin/{test-A3ESFXOR.js → test-XSDP2NX3.js} +2 -2
- package/dist/{chunk-PDZQOXUN.js → chunk-RDEAG4KE.js} +5 -1
- package/dist/{contractTestRunnerPlaywright-O7FF7GV4.js → contractTestRunnerPlaywright-EUXD6ZZK.js} +48 -11
- package/dist/index.cjs +316 -69
- package/dist/index.d.cts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +265 -60
- package/dist/src/{Types.d-CRjhbrcw.d.cts → Types.d-DYfYR3Vc.d.cts} +18 -1
- package/dist/src/{Types.d-CRjhbrcw.d.ts → Types.d-DYfYR3Vc.d.ts} +18 -1
- package/dist/src/accordion/index.d.cts +2 -2
- package/dist/src/accordion/index.d.ts +2 -2
- package/dist/src/block/index.d.cts +1 -1
- package/dist/src/block/index.d.ts +1 -1
- package/dist/src/checkbox/index.cjs +0 -22
- package/dist/src/checkbox/index.d.cts +2 -2
- package/dist/src/checkbox/index.d.ts +2 -2
- package/dist/src/checkbox/index.js +0 -22
- package/dist/src/combobox/index.d.cts +1 -1
- package/dist/src/combobox/index.d.ts +1 -1
- package/dist/src/menu/index.d.cts +1 -1
- package/dist/src/menu/index.d.ts +1 -1
- package/dist/src/radio/index.cjs +0 -8
- package/dist/src/radio/index.d.cts +2 -2
- package/dist/src/radio/index.d.ts +2 -2
- package/dist/src/radio/index.js +0 -8
- package/dist/src/tabs/index.cjs +265 -0
- package/dist/src/tabs/index.d.cts +16 -0
- package/dist/src/tabs/index.d.ts +16 -0
- package/dist/src/tabs/index.js +263 -0
- package/dist/src/toggle/index.cjs +0 -28
- package/dist/src/toggle/index.d.cts +1 -1
- package/dist/src/toggle/index.d.ts +1 -1
- package/dist/src/toggle/index.js +0 -28
- package/dist/src/utils/test/{chunk-7RMRFSJL.js → chunk-XLG3MIPQ.js} +5 -1
- package/dist/src/utils/test/{contractTestRunnerPlaywright-7BPRTIN4.js → contractTestRunnerPlaywright-N77NEY25.js} +48 -11
- package/dist/src/utils/test/contracts/AccordionContract.json +18 -17
- package/dist/src/utils/test/contracts/ComboboxContract.json +32 -48
- package/dist/src/utils/test/contracts/MenuContract.json +19 -25
- package/dist/src/utils/test/contracts/TabsContract.json +348 -0
- package/dist/src/utils/test/index.cjs +52 -11
- package/dist/src/utils/test/index.js +2 -2
- package/package.json +8 -3
package/dist/index.cjs
CHANGED
|
@@ -47,6 +47,10 @@ var init_contract = __esm({
|
|
|
47
47
|
accordion: {
|
|
48
48
|
path: "./contracts/AccordionContract.json",
|
|
49
49
|
component: "accordion"
|
|
50
|
+
},
|
|
51
|
+
tabs: {
|
|
52
|
+
path: "./contracts/TabsContract.json",
|
|
53
|
+
component: "tabs"
|
|
50
54
|
}
|
|
51
55
|
};
|
|
52
56
|
}
|
|
@@ -167,7 +171,7 @@ ${"\u2500".repeat(60)}`);
|
|
|
167
171
|
this.log(`\u{1F4A1} Optional Enhancements (${suggestions.length}):
|
|
168
172
|
`);
|
|
169
173
|
this.log(`These features are optional per APG guidelines but recommended`);
|
|
170
|
-
this.log(`for improved user experience and keyboard
|
|
174
|
+
this.log(`for improved user experience and keyboard interaction:
|
|
171
175
|
`);
|
|
172
176
|
suggestions.forEach((test, index) => {
|
|
173
177
|
this.log(`${index + 1}. ${test.description}`);
|
|
@@ -385,9 +389,9 @@ async function runContractTestsPlaywright(componentName, url) {
|
|
|
385
389
|
}
|
|
386
390
|
await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
|
|
387
391
|
}
|
|
388
|
-
const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container;
|
|
392
|
+
const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container || componentContract.selectors.tablist || componentContract.selectors.tab;
|
|
389
393
|
if (!mainSelector) {
|
|
390
|
-
throw new Error(`CRITICAL: No main selector (trigger, input, or
|
|
394
|
+
throw new Error(`CRITICAL: No main selector (trigger, input, container, tablist, or tab) found in contract for ${componentName}`);
|
|
391
395
|
}
|
|
392
396
|
try {
|
|
393
397
|
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
|
|
@@ -449,28 +453,52 @@ async function runContractTestsPlaywright(componentName, url) {
|
|
|
449
453
|
failures.push(`Target ${test.target} not found.`);
|
|
450
454
|
continue;
|
|
451
455
|
}
|
|
456
|
+
const isRedundantCheck = (selector, attrName, expectedVal) => {
|
|
457
|
+
const attrPattern = new RegExp(`\\[${attrName}(?:=["']?([^\\]"']+)["']?)?\\]`);
|
|
458
|
+
const match = selector.match(attrPattern);
|
|
459
|
+
if (!match) return false;
|
|
460
|
+
if (!expectedVal) return true;
|
|
461
|
+
const selectorValue = match[1];
|
|
462
|
+
if (selectorValue) {
|
|
463
|
+
const expectedValues = expectedVal.split(" | ");
|
|
464
|
+
return expectedValues.includes(selectorValue);
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
467
|
+
};
|
|
452
468
|
if (!test.expectedValue) {
|
|
453
469
|
const attributes = test.attribute.split(" | ");
|
|
454
470
|
let hasAny = false;
|
|
471
|
+
let allRedundant = true;
|
|
455
472
|
for (const attr of attributes) {
|
|
456
|
-
const
|
|
473
|
+
const attrTrimmed = attr.trim();
|
|
474
|
+
if (isRedundantCheck(targetSelector, attrTrimmed)) {
|
|
475
|
+
passes.push(`${attrTrimmed} on ${test.target} verified by selector (already present in: ${targetSelector}).`);
|
|
476
|
+
hasAny = true;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
allRedundant = false;
|
|
480
|
+
const value = await target.getAttribute(attrTrimmed);
|
|
457
481
|
if (value !== null) {
|
|
458
482
|
hasAny = true;
|
|
459
483
|
break;
|
|
460
484
|
}
|
|
461
485
|
}
|
|
462
|
-
if (!hasAny) {
|
|
486
|
+
if (!hasAny && !allRedundant) {
|
|
463
487
|
failures.push(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`);
|
|
464
|
-
} else {
|
|
488
|
+
} else if (!allRedundant && hasAny) {
|
|
465
489
|
passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
|
|
466
490
|
}
|
|
467
491
|
} else {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (!attributeValue || !expectedValues.includes(attributeValue)) {
|
|
471
|
-
failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
|
|
492
|
+
if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
|
|
493
|
+
passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
|
|
472
494
|
} else {
|
|
473
|
-
|
|
495
|
+
const attributeValue = await target.getAttribute(test.attribute);
|
|
496
|
+
const expectedValues = test.expectedValue.split(" | ");
|
|
497
|
+
if (!attributeValue || !expectedValues.includes(attributeValue)) {
|
|
498
|
+
failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
|
|
499
|
+
} else {
|
|
500
|
+
passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
|
|
501
|
+
}
|
|
474
502
|
}
|
|
475
503
|
}
|
|
476
504
|
}
|
|
@@ -579,6 +607,19 @@ This indicates a problem with the menu component's close functionality.`
|
|
|
579
607
|
if (shouldSkipTest) {
|
|
580
608
|
continue;
|
|
581
609
|
}
|
|
610
|
+
if (componentContract.selectors.panel && componentContract.selectors.tab && componentContract.selectors.tablist) {
|
|
611
|
+
if (dynamicTest.isVertical !== void 0 && componentContract.selectors.tablist) {
|
|
612
|
+
const tablistSelector = componentContract.selectors.tablist;
|
|
613
|
+
const tablist = page.locator(tablistSelector).first();
|
|
614
|
+
const orientation = await tablist.getAttribute("aria-orientation");
|
|
615
|
+
const isVertical = orientation === "vertical";
|
|
616
|
+
if (dynamicTest.isVertical !== isVertical) {
|
|
617
|
+
const skipReason = dynamicTest.isVertical ? `Skipping vertical tabs test - component has horizontal orientation` : `Skipping horizontal tabs test - component has vertical orientation`;
|
|
618
|
+
reporter.reportTest(dynamicTest, "skip", skipReason);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
582
623
|
for (const act of action) {
|
|
583
624
|
if (!page || page.isClosed()) {
|
|
584
625
|
failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
|
|
@@ -938,6 +979,7 @@ __export(index_exports, {
|
|
|
938
979
|
makeComboboxAccessible: () => makeComboboxAccessible,
|
|
939
980
|
makeMenuAccessible: () => makeMenuAccessible,
|
|
940
981
|
makeRadioAccessible: () => makeRadioAccessible,
|
|
982
|
+
makeTabsAccessible: () => makeTabsAccessible,
|
|
941
983
|
makeToggleAccessible: () => makeToggleAccessible,
|
|
942
984
|
testUiComponent: () => testUiComponent
|
|
943
985
|
});
|
|
@@ -1362,28 +1404,6 @@ function makeCheckboxAccessible({ checkboxGroupId, checkboxesClass }) {
|
|
|
1362
1404
|
event.preventDefault();
|
|
1363
1405
|
toggleCheckbox(index);
|
|
1364
1406
|
break;
|
|
1365
|
-
case "ArrowDown":
|
|
1366
|
-
event.preventDefault();
|
|
1367
|
-
{
|
|
1368
|
-
const nextIndex = (index + 1) % checkboxes.length;
|
|
1369
|
-
checkboxes[nextIndex].focus();
|
|
1370
|
-
}
|
|
1371
|
-
break;
|
|
1372
|
-
case "ArrowUp":
|
|
1373
|
-
event.preventDefault();
|
|
1374
|
-
{
|
|
1375
|
-
const prevIndex = (index - 1 + checkboxes.length) % checkboxes.length;
|
|
1376
|
-
checkboxes[prevIndex].focus();
|
|
1377
|
-
}
|
|
1378
|
-
break;
|
|
1379
|
-
case "Home":
|
|
1380
|
-
event.preventDefault();
|
|
1381
|
-
checkboxes[0].focus();
|
|
1382
|
-
break;
|
|
1383
|
-
case "End":
|
|
1384
|
-
event.preventDefault();
|
|
1385
|
-
checkboxes[checkboxes.length - 1].focus();
|
|
1386
|
-
break;
|
|
1387
1407
|
}
|
|
1388
1408
|
};
|
|
1389
1409
|
}
|
|
@@ -1730,14 +1750,6 @@ function makeRadioAccessible({ radioGroupId, radiosClass, defaultSelectedIndex =
|
|
|
1730
1750
|
event.preventDefault();
|
|
1731
1751
|
selectRadio(index);
|
|
1732
1752
|
break;
|
|
1733
|
-
case "Home":
|
|
1734
|
-
event.preventDefault();
|
|
1735
|
-
selectRadio(0);
|
|
1736
|
-
break;
|
|
1737
|
-
case "End":
|
|
1738
|
-
event.preventDefault();
|
|
1739
|
-
selectRadio(radios.length - 1);
|
|
1740
|
-
break;
|
|
1741
1753
|
}
|
|
1742
1754
|
};
|
|
1743
1755
|
}
|
|
@@ -1849,34 +1861,6 @@ function makeToggleAccessible({ toggleId, togglesClass, isSingleToggle = true })
|
|
|
1849
1861
|
event.preventDefault();
|
|
1850
1862
|
toggleButton(index);
|
|
1851
1863
|
break;
|
|
1852
|
-
case "ArrowDown":
|
|
1853
|
-
case "ArrowRight":
|
|
1854
|
-
if (!isSingleToggle && toggles.length > 1) {
|
|
1855
|
-
event.preventDefault();
|
|
1856
|
-
const nextIndex = (index + 1) % toggles.length;
|
|
1857
|
-
toggles[nextIndex].focus();
|
|
1858
|
-
}
|
|
1859
|
-
break;
|
|
1860
|
-
case "ArrowUp":
|
|
1861
|
-
case "ArrowLeft":
|
|
1862
|
-
if (!isSingleToggle && toggles.length > 1) {
|
|
1863
|
-
event.preventDefault();
|
|
1864
|
-
const prevIndex = (index - 1 + toggles.length) % toggles.length;
|
|
1865
|
-
toggles[prevIndex].focus();
|
|
1866
|
-
}
|
|
1867
|
-
break;
|
|
1868
|
-
case "Home":
|
|
1869
|
-
if (!isSingleToggle && toggles.length > 1) {
|
|
1870
|
-
event.preventDefault();
|
|
1871
|
-
toggles[0].focus();
|
|
1872
|
-
}
|
|
1873
|
-
break;
|
|
1874
|
-
case "End":
|
|
1875
|
-
if (!isSingleToggle && toggles.length > 1) {
|
|
1876
|
-
event.preventDefault();
|
|
1877
|
-
toggles[toggles.length - 1].focus();
|
|
1878
|
-
}
|
|
1879
|
-
break;
|
|
1880
1864
|
}
|
|
1881
1865
|
};
|
|
1882
1866
|
}
|
|
@@ -2168,6 +2152,268 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
2168
2152
|
return { cleanup, refresh, openListbox, closeListbox };
|
|
2169
2153
|
}
|
|
2170
2154
|
|
|
2155
|
+
// src/tabs/src/makeTabsAccessible/makeTabsAccessible.ts
|
|
2156
|
+
function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation = "horizontal", activateOnFocus = true, callback }) {
|
|
2157
|
+
const tabList = document.querySelector(`#${tabListId}`);
|
|
2158
|
+
if (!tabList) {
|
|
2159
|
+
console.error(`[aria-ease] Element with id="${tabListId}" not found. Make sure the tab list container exists before calling makeTabsAccessible.`);
|
|
2160
|
+
return { cleanup: () => {
|
|
2161
|
+
} };
|
|
2162
|
+
}
|
|
2163
|
+
const tabs = Array.from(tabList.querySelectorAll(`.${tabsClass}`));
|
|
2164
|
+
if (tabs.length === 0) {
|
|
2165
|
+
console.error(`[aria-ease] No elements with class="${tabsClass}" found. Make sure tab buttons exist before calling makeTabsAccessible.`);
|
|
2166
|
+
return { cleanup: () => {
|
|
2167
|
+
} };
|
|
2168
|
+
}
|
|
2169
|
+
const tabPanels = Array.from(document.querySelectorAll(`.${tabPanelsClass}`));
|
|
2170
|
+
if (tabPanels.length === 0) {
|
|
2171
|
+
console.error(`[aria-ease] No elements with class="${tabPanelsClass}" found. Make sure tab panels exist before calling makeTabsAccessible.`);
|
|
2172
|
+
return { cleanup: () => {
|
|
2173
|
+
} };
|
|
2174
|
+
}
|
|
2175
|
+
if (tabs.length !== tabPanels.length) {
|
|
2176
|
+
console.error(`[aria-ease] Tab/panel mismatch: found ${tabs.length} tabs but ${tabPanels.length} panels.`);
|
|
2177
|
+
return { cleanup: () => {
|
|
2178
|
+
} };
|
|
2179
|
+
}
|
|
2180
|
+
const handlerMap = /* @__PURE__ */ new WeakMap();
|
|
2181
|
+
const clickHandlerMap = /* @__PURE__ */ new WeakMap();
|
|
2182
|
+
const contextMenuHandlerMap = /* @__PURE__ */ new WeakMap();
|
|
2183
|
+
let activeTabIndex = 0;
|
|
2184
|
+
function initialize() {
|
|
2185
|
+
tabList.setAttribute("role", "tablist");
|
|
2186
|
+
tabList.setAttribute("aria-orientation", orientation);
|
|
2187
|
+
tabs.forEach((tab, index) => {
|
|
2188
|
+
const panel = tabPanels[index];
|
|
2189
|
+
if (!tab.id) {
|
|
2190
|
+
tab.id = `${tabListId}-tab-${index}`;
|
|
2191
|
+
}
|
|
2192
|
+
if (!panel.id) {
|
|
2193
|
+
panel.id = `${tabListId}-panel-${index}`;
|
|
2194
|
+
}
|
|
2195
|
+
tab.setAttribute("role", "tab");
|
|
2196
|
+
tab.setAttribute("aria-controls", panel.id);
|
|
2197
|
+
tab.setAttribute("aria-selected", "false");
|
|
2198
|
+
tab.setAttribute("tabindex", "-1");
|
|
2199
|
+
panel.setAttribute("role", "tabpanel");
|
|
2200
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
|
2201
|
+
panel.hidden = true;
|
|
2202
|
+
const hasFocusableContent = panel.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
2203
|
+
if (!hasFocusableContent) {
|
|
2204
|
+
panel.setAttribute("tabindex", "0");
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
activateTab(0, false);
|
|
2208
|
+
}
|
|
2209
|
+
function activateTab(index, shouldFocus = true) {
|
|
2210
|
+
if (index < 0 || index >= tabs.length) {
|
|
2211
|
+
console.error(`[aria-ease] Invalid tab index: ${index}`);
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const previousIndex = activeTabIndex;
|
|
2215
|
+
tabs.forEach((tab, i) => {
|
|
2216
|
+
const panel = tabPanels[i];
|
|
2217
|
+
tab.setAttribute("aria-selected", "false");
|
|
2218
|
+
tab.setAttribute("tabindex", "-1");
|
|
2219
|
+
panel.hidden = true;
|
|
2220
|
+
});
|
|
2221
|
+
const activeTab = tabs[index];
|
|
2222
|
+
const activePanel = tabPanels[index];
|
|
2223
|
+
activeTab.setAttribute("aria-selected", "true");
|
|
2224
|
+
activeTab.setAttribute("tabindex", "0");
|
|
2225
|
+
activePanel.hidden = false;
|
|
2226
|
+
if (shouldFocus) {
|
|
2227
|
+
activeTab.focus();
|
|
2228
|
+
}
|
|
2229
|
+
activeTabIndex = index;
|
|
2230
|
+
if (callback?.onTabChange && previousIndex !== index) {
|
|
2231
|
+
try {
|
|
2232
|
+
callback.onTabChange(index, previousIndex);
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
console.error("[aria-ease] Error in tabs onTabChange callback:", error);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
function moveFocus2(direction) {
|
|
2239
|
+
const currentFocusedIndex = tabs.findIndex((tab) => tab === document.activeElement);
|
|
2240
|
+
const currentIndex = currentFocusedIndex !== -1 ? currentFocusedIndex : activeTabIndex;
|
|
2241
|
+
let newIndex = currentIndex;
|
|
2242
|
+
switch (direction) {
|
|
2243
|
+
case "first":
|
|
2244
|
+
newIndex = 0;
|
|
2245
|
+
break;
|
|
2246
|
+
case "last":
|
|
2247
|
+
newIndex = tabs.length - 1;
|
|
2248
|
+
break;
|
|
2249
|
+
case "next":
|
|
2250
|
+
newIndex = (currentIndex + 1) % tabs.length;
|
|
2251
|
+
break;
|
|
2252
|
+
case "prev":
|
|
2253
|
+
newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2256
|
+
tabs[newIndex].focus();
|
|
2257
|
+
tabs[newIndex].setAttribute("tabindex", "0");
|
|
2258
|
+
tabs[activeTabIndex].setAttribute("tabindex", "-1");
|
|
2259
|
+
if (activateOnFocus) {
|
|
2260
|
+
activateTab(newIndex, false);
|
|
2261
|
+
} else {
|
|
2262
|
+
const currentActive = activeTabIndex;
|
|
2263
|
+
tabs.forEach((tab, i) => {
|
|
2264
|
+
if (i === newIndex) {
|
|
2265
|
+
tab.setAttribute("tabindex", "0");
|
|
2266
|
+
} else if (i !== currentActive) {
|
|
2267
|
+
tab.setAttribute("tabindex", "-1");
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
function handleTabClick(index) {
|
|
2273
|
+
return () => {
|
|
2274
|
+
activateTab(index);
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
function handleTabKeydown(index) {
|
|
2278
|
+
return (event) => {
|
|
2279
|
+
const { key } = event;
|
|
2280
|
+
let handled = false;
|
|
2281
|
+
if (orientation === "horizontal") {
|
|
2282
|
+
switch (key) {
|
|
2283
|
+
case "ArrowLeft":
|
|
2284
|
+
event.preventDefault();
|
|
2285
|
+
moveFocus2("prev");
|
|
2286
|
+
handled = true;
|
|
2287
|
+
break;
|
|
2288
|
+
case "ArrowRight":
|
|
2289
|
+
event.preventDefault();
|
|
2290
|
+
moveFocus2("next");
|
|
2291
|
+
handled = true;
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
2294
|
+
} else {
|
|
2295
|
+
switch (key) {
|
|
2296
|
+
case "ArrowUp":
|
|
2297
|
+
event.preventDefault();
|
|
2298
|
+
moveFocus2("prev");
|
|
2299
|
+
handled = true;
|
|
2300
|
+
break;
|
|
2301
|
+
case "ArrowDown":
|
|
2302
|
+
event.preventDefault();
|
|
2303
|
+
moveFocus2("next");
|
|
2304
|
+
handled = true;
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (!handled) {
|
|
2309
|
+
switch (key) {
|
|
2310
|
+
case "Home":
|
|
2311
|
+
event.preventDefault();
|
|
2312
|
+
moveFocus2("first");
|
|
2313
|
+
break;
|
|
2314
|
+
case "End":
|
|
2315
|
+
event.preventDefault();
|
|
2316
|
+
moveFocus2("last");
|
|
2317
|
+
break;
|
|
2318
|
+
case " ":
|
|
2319
|
+
case "Enter":
|
|
2320
|
+
if (!activateOnFocus) {
|
|
2321
|
+
event.preventDefault();
|
|
2322
|
+
activateTab(index);
|
|
2323
|
+
}
|
|
2324
|
+
break;
|
|
2325
|
+
case "F10":
|
|
2326
|
+
if (event.shiftKey && callback?.onContextMenu) {
|
|
2327
|
+
event.preventDefault();
|
|
2328
|
+
try {
|
|
2329
|
+
callback.onContextMenu(index, tabs[index]);
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
console.error("[aria-ease] Error in tabs onContextMenu callback:", error);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
break;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
function handleTabContextMenu(index) {
|
|
2340
|
+
return (event) => {
|
|
2341
|
+
if (callback?.onContextMenu) {
|
|
2342
|
+
event.preventDefault();
|
|
2343
|
+
try {
|
|
2344
|
+
callback.onContextMenu(index, tabs[index]);
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
console.error("[aria-ease] Error in tabs onContextMenu callback:", error);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
function addListeners() {
|
|
2352
|
+
tabs.forEach((tab, index) => {
|
|
2353
|
+
const clickHandler = handleTabClick(index);
|
|
2354
|
+
const keydownHandler = handleTabKeydown(index);
|
|
2355
|
+
const contextMenuHandler = handleTabContextMenu(index);
|
|
2356
|
+
tab.addEventListener("click", clickHandler);
|
|
2357
|
+
tab.addEventListener("keydown", keydownHandler);
|
|
2358
|
+
if (callback?.onContextMenu) {
|
|
2359
|
+
tab.addEventListener("contextmenu", contextMenuHandler);
|
|
2360
|
+
contextMenuHandlerMap.set(tab, contextMenuHandler);
|
|
2361
|
+
}
|
|
2362
|
+
handlerMap.set(tab, keydownHandler);
|
|
2363
|
+
clickHandlerMap.set(tab, clickHandler);
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
function removeListeners() {
|
|
2367
|
+
tabs.forEach((tab) => {
|
|
2368
|
+
const keydownHandler = handlerMap.get(tab);
|
|
2369
|
+
const clickHandler = clickHandlerMap.get(tab);
|
|
2370
|
+
const contextMenuHandler = contextMenuHandlerMap.get(tab);
|
|
2371
|
+
if (keydownHandler) {
|
|
2372
|
+
tab.removeEventListener("keydown", keydownHandler);
|
|
2373
|
+
handlerMap.delete(tab);
|
|
2374
|
+
}
|
|
2375
|
+
if (clickHandler) {
|
|
2376
|
+
tab.removeEventListener("click", clickHandler);
|
|
2377
|
+
clickHandlerMap.delete(tab);
|
|
2378
|
+
}
|
|
2379
|
+
if (contextMenuHandler) {
|
|
2380
|
+
tab.removeEventListener("contextmenu", contextMenuHandler);
|
|
2381
|
+
contextMenuHandlerMap.delete(tab);
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
function cleanup() {
|
|
2386
|
+
removeListeners();
|
|
2387
|
+
tabs.forEach((tab, index) => {
|
|
2388
|
+
const panel = tabPanels[index];
|
|
2389
|
+
tab.removeAttribute("role");
|
|
2390
|
+
tab.removeAttribute("aria-selected");
|
|
2391
|
+
tab.removeAttribute("aria-controls");
|
|
2392
|
+
tab.removeAttribute("tabindex");
|
|
2393
|
+
panel.removeAttribute("role");
|
|
2394
|
+
panel.removeAttribute("aria-labelledby");
|
|
2395
|
+
panel.removeAttribute("tabindex");
|
|
2396
|
+
panel.hidden = false;
|
|
2397
|
+
});
|
|
2398
|
+
tabList.removeAttribute("role");
|
|
2399
|
+
tabList.removeAttribute("aria-orientation");
|
|
2400
|
+
}
|
|
2401
|
+
function refresh() {
|
|
2402
|
+
removeListeners();
|
|
2403
|
+
const newTabs = Array.from(tabList.querySelectorAll(`.${tabsClass}`));
|
|
2404
|
+
const newPanels = Array.from(document.querySelectorAll(`.${tabPanelsClass}`));
|
|
2405
|
+
tabs.length = 0;
|
|
2406
|
+
tabs.push(...newTabs);
|
|
2407
|
+
tabPanels.length = 0;
|
|
2408
|
+
tabPanels.push(...newPanels);
|
|
2409
|
+
initialize();
|
|
2410
|
+
addListeners();
|
|
2411
|
+
}
|
|
2412
|
+
initialize();
|
|
2413
|
+
addListeners();
|
|
2414
|
+
return { activateTab, cleanup, refresh };
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2171
2417
|
// src/utils/test/src/test.ts
|
|
2172
2418
|
var import_jest_axe = require("jest-axe");
|
|
2173
2419
|
|
|
@@ -2371,6 +2617,7 @@ async function cleanupTests() {
|
|
|
2371
2617
|
makeComboboxAccessible,
|
|
2372
2618
|
makeMenuAccessible,
|
|
2373
2619
|
makeRadioAccessible,
|
|
2620
|
+
makeTabsAccessible,
|
|
2374
2621
|
makeToggleAccessible,
|
|
2375
2622
|
testUiComponent
|
|
2376
2623
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -17,6 +17,9 @@ interface AccessibilityInstance {
|
|
|
17
17
|
collapseItem?: (index: number) => void;
|
|
18
18
|
toggleItem?: (index: number) => void;
|
|
19
19
|
|
|
20
|
+
// Tabs methods
|
|
21
|
+
activateTab?: (index: number, shouldFocus?: boolean) => void;
|
|
22
|
+
|
|
20
23
|
// Radio methods
|
|
21
24
|
selectRadio?: (index: number) => void;
|
|
22
25
|
getSelectedIndex?: () => number;
|
|
@@ -51,6 +54,20 @@ interface AccordionCallback {
|
|
|
51
54
|
onCollapse?: (index: number) => void;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
interface TabsConfig {
|
|
58
|
+
tabListId: string;
|
|
59
|
+
tabsClass: string;
|
|
60
|
+
tabPanelsClass: string;
|
|
61
|
+
orientation?: "horizontal" | "vertical";
|
|
62
|
+
activateOnFocus?: boolean;
|
|
63
|
+
callback?: TabsCallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface TabsCallback {
|
|
67
|
+
onTabChange?: (activeIndex: number, previousIndex: number) => void;
|
|
68
|
+
onContextMenu?: (tabIndex: number, tabElement: HTMLElement) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
interface ComboboxConfig {
|
|
55
72
|
comboboxInputId: string;
|
|
56
73
|
comboboxButtonId?: string;
|
|
@@ -78,7 +95,7 @@ interface MenuCallback {
|
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
/**
|
|
81
|
-
* Makes an accordion accessible by managing ARIA attributes, keyboard
|
|
98
|
+
* Makes an accordion accessible by managing ARIA attributes, keyboard interaction, and state.
|
|
82
99
|
* Handles multiple accordion items with proper focus management and keyboard interactions.
|
|
83
100
|
* @param {string} accordionId - The id of the accordion container.
|
|
84
101
|
* @param {string} triggersClass - The shared class of all accordion trigger buttons.
|
|
@@ -102,7 +119,7 @@ interface BlockConfig {
|
|
|
102
119
|
declare function makeBlockAccessible({ blockId, blockItemsClass }: BlockConfig): AccessibilityInstance;
|
|
103
120
|
|
|
104
121
|
/**
|
|
105
|
-
* Makes a checkbox group accessible by managing ARIA attributes and keyboard
|
|
122
|
+
* Makes a checkbox group accessible by managing ARIA attributes and keyboard interaction.
|
|
106
123
|
* Handles multiple independent checkboxes with proper focus management and keyboard interactions.
|
|
107
124
|
* @param {string} checkboxGroupId - The id of the checkbox group container.
|
|
108
125
|
* @param {string} checkboxesClass - The shared class of all checkboxes.
|
|
@@ -124,7 +141,7 @@ declare function makeCheckboxAccessible({ checkboxGroupId, checkboxesClass }: Ch
|
|
|
124
141
|
declare function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }: MenuConfig): AccessibilityInstance;
|
|
125
142
|
|
|
126
143
|
/**
|
|
127
|
-
* Makes a radio group accessible by managing ARIA attributes, keyboard
|
|
144
|
+
* Makes a radio group accessible by managing ARIA attributes, keyboard interaction, and state.
|
|
128
145
|
* Handles radio button selection with proper focus management and keyboard interactions.
|
|
129
146
|
* @param {string} radioGroupId - The id of the radio group container.
|
|
130
147
|
* @param {string} radiosClass - The shared class of all radio buttons.
|
|
@@ -164,6 +181,19 @@ declare function makeToggleAccessible({ toggleId, togglesClass, isSingleToggle }
|
|
|
164
181
|
|
|
165
182
|
declare function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId, listBoxItemsClass, callback }: ComboboxConfig): AccessibilityInstance;
|
|
166
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Makes tabs accessible by managing ARIA attributes, keyboard interaction, and state.
|
|
186
|
+
* Implements WAI-ARIA tabs pattern with full keyboard support and ARIA properties.
|
|
187
|
+
* @param {string} tabListId - The id of the tab list container.
|
|
188
|
+
* @param {string} tabsClass - The shared class of all tab buttons.
|
|
189
|
+
* @param {string} tabPanelsClass - The shared class of all tab panels.
|
|
190
|
+
* @param {('horizontal' | 'vertical')} orientation - Tab list orientation (default: 'horizontal').
|
|
191
|
+
* @param {boolean} activateOnFocus - Whether tabs activate automatically on focus (default: true).
|
|
192
|
+
* @param {TabsCallback} callback - Configuration options for callbacks.
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
declare function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation, activateOnFocus, callback }: TabsConfig): AccessibilityInstance;
|
|
196
|
+
|
|
167
197
|
/**
|
|
168
198
|
* Runs static and interactions accessibility test on UI components.
|
|
169
199
|
* @param {string} componentName The name of the component contract to test against
|
|
@@ -178,4 +208,4 @@ declare function testUiComponent(componentName: string, component: HTMLElement |
|
|
|
178
208
|
*/
|
|
179
209
|
declare function cleanupTests(): Promise<void>;
|
|
180
210
|
|
|
181
|
-
export { cleanupTests, makeAccordionAccessible, makeBlockAccessible, makeCheckboxAccessible, makeComboboxAccessible, makeMenuAccessible, makeRadioAccessible, makeToggleAccessible, testUiComponent };
|
|
211
|
+
export { cleanupTests, makeAccordionAccessible, makeBlockAccessible, makeCheckboxAccessible, makeComboboxAccessible, makeMenuAccessible, makeRadioAccessible, makeTabsAccessible, makeToggleAccessible, testUiComponent };
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ interface AccessibilityInstance {
|
|
|
17
17
|
collapseItem?: (index: number) => void;
|
|
18
18
|
toggleItem?: (index: number) => void;
|
|
19
19
|
|
|
20
|
+
// Tabs methods
|
|
21
|
+
activateTab?: (index: number, shouldFocus?: boolean) => void;
|
|
22
|
+
|
|
20
23
|
// Radio methods
|
|
21
24
|
selectRadio?: (index: number) => void;
|
|
22
25
|
getSelectedIndex?: () => number;
|
|
@@ -51,6 +54,20 @@ interface AccordionCallback {
|
|
|
51
54
|
onCollapse?: (index: number) => void;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
interface TabsConfig {
|
|
58
|
+
tabListId: string;
|
|
59
|
+
tabsClass: string;
|
|
60
|
+
tabPanelsClass: string;
|
|
61
|
+
orientation?: "horizontal" | "vertical";
|
|
62
|
+
activateOnFocus?: boolean;
|
|
63
|
+
callback?: TabsCallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface TabsCallback {
|
|
67
|
+
onTabChange?: (activeIndex: number, previousIndex: number) => void;
|
|
68
|
+
onContextMenu?: (tabIndex: number, tabElement: HTMLElement) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
interface ComboboxConfig {
|
|
55
72
|
comboboxInputId: string;
|
|
56
73
|
comboboxButtonId?: string;
|
|
@@ -78,7 +95,7 @@ interface MenuCallback {
|
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
/**
|
|
81
|
-
* Makes an accordion accessible by managing ARIA attributes, keyboard
|
|
98
|
+
* Makes an accordion accessible by managing ARIA attributes, keyboard interaction, and state.
|
|
82
99
|
* Handles multiple accordion items with proper focus management and keyboard interactions.
|
|
83
100
|
* @param {string} accordionId - The id of the accordion container.
|
|
84
101
|
* @param {string} triggersClass - The shared class of all accordion trigger buttons.
|
|
@@ -102,7 +119,7 @@ interface BlockConfig {
|
|
|
102
119
|
declare function makeBlockAccessible({ blockId, blockItemsClass }: BlockConfig): AccessibilityInstance;
|
|
103
120
|
|
|
104
121
|
/**
|
|
105
|
-
* Makes a checkbox group accessible by managing ARIA attributes and keyboard
|
|
122
|
+
* Makes a checkbox group accessible by managing ARIA attributes and keyboard interaction.
|
|
106
123
|
* Handles multiple independent checkboxes with proper focus management and keyboard interactions.
|
|
107
124
|
* @param {string} checkboxGroupId - The id of the checkbox group container.
|
|
108
125
|
* @param {string} checkboxesClass - The shared class of all checkboxes.
|
|
@@ -124,7 +141,7 @@ declare function makeCheckboxAccessible({ checkboxGroupId, checkboxesClass }: Ch
|
|
|
124
141
|
declare function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }: MenuConfig): AccessibilityInstance;
|
|
125
142
|
|
|
126
143
|
/**
|
|
127
|
-
* Makes a radio group accessible by managing ARIA attributes, keyboard
|
|
144
|
+
* Makes a radio group accessible by managing ARIA attributes, keyboard interaction, and state.
|
|
128
145
|
* Handles radio button selection with proper focus management and keyboard interactions.
|
|
129
146
|
* @param {string} radioGroupId - The id of the radio group container.
|
|
130
147
|
* @param {string} radiosClass - The shared class of all radio buttons.
|
|
@@ -164,6 +181,19 @@ declare function makeToggleAccessible({ toggleId, togglesClass, isSingleToggle }
|
|
|
164
181
|
|
|
165
182
|
declare function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId, listBoxItemsClass, callback }: ComboboxConfig): AccessibilityInstance;
|
|
166
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Makes tabs accessible by managing ARIA attributes, keyboard interaction, and state.
|
|
186
|
+
* Implements WAI-ARIA tabs pattern with full keyboard support and ARIA properties.
|
|
187
|
+
* @param {string} tabListId - The id of the tab list container.
|
|
188
|
+
* @param {string} tabsClass - The shared class of all tab buttons.
|
|
189
|
+
* @param {string} tabPanelsClass - The shared class of all tab panels.
|
|
190
|
+
* @param {('horizontal' | 'vertical')} orientation - Tab list orientation (default: 'horizontal').
|
|
191
|
+
* @param {boolean} activateOnFocus - Whether tabs activate automatically on focus (default: true).
|
|
192
|
+
* @param {TabsCallback} callback - Configuration options for callbacks.
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
declare function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation, activateOnFocus, callback }: TabsConfig): AccessibilityInstance;
|
|
196
|
+
|
|
167
197
|
/**
|
|
168
198
|
* Runs static and interactions accessibility test on UI components.
|
|
169
199
|
* @param {string} componentName The name of the component contract to test against
|
|
@@ -178,4 +208,4 @@ declare function testUiComponent(componentName: string, component: HTMLElement |
|
|
|
178
208
|
*/
|
|
179
209
|
declare function cleanupTests(): Promise<void>;
|
|
180
210
|
|
|
181
|
-
export { cleanupTests, makeAccordionAccessible, makeBlockAccessible, makeCheckboxAccessible, makeComboboxAccessible, makeMenuAccessible, makeRadioAccessible, makeToggleAccessible, testUiComponent };
|
|
211
|
+
export { cleanupTests, makeAccordionAccessible, makeBlockAccessible, makeCheckboxAccessible, makeComboboxAccessible, makeMenuAccessible, makeRadioAccessible, makeTabsAccessible, makeToggleAccessible, testUiComponent };
|