@ordojs/accessibility
Version:
Comprehensive accessibility system for OrdoJS with ARIA generation, automated testing, and screen reader support
365 lines (363 loc) • 10.2 kB
JavaScript
import { EventEmitter } from 'events';
// src/focus/index.ts
var FocusManager = class extends EventEmitter {
config;
focusableElements;
focusTraps;
skipLinks;
isInitialized;
/**
* Create a new FocusManager instance
*
* @param config - Focus configuration
*/
constructor(config) {
super();
this.config = config;
this.focusableElements = /* @__PURE__ */ new Map();
this.focusTraps = /* @__PURE__ */ new Map();
this.skipLinks = /* @__PURE__ */ new Map();
this.isInitialized = false;
}
/**
* Initialize the focus manager
*/
async initialize() {
if (this.isInitialized) {
console.warn("Focus manager is already initialized");
return;
}
try {
await this.initializeFocusManagement();
this.isInitialized = true;
console.log("Focus manager initialized successfully");
this.emit("initialized");
} catch (error) {
console.error("Failed to initialize focus manager:", error);
this.emit("error", error);
throw error;
}
}
/**
* Register focusable element
*
* @param element - Element selector
* @param info - Focus information
*/
registerFocusableElement(element, info = {}) {
const focusInfo = {
element,
tabIndex: info.tabIndex || 0,
focusOrder: info.focusOrder || 0,
focusable: info.focusable !== false,
focused: false,
focusTrap: info.focusTrap || false,
skipLink: info.skipLink || false,
ariaState: info.ariaState || {},
ariaProperty: info.ariaProperty || {}
};
if (info.ariaLabel) {
focusInfo.ariaLabel = info.ariaLabel;
}
if (info.ariaDescription) {
focusInfo.ariaDescription = info.ariaDescription;
}
if (info.ariaRole) {
focusInfo.ariaRole = info.ariaRole;
}
this.focusableElements.set(element, focusInfo);
this.emit("elementRegistered", focusInfo);
}
/**
* Create focus trap
*
* @param trapId - Focus trap ID
* @param elements - Elements in the trap
* @param options - Trap options
*/
createFocusTrap(trapId, elements, options = {}) {
this.focusTraps.set(trapId, elements);
elements.forEach((element, index) => {
const focusInfo = this.focusableElements.get(element);
if (focusInfo) {
focusInfo.focusOrder = index;
focusInfo.focusTrap = true;
this.focusableElements.set(element, focusInfo);
}
});
if (options.initialFocus) {
this.setFocus(options.initialFocus);
}
this.emit("focusTrapCreated", { trapId, elements, options });
}
/**
* Remove focus trap
*
* @param trapId - Focus trap ID
* @param options - Removal options
*/
removeFocusTrap(trapId, options = {}) {
const elements = this.focusTraps.get(trapId);
if (elements) {
elements.forEach((element) => {
const focusInfo = this.focusableElements.get(element);
if (focusInfo) {
focusInfo.focusTrap = false;
this.focusableElements.set(element, focusInfo);
}
});
this.focusTraps.delete(trapId);
this.emit("focusTrapRemoved", { trapId, options });
}
}
/**
* Set focus to element
*
* @param element - Element selector
*/
setFocus(element) {
const focusInfo = this.focusableElements.get(element);
if (focusInfo && focusInfo.focusable) {
for (const info of this.focusableElements.values()) {
info.focused = false;
}
focusInfo.focused = true;
this.focusableElements.set(element, focusInfo);
this.emit("focusChanged", focusInfo);
}
}
/**
* Move focus to next element
*
* @param currentElement - Current element
* @returns Next focused element or undefined
*/
moveToNextElement(currentElement) {
const current = this.focusableElements.get(currentElement);
if (!current) return void 0;
const focusableElements = Array.from(this.focusableElements.values()).filter((info) => info.focusable).sort((a, b) => a.focusOrder - b.focusOrder);
const currentIndex = focusableElements.findIndex((info) => info.element === currentElement);
if (currentIndex === -1) return void 0;
const nextIndex = (currentIndex + 1) % focusableElements.length;
const nextElement = focusableElements[nextIndex];
if (nextElement) {
this.setFocus(nextElement.element);
return nextElement;
}
return void 0;
}
/**
* Move focus to previous element
*
* @param currentElement - Current element
* @returns Previous focused element or undefined
*/
moveToPreviousElement(currentElement) {
const current = this.focusableElements.get(currentElement);
if (!current) return void 0;
const focusableElements = Array.from(this.focusableElements.values()).filter((info) => info.focusable).sort((a, b) => a.focusOrder - b.focusOrder);
const currentIndex = focusableElements.findIndex((info) => info.element === currentElement);
if (currentIndex === -1) return void 0;
const prevIndex = currentIndex === 0 ? focusableElements.length - 1 : currentIndex - 1;
const prevElement = focusableElements[prevIndex];
if (prevElement) {
this.setFocus(prevElement.element);
return prevElement;
}
return void 0;
}
/**
* Add skip link
*
* @param target - Target element
* @param label - Skip link label
* @param position - Skip link position
*/
addSkipLink(target, label, position = "top") {
this.skipLinks.set(target, label);
this.emit("skipLinkAdded", { target, label, position });
}
/**
* Remove skip link
*
* @param target - Target element
*/
removeSkipLink(target) {
this.skipLinks.delete(target);
this.emit("skipLinkRemoved", { target });
}
/**
* Handle keyboard navigation
*
* @param event - Keyboard event
* @param currentElement - Current element
* @returns Navigation result
*/
handleKeyboardNavigation(event, currentElement) {
const navigation = {
id: `nav_${Date.now()}`,
type: "custom",
key: event.key,
keyCode: event.keyCode,
target: currentElement,
action: "",
description: "",
enabled: true,
visible: true
};
let handled = false;
let action = "";
switch (event.key) {
case "Tab":
if (event.shiftKey) {
const prevElement = this.moveToPreviousElement(currentElement);
if (prevElement) {
action = "moveToPrevious";
handled = true;
}
} else {
const nextElement = this.moveToNextElement(currentElement);
if (nextElement) {
action = "moveToNext";
handled = true;
}
}
break;
case "Escape":
action = "escape";
handled = true;
break;
case "Enter":
case " ":
action = "activate";
handled = true;
break;
case "ArrowUp":
action = "navigateUp";
handled = true;
break;
case "ArrowDown":
action = "navigateDown";
handled = true;
break;
case "ArrowLeft":
action = "navigateLeft";
handled = true;
break;
case "ArrowRight":
action = "navigateRight";
handled = true;
break;
case "Home":
action = "moveToFirst";
handled = true;
break;
case "End":
action = "moveToLast";
handled = true;
break;
}
if (handled) {
event.preventDefault();
event.stopPropagation();
}
navigation.action = action;
this.emit("keyboardNavigation", navigation);
return {
handled,
action,
target: currentElement
};
}
/**
* Get focusable elements
*
* @returns Array of focusable elements
*/
getFocusableElements() {
return Array.from(this.focusableElements.values()).filter((info) => info.focusable).sort((a, b) => a.focusOrder - b.focusOrder);
}
/**
* Get focus trap elements
*
* @param trapId - Focus trap ID
* @returns Array of trapped elements
*/
getFocusTrapElements(trapId) {
const elements = this.focusTraps.get(trapId);
if (!elements) return [];
return elements.map((element) => this.focusableElements.get(element)).filter((info) => info !== void 0).sort((a, b) => a.focusOrder - b.focusOrder);
}
/**
* Get skip links
*
* @returns Array of skip links
*/
getSkipLinks() {
return Array.from(this.skipLinks.entries()).map(([target, label]) => ({
target,
label
}));
}
/**
* Get currently focused element
*
* @returns Focused element or undefined
*/
getFocusedElement() {
return Array.from(this.focusableElements.values()).find((info) => info.focused);
}
/**
* Get focus manager statistics
*
* @returns Statistics
*/
getStats() {
const totalElements = this.focusableElements.size;
const focusableElements = Array.from(this.focusableElements.values()).filter((info) => info.focusable).length;
const focusedElements = Array.from(this.focusableElements.values()).filter((info) => info.focused).length;
return {
totalElements,
focusableElements,
focusedElements,
focusTraps: this.focusTraps.size,
skipLinks: this.skipLinks.size
};
}
/**
* Initialize focus management
*/
async initializeFocusManagement() {
console.log("Initializing focus management...");
if (this.config.focusIndicators) {
this.setupFocusIndicators();
}
if (this.config.skipLinks) {
this.setupSkipLinks();
}
if (this.config.focusRestoration) {
this.setupFocusRestoration();
}
}
/**
* Setup focus indicators
*/
setupFocusIndicators() {
console.log("Setting up focus indicators...");
}
/**
* Setup skip links
*/
setupSkipLinks() {
console.log("Setting up skip links...");
}
/**
* Setup focus restoration
*/
setupFocusRestoration() {
console.log("Setting up focus restoration...");
}
};
export { FocusManager };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map