vue-a11y-directives
Version:
A comprehensive set of Vue 3 directives for building accessible web applications with WCAG compliance
431 lines (430 loc) • 17.1 kB
JavaScript
const K = ["BUTTON", "A", "INPUT", "TEXTAREA", "SELECT", "IFRAME"], H = {
mounted(e, a) {
const t = typeof a.value == "object" ? a.value : { delay: a.value || 0 }, n = t.delay || 0, i = t.select || !1;
K.includes(e.tagName) || e.hasAttribute("tabindex") || e.hasAttribute("contenteditable") || (e.setAttribute("tabindex", "0"), e.__a11yFocusAddedTabindex = !0), setTimeout(() => {
e.focus && (e.focus(), i && (e.tagName === "INPUT" || e.tagName === "TEXTAREA") && e.select());
}, n);
},
unmounted(e) {
e.__a11yFocusAddedTabindex && (e.removeAttribute("tabindex"), delete e.__a11yFocusAddedTabindex);
}
};
function m(e) {
if (!e) return [];
const a = [
"a[href]",
"button:not([disabled])",
"textarea:not([disabled])",
'input:not([disabled]):not([type="hidden"])',
"select:not([disabled])",
'[tabindex]:not([tabindex="-1"])',
'[contenteditable="true"]',
"audio[controls]",
"video[controls]"
];
return Array.from(e.querySelectorAll(a.join(", "))).filter((n) => {
const i = window.getComputedStyle(n);
return n.offsetWidth > 0 && n.offsetHeight > 0 && i.visibility !== "hidden" && i.display !== "none";
});
}
function T(e, a) {
Object.entries(a).forEach(([t, n]) => {
n != null && e.setAttribute(t, n);
});
}
function $(e, a) {
a.forEach((t) => {
e.removeAttribute(t);
});
}
function F(e, a = "polite") {
const t = document.createElement("div");
t.setAttribute("role", "status"), t.setAttribute("aria-live", a), t.setAttribute("aria-atomic", "true"), t.style.position = "absolute", t.style.left = "-10000px", t.style.width = "1px", t.style.height = "1px", t.style.overflow = "hidden", document.body.appendChild(t), setTimeout(() => {
t.textContent = e;
}, 100), setTimeout(() => {
document.body.removeChild(t);
}, 3e3);
}
function J(e) {
const a = m(e);
return a.length > 0 ? a[0] : null;
}
function G(e) {
const a = m(e);
return a.length > 0 ? a[a.length - 1] : null;
}
function z(e) {
return e.contains(document.activeElement);
}
function Q() {
return document.activeElement;
}
function Y(e) {
e && e.focus && e.focus();
}
function Z(e = "a11y") {
return `${e}-${Math.random().toString(36).substr(2, 9)}`;
}
const C = {
mounted(e, a) {
const t = a.value || {}, n = t.autoFocus !== !1, i = t.onEscape || null, r = document.activeElement;
e.__previousFocus = r, n && setTimeout(() => {
const u = m(e);
u.length > 0 && u[0].focus();
}, 100);
const o = (u) => {
if (u.key !== "Tab") return;
const y = m(e);
if (y.length === 0) return;
const k = y[0], h = y[y.length - 1];
u.shiftKey && document.activeElement === k ? (u.preventDefault(), h.focus()) : !u.shiftKey && document.activeElement === h && (u.preventDefault(), k.focus());
}, d = (u) => {
u.key === "Escape" && i && (u.preventDefault(), u.stopPropagation(), i());
};
e.addEventListener("keydown", o), e.__handleKeydown = o, i && (document.addEventListener("keydown", d, !0), e.__handleEscape = d);
},
unmounted(e) {
e.__handleKeydown && (e.removeEventListener("keydown", e.__handleKeydown), delete e.__handleKeydown), e.__handleEscape && (document.removeEventListener("keydown", e.__handleEscape, !0), delete e.__handleEscape), e.__previousFocus && (setTimeout(() => {
if (e.__previousFocus && document.body.contains(e.__previousFocus) && typeof e.__previousFocus.focus == "function")
try {
e.__previousFocus.focus();
} catch (a) {
console.warn("Could not restore focus:", a);
}
}, 0), delete e.__previousFocus);
}
}, N = {
mounted(e, a) {
const t = a.value || {};
console.log("Keyboard directive mounted with config:", t);
const n = (i) => {
console.log("Key pressed:", i.key, "Config:", t);
const r = {
Enter: "enter",
" ": "space",
Escape: "escape",
ArrowUp: "arrowUp",
ArrowDown: "arrowDown",
ArrowLeft: "arrowLeft",
ArrowRight: "arrowRight",
Tab: "tab",
Delete: "delete",
Backspace: "backspace"
};
let o = "";
(i.ctrlKey || i.metaKey) && (o += "ctrl+"), i.altKey && (o += "alt+"), i.shiftKey && (o += "shift+"), o += i.key.toLowerCase();
const d = r[i.key];
if (d && typeof t[d] == "function") {
i.preventDefault(), t[d](i);
return;
}
if (t[o] && typeof t[o] == "function") {
i.preventDefault(), t[o](i);
return;
}
if (t[i.key] && typeof t[i.key] == "function") {
i.preventDefault(), t[i.key](i);
return;
}
if (t.arrows && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(i.key) && typeof t.arrows == "function") {
i.preventDefault(), t.arrows(i);
return;
}
if (t.handlers) {
if (t.handlers[i.key] && typeof t.handlers[i.key] == "function") {
i.preventDefault(), t.handlers[i.key](i);
return;
}
if (t.handlers[o] && typeof t.handlers[o] == "function") {
i.preventDefault(), t.handlers[o](i);
return;
}
}
if (t.custom && typeof t.custom == "function") {
t.custom(i);
return;
}
t.enter === !0 && i.key === "Enter" && !["INPUT", "TEXTAREA", "SELECT"].includes(i.target.tagName) && (i.preventDefault(), e.click && e.click()), t.space === !0 && i.key === " " && ["BUTTON", "A"].includes(e.tagName) && (i.preventDefault(), e.click && e.click());
};
e.addEventListener("keydown", n), e.__keyboardHandler = n;
},
unmounted(e) {
e.__keyboardHandler && (e.removeEventListener("keydown", e.__keyboardHandler), delete e.__keyboardHandler);
}
}, q = {
mounted(e, a) {
if (!a.value) return;
const t = typeof a.value == "string" ? { message: a.value, priority: "polite" } : a.value;
t && t.message && F(t.message, t.priority || "polite");
},
updated(e, a) {
if (a.value !== a.oldValue && a.value) {
const t = typeof a.value == "string" ? { message: a.value, priority: "polite" } : a.value;
t && t.message && F(t.message, t.priority || "polite");
}
}
}, U = {
mounted(e, a) {
const t = a.value, n = (i) => {
i.preventDefault();
const r = document.querySelector(t);
r && (r.hasAttribute("tabindex") || r.setAttribute("tabindex", "-1"), r.focus(), r.scrollIntoView({ behavior: "smooth", block: "start" }));
};
e.addEventListener("click", n), e.__skipLinkHandler = n, e.setAttribute("role", "link"), t && e.setAttribute("aria-label", `Skip to ${t.replace("#", "")}`);
},
unmounted(e) {
e.__skipLinkHandler && (e.removeEventListener("click", e.__skipLinkHandler), delete e.__skipLinkHandler);
}
}, R = {
mounted(e, a) {
O(e, a);
},
updated(e, a) {
O(e, a);
},
unmounted(e) {
delete e.__a11ySkipOriginalTabIndex, delete e.__a11ySkipOriginalAriaHidden, delete e.__a11ySkipChildrenState;
}
};
function O(e, a) {
if (a.value === void 0 ? !0 : !!a.value) {
e.__a11ySkipOriginalTabIndex === void 0 && (e.__a11ySkipOriginalTabIndex = e.getAttribute("tabindex")), e.__a11ySkipOriginalAriaHidden === void 0 && (e.__a11ySkipOriginalAriaHidden = e.getAttribute("aria-hidden")), e.setAttribute("tabindex", "-1"), e.setAttribute("aria-hidden", "true"), e.setAttribute("data-a11y-skip", "true");
const n = e.querySelectorAll(
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
e.__a11ySkipChildrenState || (e.__a11ySkipChildrenState = []), n.forEach((i, r) => {
e.__a11ySkipChildrenState[r] || (e.__a11ySkipChildrenState[r] = {
tabIndex: i.getAttribute("tabindex"),
disabled: i.disabled
}), i.setAttribute("tabindex", "-1"), "disabled" in i && (i.disabled = !0);
}), a.modifiers.noInteraction && (e.style.pointerEvents = "none"), a.modifiers.visual && (e.style.opacity = "0.5", e.style.filter = "grayscale(100%)", e.style.pointerEvents = "none");
} else {
e.__a11ySkipOriginalTabIndex !== void 0 ? e.__a11ySkipOriginalTabIndex === null ? e.removeAttribute("tabindex") : e.setAttribute("tabindex", e.__a11ySkipOriginalTabIndex) : e.removeAttribute("tabindex"), e.__a11ySkipOriginalAriaHidden !== void 0 ? e.__a11ySkipOriginalAriaHidden === null ? e.removeAttribute("aria-hidden") : e.setAttribute("aria-hidden", e.__a11ySkipOriginalAriaHidden) : e.removeAttribute("aria-hidden"), e.removeAttribute("data-a11y-skip");
const n = e.querySelectorAll(
"a, button, input, select, textarea, [tabindex]"
);
e.__a11ySkipChildrenState && n.forEach((i, r) => {
const o = e.__a11ySkipChildrenState[r];
o && (o.tabIndex === null ? i.removeAttribute("tabindex") : i.setAttribute("tabindex", o.tabIndex), "disabled" in i && (i.disabled = o.disabled));
}), a.modifiers.noInteraction && (e.style.pointerEvents = ""), a.modifiers.visual && (e.style.opacity = "", e.style.filter = "", e.style.pointerEvents = "");
}
}
const j = {
mounted(e, a) {
const t = a.value || {}, n = {
label: "aria-label",
labelledby: "aria-labelledby",
describedby: "aria-describedby",
expanded: "aria-expanded",
pressed: "aria-pressed",
selected: "aria-selected",
checked: "aria-checked",
disabled: "aria-disabled",
hidden: "aria-hidden",
invalid: "aria-invalid",
required: "aria-required",
live: "aria-live",
atomic: "aria-atomic",
busy: "aria-busy",
controls: "aria-controls",
owns: "aria-owns",
haspopup: "aria-haspopup",
level: "aria-level",
modal: "aria-modal",
multiselectable: "aria-multiselectable",
orientation: "aria-orientation",
placeholder: "aria-placeholder",
readonly: "aria-readonly",
relevant: "aria-relevant",
valuemax: "aria-valuemax",
valuemin: "aria-valuemin",
valuenow: "aria-valuenow",
valuetext: "aria-valuetext"
}, i = {};
Object.entries(t).forEach(([r, o]) => {
const d = n[r] || r;
i[d] = o;
}), T(e, i), e.__ariaAttributes = Object.keys(i);
},
updated(e, a) {
if (JSON.stringify(a.value) !== JSON.stringify(a.oldValue)) {
e.__ariaAttributes && e.__ariaAttributes.forEach((r) => e.removeAttribute(r));
const t = a.value || {}, n = {
label: "aria-label",
labelledby: "aria-labelledby",
describedby: "aria-describedby",
expanded: "aria-expanded",
pressed: "aria-pressed",
selected: "aria-selected",
checked: "aria-checked",
disabled: "aria-disabled",
hidden: "aria-hidden",
invalid: "aria-invalid",
required: "aria-required",
live: "aria-live",
atomic: "aria-atomic",
busy: "aria-busy",
controls: "aria-controls",
owns: "aria-owns",
haspopup: "aria-haspopup",
level: "aria-level",
modal: "aria-modal"
}, i = {};
Object.entries(t).forEach(([r, o]) => {
const d = n[r] || r;
i[d] = o;
}), T(e, i), e.__ariaAttributes = Object.keys(i);
}
},
unmounted(e) {
e.__ariaAttributes && (e.__ariaAttributes.forEach((a) => e.removeAttribute(a)), delete e.__ariaAttributes);
}
}, M = {
mounted(e, a) {
const t = a.value || {}, n = t.delay || 100, i = {
// Panel selectors (where the calendar appears)
panel: t.panelSelector || ".el-picker-panel, .el-date-picker, .v-picker, .v-date-picker, .ant-picker-dropdown, .p-datepicker",
// Selected day selectors (current selected date)
selectedDay: t.selectedSelector || "td.is-selected, td.selected, td.is-today.is-selected, .v-date-picker-table__current, .ant-picker-cell-selected, .p-highlight",
// Available day selectors (any selectable date)
availableDay: t.availableSelector || "td:not(.disabled):not(.is-disabled):not([disabled]), .v-date-picker-table__events, .ant-picker-cell:not(.ant-picker-cell-disabled), .p-datepicker-calendar td:not(.p-disabled)",
// Input selectors (the input field)
input: t.inputSelector || ".el-input__inner, input, .v-text-field__input, .ant-picker-input, .p-inputtext"
};
let r = !1, o = null, d = null;
const u = (c) => {
const _ = c.getAttribute("aria-controls");
let s = null;
if (_ && (s = document.getElementById(_)), !s) {
const f = document.querySelectorAll(".el-picker-panel");
for (const v of f)
if (v.offsetParent !== null) {
s = v;
break;
}
}
if (!s)
return !1;
let l = null;
const g = s.querySelector("td.is-selected");
if (g && (l = g), !l) {
const f = s.querySelector("td.is-today");
f && (l = f);
}
if (!l) {
const f = s.querySelector(".el-date-table tbody td:not(.disabled):not(.is-disabled)");
f && (l = f);
}
if (!l)
return !1;
l.setAttribute("tabindex", "0");
let w = 0;
const L = 10, S = () => {
if (w++, l.focus({ preventScroll: !0 }), document.activeElement === l)
return !0;
w < L && setTimeout(S, 10);
};
S();
const x = (f) => {
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(f.key)) {
f.preventDefault(), s.__a11yNavigatingWithKeyboard = !0;
const v = (b) => (b.preventDefault(), b.stopPropagation(), b.stopImmediatePropagation(), !1), D = ["click", "mousedown", "mouseup", "pointerdown", "pointerup", "touchstart", "touchend"];
D.forEach((b) => {
s.addEventListener(b, v, { capture: !0, once: !0 });
}), s.querySelectorAll("td").forEach((b) => {
D.forEach((I) => {
b.addEventListener(I, v, { capture: !0, once: !0 });
});
}), setTimeout(() => {
s.__a11yNavigatingWithKeyboard = !1;
}, 50);
}
};
return s.__a11yKeyboardHandlerAdded || (s.__a11yKeyboardHandlerAdded = !0, s.addEventListener("keydown", x, { capture: !0 }), s.__a11yKeyboardHandler = x), !0;
}, y = (c) => {
d && clearTimeout(d), requestAnimationFrame(() => {
d = setTimeout(() => {
u(c);
}, n);
});
}, k = (c) => {
o = new MutationObserver((_) => {
const s = document.querySelector(".el-picker-panel");
s && !r ? (r = !0, y(c)) : !s && r && (r = !1, d && (clearTimeout(d), d = null));
}), o.observe(document.body, {
childList: !0,
subtree: !0
});
}, p = (() => {
let c = e.querySelector(i.input);
return c || (c = e.querySelectorAll("input")[0]), c || e;
})();
k(p);
const A = () => {
};
p.addEventListener("focus", A), p.addEventListener("click", A);
const E = (c) => {
c.key === "Enter" || c.key;
};
p.addEventListener("keydown", E);
const P = (() => {
const c = new MutationObserver((_) => {
_.forEach((s) => {
s.type === "attributes" && s.attributeName === "aria-expanded" && p.getAttribute("aria-expanded") === "true" && y(p);
});
});
return c.observe(p, {
attributes: !0,
attributeFilter: ["aria-expanded"]
}), c;
})();
e.__a11yDatePickerAriaObserver = P, e.__a11yDatePickerInput = p, e.__a11yDatePickerFocusHandler = A, e.__a11yDatePickerKeyHandler = E, e.__a11yDatePickerObserver = o;
},
unmounted(e) {
if (e.__a11yDatePickerTimeout && clearTimeout(e.__a11yDatePickerTimeout), e.__a11yDatePickerInput) {
const a = e.__a11yDatePickerInput;
e.__a11yDatePickerFocusHandler && (a.removeEventListener("focus", e.__a11yDatePickerFocusHandler), a.removeEventListener("click", e.__a11yDatePickerFocusHandler)), e.__a11yDatePickerKeyHandler && a.removeEventListener("keydown", e.__a11yDatePickerKeyHandler);
}
e.__a11yDatePickerObserver && e.__a11yDatePickerObserver.disconnect(), e.__a11yDatePickerAriaObserver && e.__a11yDatePickerAriaObserver.disconnect(), delete e.__a11yDatePickerInput, delete e.__a11yDatePickerFocusHandler, delete e.__a11yDatePickerKeyHandler, delete e.__a11yDatePickerObserver, delete e.__a11yDatePickerAriaObserver, delete e.__a11yDatePickerTimeout;
}
}, B = {
"a11y-focus": H,
"a11y-trap-focus": C,
"a11y-keyboard": N,
"a11y-announce": q,
"a11y-skip-link": U,
"a11y-skip": R,
"a11y-aria": j,
"a11y-date-picker": M
};
function W(e) {
Object.entries(B).forEach(([a, t]) => {
e.directive(a, t);
});
}
const ee = {
install: W
};
export {
B as a11yDirectives,
F as announce,
q as announceDirective,
j as ariaDirective,
z as containsActiveElement,
M as datePickerDirective,
ee as default,
H as focusDirective,
Z as generateId,
J as getFirstFocusable,
m as getFocusableElements,
G as getLastFocusable,
W as installA11yDirectives,
N as keyboardDirective,
$ as removeAriaAttributes,
Y as restoreFocus,
Q as saveFocus,
T as setAriaAttributes,
R as skipDirective,
U as skipLinkDirective,
C as trapFocusDirective
};