accessible-kit 1.0.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/LICENSE +21 -0
- package/README.md +1038 -0
- package/package.json +81 -0
- package/src/README.md +2240 -0
- package/src/css/a11y-accordion.core.css +94 -0
- package/src/css/a11y-accordion.theme.css +246 -0
- package/src/css/a11y-dropdown.core.css +169 -0
- package/src/css/a11y-dropdown.theme.css +175 -0
- package/src/css/a11y-modal.core.css +136 -0
- package/src/css/a11y-modal.theme.css +218 -0
- package/src/css/a11y-offcanvas.core.css +122 -0
- package/src/css/a11y-offcanvas.theme.css +170 -0
- package/src/css/a11y-tabs.core.css +120 -0
- package/src/css/a11y-tabs.theme.css +312 -0
- package/src/js/a11y-accordion.js +447 -0
- package/src/js/a11y-dropdown.js +353 -0
- package/src/js/a11y-modal.js +290 -0
- package/src/js/a11y-offcanvas.js +298 -0
- package/src/js/a11y-tabs.js +336 -0
- package/src/js/index.js +46 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessible Offcanvas Component
|
|
3
|
+
* Vanilla JS, a11y friendly, focus trap, keyboard navigation
|
|
4
|
+
* Follows WAI-ARIA Authoring Practices Guide for Dialog Pattern
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class AccessibleOffcanvas {
|
|
8
|
+
constructor(element, options = {}) {
|
|
9
|
+
this.container = element;
|
|
10
|
+
this.triggers = Array.from(
|
|
11
|
+
document.querySelectorAll(`[data-offcanvas-trigger="${element.id}"]`)
|
|
12
|
+
);
|
|
13
|
+
this.panel = element.querySelector("[data-offcanvas-panel]");
|
|
14
|
+
this.backdrop = element.querySelector("[data-offcanvas-backdrop]");
|
|
15
|
+
this.closeButtons = Array.from(
|
|
16
|
+
this.panel.querySelectorAll("[data-offcanvas-close]")
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Options
|
|
20
|
+
this.options = {
|
|
21
|
+
closeOnBackdrop: options.closeOnBackdrop !== false,
|
|
22
|
+
closeOnEscape: options.closeOnEscape !== false,
|
|
23
|
+
trapFocus: options.trapFocus !== false,
|
|
24
|
+
scrollLock: options.scrollLock !== false,
|
|
25
|
+
onOpen: options.onOpen || null,
|
|
26
|
+
onClose: options.onClose || null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
this.isOpen = false;
|
|
30
|
+
this.lastFocusedElement = null;
|
|
31
|
+
this.focusableElements = [];
|
|
32
|
+
this.firstFocusable = null;
|
|
33
|
+
this.lastFocusable = null;
|
|
34
|
+
|
|
35
|
+
this.init();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
init() {
|
|
39
|
+
// Set up ARIA attributes
|
|
40
|
+
this.setupAria();
|
|
41
|
+
|
|
42
|
+
// Bind event listeners
|
|
43
|
+
this.triggers.forEach((trigger) => {
|
|
44
|
+
trigger.addEventListener("click", (e) => this.open(e));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.closeButtons.forEach((button) => {
|
|
48
|
+
button.addEventListener("click", (e) => this.close(e));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Backdrop click
|
|
52
|
+
if (this.backdrop && this.options.closeOnBackdrop) {
|
|
53
|
+
this.backdrop.addEventListener("click", (e) => this.close(e));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Escape key
|
|
57
|
+
if (this.options.closeOnEscape) {
|
|
58
|
+
document.addEventListener("keydown", (e) => {
|
|
59
|
+
if (e.key === "Escape" && this.isOpen) {
|
|
60
|
+
this.close(e);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Focus trap
|
|
66
|
+
if (this.options.trapFocus) {
|
|
67
|
+
this.panel.addEventListener("keydown", (e) =>
|
|
68
|
+
this.handleFocusTrap(e)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setupAria() {
|
|
74
|
+
// Generate unique IDs if not present
|
|
75
|
+
if (!this.container.id) {
|
|
76
|
+
this.container.id = `offcanvas-${Math.random()
|
|
77
|
+
.toString(36)
|
|
78
|
+
.substr(2, 9)}`;
|
|
79
|
+
}
|
|
80
|
+
if (!this.panel.id) {
|
|
81
|
+
this.panel.id = `${this.container.id}-panel`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Panel ARIA attributes
|
|
85
|
+
this.panel.setAttribute("role", "dialog");
|
|
86
|
+
this.panel.setAttribute("aria-modal", "true");
|
|
87
|
+
this.panel.setAttribute("aria-hidden", "true");
|
|
88
|
+
this.panel.setAttribute("tabindex", "-1");
|
|
89
|
+
|
|
90
|
+
// Find title for aria-labelledby
|
|
91
|
+
const title = this.panel.querySelector("[data-offcanvas-title]");
|
|
92
|
+
if (title) {
|
|
93
|
+
if (!title.id) {
|
|
94
|
+
title.id = `${this.container.id}-title`;
|
|
95
|
+
}
|
|
96
|
+
this.panel.setAttribute("aria-labelledby", title.id);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Backdrop ARIA
|
|
100
|
+
if (this.backdrop) {
|
|
101
|
+
this.backdrop.setAttribute("aria-hidden", "true");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Trigger ARIA
|
|
105
|
+
this.triggers.forEach((trigger) => {
|
|
106
|
+
trigger.setAttribute("aria-controls", this.panel.id);
|
|
107
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
open(e) {
|
|
112
|
+
if (e) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
this.lastFocusedElement = e.target;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.isOpen) return;
|
|
118
|
+
|
|
119
|
+
this.isOpen = true;
|
|
120
|
+
this.panel.setAttribute("aria-hidden", "false");
|
|
121
|
+
if (this.backdrop) {
|
|
122
|
+
this.backdrop.setAttribute("aria-hidden", "false");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.triggers.forEach((trigger) => {
|
|
126
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Scroll lock
|
|
130
|
+
if (this.options.scrollLock) {
|
|
131
|
+
document.body.classList.add("offcanvas-open");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Update focusable elements
|
|
135
|
+
this.updateFocusableElements();
|
|
136
|
+
|
|
137
|
+
// Focus first element
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
if (this.firstFocusable) {
|
|
140
|
+
this.firstFocusable.focus();
|
|
141
|
+
} else {
|
|
142
|
+
this.panel.focus();
|
|
143
|
+
}
|
|
144
|
+
}, 100);
|
|
145
|
+
|
|
146
|
+
// Callback
|
|
147
|
+
if (this.options.onOpen) {
|
|
148
|
+
this.options.onOpen(this);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
close(e) {
|
|
153
|
+
if (e) {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!this.isOpen) return;
|
|
158
|
+
|
|
159
|
+
this.isOpen = false;
|
|
160
|
+
this.panel.setAttribute("aria-hidden", "true");
|
|
161
|
+
if (this.backdrop) {
|
|
162
|
+
this.backdrop.setAttribute("aria-hidden", "true");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.triggers.forEach((trigger) => {
|
|
166
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Scroll lock
|
|
170
|
+
if (this.options.scrollLock) {
|
|
171
|
+
document.body.classList.remove("offcanvas-open");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Return focus to trigger
|
|
175
|
+
if (this.lastFocusedElement) {
|
|
176
|
+
this.lastFocusedElement.focus();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Callback
|
|
180
|
+
if (this.options.onClose) {
|
|
181
|
+
this.options.onClose(this);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
updateFocusableElements() {
|
|
186
|
+
// Find all focusable elements
|
|
187
|
+
const focusableSelectors = [
|
|
188
|
+
'a[href]',
|
|
189
|
+
'button:not([disabled])',
|
|
190
|
+
'textarea:not([disabled])',
|
|
191
|
+
'input:not([disabled])',
|
|
192
|
+
'select:not([disabled])',
|
|
193
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
this.focusableElements = Array.from(
|
|
197
|
+
this.panel.querySelectorAll(focusableSelectors.join(","))
|
|
198
|
+
).filter((el) => {
|
|
199
|
+
return (
|
|
200
|
+
el.offsetWidth > 0 ||
|
|
201
|
+
el.offsetHeight > 0 ||
|
|
202
|
+
el.getClientRects().length > 0
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
this.firstFocusable = this.focusableElements[0] || null;
|
|
207
|
+
this.lastFocusable =
|
|
208
|
+
this.focusableElements[this.focusableElements.length - 1] || null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
handleFocusTrap(e) {
|
|
212
|
+
if (!this.isOpen || e.key !== "Tab") return;
|
|
213
|
+
|
|
214
|
+
// If no focusable elements, prevent default
|
|
215
|
+
if (this.focusableElements.length === 0) {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Shift + Tab (backward)
|
|
221
|
+
if (e.shiftKey) {
|
|
222
|
+
if (document.activeElement === this.firstFocusable) {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
this.lastFocusable.focus();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Tab (forward)
|
|
228
|
+
else {
|
|
229
|
+
if (document.activeElement === this.lastFocusable) {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
this.firstFocusable.focus();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
destroy() {
|
|
237
|
+
// Remove event listeners
|
|
238
|
+
this.triggers.forEach((trigger) => {
|
|
239
|
+
trigger.removeEventListener("click", this.open);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
this.closeButtons.forEach((button) => {
|
|
243
|
+
button.removeEventListener("click", this.close);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (this.backdrop) {
|
|
247
|
+
this.backdrop.removeEventListener("click", this.close);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Remove scroll lock
|
|
251
|
+
if (this.options.scrollLock) {
|
|
252
|
+
document.body.classList.remove("offcanvas-open");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Close if open
|
|
256
|
+
if (this.isOpen) {
|
|
257
|
+
this.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Auto-initialize offcanvas
|
|
263
|
+
function initOffcanvas() {
|
|
264
|
+
const offcanvasElements = document.querySelectorAll("[data-offcanvas]");
|
|
265
|
+
const instances = [];
|
|
266
|
+
|
|
267
|
+
offcanvasElements.forEach((element) => {
|
|
268
|
+
const options = {
|
|
269
|
+
closeOnBackdrop: element.dataset.closeOnBackdrop !== "false",
|
|
270
|
+
closeOnEscape: element.dataset.closeOnEscape !== "false",
|
|
271
|
+
trapFocus: element.dataset.trapFocus !== "false",
|
|
272
|
+
scrollLock: element.dataset.scrollLock !== "false",
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
instances.push(new AccessibleOffcanvas(element, options));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return instances;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Initialize on DOM ready (only if not using module bundler)
|
|
282
|
+
if (typeof window !== 'undefined' && !window.a11yKitManualInit) {
|
|
283
|
+
if (document.readyState === 'loading') {
|
|
284
|
+
document.addEventListener('DOMContentLoaded', initOffcanvas);
|
|
285
|
+
} else {
|
|
286
|
+
initOffcanvas();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Register in global namespace for CDN usage
|
|
291
|
+
if (typeof window !== 'undefined') {
|
|
292
|
+
window.a11yKit = window.a11yKit || {};
|
|
293
|
+
window.a11yKit.Offcanvas = AccessibleOffcanvas;
|
|
294
|
+
window.a11yKit.initOffcanvas = initOffcanvas;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ES6 exports with short aliases
|
|
298
|
+
export { AccessibleOffcanvas, AccessibleOffcanvas as Offcanvas, initOffcanvas };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessible Tabs Component
|
|
3
|
+
* Vanilla JS, a11y friendly, keyboard navigation support
|
|
4
|
+
* Follows WAI-ARIA Authoring Practices Guide for Tabs Pattern
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class AccessibleTabs {
|
|
8
|
+
constructor(element, options = {}) {
|
|
9
|
+
this.container = element;
|
|
10
|
+
this.tablist = element.querySelector("[data-tabs-list]");
|
|
11
|
+
this.tabs = Array.from(
|
|
12
|
+
this.tablist.querySelectorAll("[data-tabs-tab]")
|
|
13
|
+
);
|
|
14
|
+
this.panels = Array.from(
|
|
15
|
+
element.querySelectorAll("[data-tabs-panel]")
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// Options
|
|
19
|
+
this.options = {
|
|
20
|
+
activeIndex: options.activeIndex || 0,
|
|
21
|
+
automatic: options.automatic !== false, // Automatic activation on arrow keys
|
|
22
|
+
orientation: options.orientation || "horizontal", // horizontal or vertical
|
|
23
|
+
onChange: options.onChange || null,
|
|
24
|
+
onTabClick: options.onTabClick || null,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
this.currentIndex = this.options.activeIndex;
|
|
28
|
+
|
|
29
|
+
this.init();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
init() {
|
|
33
|
+
// Set up ARIA attributes
|
|
34
|
+
this.setupAria();
|
|
35
|
+
|
|
36
|
+
// Activate initial tab
|
|
37
|
+
this.activateTab(this.currentIndex, false);
|
|
38
|
+
|
|
39
|
+
// Bind event listeners
|
|
40
|
+
this.tabs.forEach((tab, index) => {
|
|
41
|
+
tab.addEventListener("click", (e) => this.handleTabClick(e, index));
|
|
42
|
+
tab.addEventListener("keydown", (e) =>
|
|
43
|
+
this.handleTabKeydown(e, index)
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setupAria() {
|
|
49
|
+
// Generate unique IDs if not present
|
|
50
|
+
const baseId = `tabs-${Math.random().toString(36).substr(2, 9)}`;
|
|
51
|
+
|
|
52
|
+
// Setup tablist
|
|
53
|
+
this.tablist.setAttribute("role", "tablist");
|
|
54
|
+
const orientation =
|
|
55
|
+
this.container.classList.contains("tabs--vertical")
|
|
56
|
+
? "vertical"
|
|
57
|
+
: "horizontal";
|
|
58
|
+
this.tablist.setAttribute("aria-orientation", orientation);
|
|
59
|
+
|
|
60
|
+
// Setup tabs and panels
|
|
61
|
+
this.tabs.forEach((tab, index) => {
|
|
62
|
+
const panel = this.panels[index];
|
|
63
|
+
|
|
64
|
+
// Generate IDs
|
|
65
|
+
if (!tab.id) {
|
|
66
|
+
tab.id = `${baseId}-tab-${index}`;
|
|
67
|
+
}
|
|
68
|
+
if (!panel.id) {
|
|
69
|
+
panel.id = `${baseId}-panel-${index}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Tab attributes
|
|
73
|
+
tab.setAttribute("role", "tab");
|
|
74
|
+
tab.setAttribute("aria-controls", panel.id);
|
|
75
|
+
tab.setAttribute("aria-selected", "false");
|
|
76
|
+
tab.setAttribute("tabindex", "-1");
|
|
77
|
+
|
|
78
|
+
// Check if tab is disabled
|
|
79
|
+
if (
|
|
80
|
+
tab.hasAttribute("disabled") ||
|
|
81
|
+
tab.hasAttribute("aria-disabled")
|
|
82
|
+
) {
|
|
83
|
+
tab.setAttribute("aria-disabled", "true");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Panel attributes
|
|
87
|
+
panel.setAttribute("role", "tabpanel");
|
|
88
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
|
89
|
+
panel.setAttribute("tabindex", "0");
|
|
90
|
+
panel.setAttribute("aria-hidden", "true");
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
handleTabClick(e, index) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
|
|
97
|
+
// Don't activate disabled tabs
|
|
98
|
+
if (this.isTabDisabled(index)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.activateTab(index);
|
|
103
|
+
|
|
104
|
+
if (this.options.onTabClick) {
|
|
105
|
+
this.options.onTabClick(this.tabs[index], index, this);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
handleTabKeydown(e, index) {
|
|
110
|
+
const orientation = this.tablist.getAttribute("aria-orientation");
|
|
111
|
+
let targetIndex = null;
|
|
112
|
+
|
|
113
|
+
switch (e.key) {
|
|
114
|
+
case "ArrowLeft":
|
|
115
|
+
if (orientation === "horizontal") {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
targetIndex = this.getPreviousEnabledTabIndex(index);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case "ArrowRight":
|
|
122
|
+
if (orientation === "horizontal") {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
targetIndex = this.getNextEnabledTabIndex(index);
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case "ArrowUp":
|
|
129
|
+
if (orientation === "vertical") {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
targetIndex = this.getPreviousEnabledTabIndex(index);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
case "ArrowDown":
|
|
136
|
+
if (orientation === "vertical") {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
targetIndex = this.getNextEnabledTabIndex(index);
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case "Home":
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
targetIndex = this.getFirstEnabledTabIndex();
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case "End":
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
targetIndex = this.getLastEnabledTabIndex();
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case "Enter":
|
|
153
|
+
case " ":
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
if (!this.isTabDisabled(index)) {
|
|
156
|
+
this.activateTab(index);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Move focus and optionally activate
|
|
162
|
+
if (targetIndex !== null) {
|
|
163
|
+
this.tabs[targetIndex].focus();
|
|
164
|
+
|
|
165
|
+
// Automatic activation
|
|
166
|
+
if (this.options.automatic && !this.isTabDisabled(targetIndex)) {
|
|
167
|
+
this.activateTab(targetIndex);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
activateTab(index, focus = true) {
|
|
173
|
+
if (index < 0 || index >= this.tabs.length) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Don't activate disabled tabs
|
|
178
|
+
if (this.isTabDisabled(index)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const previousIndex = this.currentIndex;
|
|
183
|
+
|
|
184
|
+
// Function to update DOM
|
|
185
|
+
const updateDOM = () => {
|
|
186
|
+
// Deactivate all tabs and panels
|
|
187
|
+
this.tabs.forEach((tab, i) => {
|
|
188
|
+
const isActive = i === index;
|
|
189
|
+
|
|
190
|
+
tab.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
191
|
+
tab.setAttribute("tabindex", isActive ? "0" : "-1");
|
|
192
|
+
|
|
193
|
+
this.panels[i].setAttribute("aria-hidden", isActive ? "false" : "true");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.currentIndex = index;
|
|
197
|
+
|
|
198
|
+
// Focus the activated tab
|
|
199
|
+
if (focus) {
|
|
200
|
+
this.tabs[index].focus();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Callback
|
|
204
|
+
if (this.options.onChange && previousIndex !== index) {
|
|
205
|
+
this.options.onChange(
|
|
206
|
+
this.tabs[index],
|
|
207
|
+
this.panels[index],
|
|
208
|
+
index,
|
|
209
|
+
this
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Use View Transitions API if available and animations are enabled
|
|
215
|
+
if (!this.container.hasAttribute('data-no-animation') &&
|
|
216
|
+
document.startViewTransition &&
|
|
217
|
+
!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
218
|
+
document.startViewTransition(() => updateDOM());
|
|
219
|
+
} else {
|
|
220
|
+
updateDOM();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
isTabDisabled(index) {
|
|
225
|
+
const tab = this.tabs[index];
|
|
226
|
+
return (
|
|
227
|
+
tab.hasAttribute("disabled") ||
|
|
228
|
+
tab.getAttribute("aria-disabled") === "true"
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getNextEnabledTabIndex(currentIndex) {
|
|
233
|
+
let nextIndex = currentIndex + 1;
|
|
234
|
+
|
|
235
|
+
// Loop to beginning
|
|
236
|
+
if (nextIndex >= this.tabs.length) {
|
|
237
|
+
nextIndex = 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Skip disabled tabs
|
|
241
|
+
while (this.isTabDisabled(nextIndex) && nextIndex !== currentIndex) {
|
|
242
|
+
nextIndex++;
|
|
243
|
+
if (nextIndex >= this.tabs.length) {
|
|
244
|
+
nextIndex = 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return nextIndex;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getPreviousEnabledTabIndex(currentIndex) {
|
|
252
|
+
let prevIndex = currentIndex - 1;
|
|
253
|
+
|
|
254
|
+
// Loop to end
|
|
255
|
+
if (prevIndex < 0) {
|
|
256
|
+
prevIndex = this.tabs.length - 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Skip disabled tabs
|
|
260
|
+
while (this.isTabDisabled(prevIndex) && prevIndex !== currentIndex) {
|
|
261
|
+
prevIndex--;
|
|
262
|
+
if (prevIndex < 0) {
|
|
263
|
+
prevIndex = this.tabs.length - 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return prevIndex;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
getFirstEnabledTabIndex() {
|
|
271
|
+
for (let i = 0; i < this.tabs.length; i++) {
|
|
272
|
+
if (!this.isTabDisabled(i)) {
|
|
273
|
+
return i;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
getLastEnabledTabIndex() {
|
|
280
|
+
for (let i = this.tabs.length - 1; i >= 0; i--) {
|
|
281
|
+
if (!this.isTabDisabled(i)) {
|
|
282
|
+
return i;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return this.tabs.length - 1;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
destroy() {
|
|
289
|
+
// Remove event listeners
|
|
290
|
+
this.tabs.forEach((tab, index) => {
|
|
291
|
+
tab.removeEventListener("click", (e) =>
|
|
292
|
+
this.handleTabClick(e, index)
|
|
293
|
+
);
|
|
294
|
+
tab.removeEventListener("keydown", (e) =>
|
|
295
|
+
this.handleTabKeydown(e, index)
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Auto-initialize tabs
|
|
302
|
+
function initTabs() {
|
|
303
|
+
const tabsElements = document.querySelectorAll("[data-tabs]");
|
|
304
|
+
const instances = [];
|
|
305
|
+
|
|
306
|
+
tabsElements.forEach((element) => {
|
|
307
|
+
const options = {
|
|
308
|
+
activeIndex: parseInt(element.dataset.activeIndex) || 0,
|
|
309
|
+
automatic: element.dataset.automatic !== "false",
|
|
310
|
+
orientation: element.dataset.orientation || "horizontal",
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
instances.push(new AccessibleTabs(element, options));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return instances;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Initialize on DOM ready (only if not using module bundler)
|
|
320
|
+
if (typeof window !== 'undefined' && !window.a11yKitManualInit) {
|
|
321
|
+
if (document.readyState === 'loading') {
|
|
322
|
+
document.addEventListener('DOMContentLoaded', initTabs);
|
|
323
|
+
} else {
|
|
324
|
+
initTabs();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Register in global namespace for CDN usage
|
|
329
|
+
if (typeof window !== 'undefined') {
|
|
330
|
+
window.a11yKit = window.a11yKit || {};
|
|
331
|
+
window.a11yKit.Tabs = AccessibleTabs;
|
|
332
|
+
window.a11yKit.initTabs = initTabs;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ES6 exports with short aliases
|
|
336
|
+
export { AccessibleTabs, AccessibleTabs as Tabs, initTabs };
|
package/src/js/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a11y-kit
|
|
3
|
+
* Lightweight, accessible UI component library with full ARIA support
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Export all components with both full and short aliases
|
|
10
|
+
export { AccessibleDropdown, Dropdown, initDropdowns } from './a11y-dropdown.js';
|
|
11
|
+
export { AccessibleTabs, Tabs, initTabs } from './a11y-tabs.js';
|
|
12
|
+
export { AccessibleOffcanvas, Offcanvas, initOffcanvas } from './a11y-offcanvas.js';
|
|
13
|
+
export { AccessibleModal, Modal, initModals } from './a11y-modal.js';
|
|
14
|
+
export { AccessibleCollapse, Collapse, initCollapses, AccessibleAccordion, Accordion, initAccordions } from './a11y-accordion.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize all components at once
|
|
18
|
+
* Convenient helper for auto-initializing all a11y-kit components
|
|
19
|
+
*/
|
|
20
|
+
export function initAll() {
|
|
21
|
+
const instances = {
|
|
22
|
+
dropdowns: initDropdowns(),
|
|
23
|
+
tabs: initTabs(),
|
|
24
|
+
offcanvas: initOffcanvas(),
|
|
25
|
+
modals: initModals(),
|
|
26
|
+
collapses: initCollapses(),
|
|
27
|
+
accordions: initAccordions()
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return instances;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Auto-initialize on DOM ready if not using module bundler
|
|
34
|
+
if (typeof window !== 'undefined' && !window.a11yKitManualInit) {
|
|
35
|
+
if (document.readyState === 'loading') {
|
|
36
|
+
document.addEventListener('DOMContentLoaded', initAll);
|
|
37
|
+
} else {
|
|
38
|
+
initAll();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Register in global namespace for CDN usage
|
|
43
|
+
if (typeof window !== 'undefined') {
|
|
44
|
+
window.a11yKit = window.a11yKit || {};
|
|
45
|
+
window.a11yKit.initAll = initAll;
|
|
46
|
+
}
|