UNPKG

@ordojs/accessibility

Version:

Comprehensive accessibility system for OrdoJS with ARIA generation, automated testing, and screen reader support

1,787 lines (1,785 loc) 54.4 kB
'use strict'; var events = require('events'); // src/accessibility-manager.ts var AccessibilityManager = class extends events.EventEmitter { config; isInitialized; auditResults; violations; /** * Create a new AccessibilityManager instance * * @param config - Accessibility configuration */ constructor(config = {}) { super(); this.config = { enableARIA: true, enableTesting: true, enableFocusManagement: true, enableScreenReader: true, enableKeyboardNavigation: true, enableColorContrast: true, enableSemanticHTML: true, enableLiveRegions: true, enableSkipLinks: true, enableFocusIndicators: true, wcagLevel: "AA", customARIA: {}, testing: { enabled: true, framework: "axe-core", rules: [], ignoreRules: [], timeout: 3e4, retries: 3, generateReports: true, reportFormat: "json", reportDir: "./accessibility-reports" }, focus: { enabled: true, focusTrap: true, focusIndicators: true, skipLinks: true, focusOrder: "tab", focusRestoration: true, focusDelegation: false }, screenReader: { enabled: true, announcements: true, liveRegions: true, ariaLabels: true, ariaDescriptions: true, ariaLandmarks: true, ariaRoles: true, ariaStates: true, ariaProperties: true }, ...config }; this.isInitialized = false; this.auditResults = /* @__PURE__ */ new Map(); this.violations = /* @__PURE__ */ new Map(); } /** * Initialize the accessibility system */ async initialize() { if (this.isInitialized) { console.warn("Accessibility system is already initialized"); return; } try { if (this.config.enableARIA) { await this.initializeARIA(); } if (this.config.enableTesting) { await this.initializeTesting(); } if (this.config.enableFocusManagement) { await this.initializeFocusManagement(); } if (this.config.enableScreenReader) { await this.initializeScreenReader(); } this.isInitialized = true; console.log("Accessibility system initialized successfully"); this.emit("initialized"); } catch (error) { console.error("Failed to initialize accessibility system:", error); this.emit("error", error); throw error; } } /** * Run accessibility audit * * @param url - URL to audit * @param options - Audit options * @returns Audit result */ async runAudit(url, options = {}) { if (!this.isInitialized) { throw new Error("Accessibility system not initialized"); } try { const auditId = `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const startTime = Date.now(); console.log(`Running accessibility audit for ${url}...`); const testResults = []; if (this.config.enableTesting) { const testingResults = await this.runAccessibilityTests(url, options); testResults.push(...testingResults); } const violations = []; for (const result of testResults) { violations.push(...result.violations); } const totalTests = testResults.length; const passedTests = testResults.filter((r) => r.status === "pass").length; const score = totalTests > 0 ? passedTests / totalTests * 100 : 0; const level = options.level || this.config.wcagLevel; const compliant = this.isCompliant(violations, level); const summary = this.createAuditSummary(violations, testResults); const audit = { id: auditId, timestamp: /* @__PURE__ */ new Date(), url, violations, passes: testResults.filter((r) => r.status === "pass"), inapplicable: testResults.filter((r) => r.status === "inapplicable"), score, level, compliant, summary, metadata: { config: this.config, options, duration: Date.now() - startTime } }; this.auditResults.set(auditId, audit); this.violations.set(auditId, violations); console.log(`Accessibility audit completed: ${score.toFixed(1)}% score`); this.emit("auditCompleted", audit); return audit; } catch (error) { console.error("Failed to run accessibility audit:", error); this.emit("error", error); throw error; } } /** * Generate ARIA attributes for an element * * @param element - Element to generate ARIA for * @param context - Element context * @returns Generated ARIA attributes */ generateARIA(element, context = {}) { if (!this.config.enableARIA) { return {}; } const ariaAttributes = {}; if (context.role) { ariaAttributes["role"] = context.role; } if (context.label) { ariaAttributes["aria-label"] = context.label; } if (context.description) { ariaAttributes["aria-describedby"] = context.description; } if (context.state) { for (const [key, value] of Object.entries(context.state)) { ariaAttributes[`aria-${key}`] = String(value); } } if (context.properties) { for (const [key, value] of Object.entries(context.properties)) { ariaAttributes[`aria-${key}`] = String(value); } } return ariaAttributes; } /** * Check color contrast * * @param foreground - Foreground color * @param background - Background color * @returns Color contrast information */ checkColorContrast(foreground, background) { if (!this.config.enableColorContrast) { return { ratio: 0, wcagAA: false, wcagAAA: false, largeText: false, uiComponent: false, suggestions: [] }; } const ratio = this.calculateContrastRatio(foreground, background); const wcagAA = ratio >= 4.5; const wcagAAA = ratio >= 7; const largeText = ratio >= 3; const uiComponent = ratio >= 3; const suggestions = []; if (!wcagAA) { suggestions.push("Increase contrast ratio to meet WCAG AA standards (4.5:1)"); } if (!wcagAAA) { suggestions.push("Increase contrast ratio to meet WCAG AAA standards (7:1)"); } return { ratio, wcagAA, wcagAAA, largeText, uiComponent, suggestions }; } /** * Generate semantic HTML * * @param content - Content to make semantic * @param options - Semantic options * @returns Semantic HTML */ generateSemanticHTML(content, options = {}) { if (!this.config.enableSemanticHTML) { return content; } let semanticContent = content; if (options.headingLevel) { semanticContent = this.addHeadingStructure(semanticContent, options.headingLevel); } if (options.listType) { semanticContent = this.addListStructure(semanticContent, options.listType); } if (options.tableHeaders) { semanticContent = this.addTableStructure(semanticContent, options.tableHeaders); } if (options.formLabels) { semanticContent = this.addFormStructure(semanticContent, options.formLabels); } return semanticContent; } /** * Get accessibility statistics * * @returns Statistics */ getStats() { const totalAudits = this.auditResults.size; let totalViolations = 0; let totalScore = 0; for (const audit of this.auditResults.values()) { totalViolations += audit.violations.length; totalScore += audit.score; } const averageScore = totalAudits > 0 ? totalScore / totalAudits : 0; const complianceRate = totalAudits > 0 ? Array.from(this.auditResults.values()).filter((a) => a.compliant).length / totalAudits : 0; return { totalAudits, totalViolations, averageScore, complianceRate, config: this.config }; } /** * Update accessibility configuration * * @param newConfig - New configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.emit("configUpdated", this.config); } /** * Get audit result by ID * * @param auditId - Audit ID * @returns Audit result or undefined */ getAuditResult(auditId) { return this.auditResults.get(auditId); } /** * Get all audit results * * @returns Array of audit results */ getAllAuditResults() { return Array.from(this.auditResults.values()); } /** * Clear audit results */ clearAuditResults() { this.auditResults.clear(); this.violations.clear(); this.emit("auditResultsCleared"); } /** * Initialize ARIA system */ async initializeARIA() { console.log("Initializing ARIA system..."); } /** * Initialize testing system */ async initializeTesting() { console.log("Initializing testing system..."); } /** * Initialize focus management */ async initializeFocusManagement() { console.log("Initializing focus management..."); } /** * Initialize screen reader support */ async initializeScreenReader() { console.log("Initializing screen reader support..."); } /** * Run accessibility tests * * @param url - URL to test * @param options - Test options * @returns Test results */ async runAccessibilityTests(url, options = {}) { const results = []; results.push({ id: "test_1", name: "Color Contrast", status: "pass", description: "Check color contrast ratios", impact: "serious", violations: [], passes: [], inapplicable: [], timestamp: /* @__PURE__ */ new Date(), duration: 1e3, url, metadata: {} }); return results; } /** * Check if violations are compliant with WCAG level * * @param violations - Violations to check * @param level - WCAG level * @returns True if compliant */ isCompliant(violations, level) { const criticalViolations = violations.filter((v) => v.impact === "critical"); const seriousViolations = violations.filter((v) => v.impact === "serious"); if (criticalViolations.length > 0) { return false; } switch (level) { case "A": return seriousViolations.length <= 5; case "AA": return seriousViolations.length <= 2; case "AAA": return seriousViolations.length === 0; default: return false; } } /** * Create audit summary * * @param violations - Violations * @param testResults - Test results * @returns Audit summary */ createAuditSummary(violations, testResults) { const criticalViolations = violations.filter((v) => v.impact === "critical").length; const seriousViolations = violations.filter((v) => v.impact === "serious").length; const moderateViolations = violations.filter((v) => v.impact === "moderate").length; const minorViolations = violations.filter((v) => v.impact === "minor").length; const totalPasses = testResults.filter((r) => r.status === "pass").length; const totalInapplicable = testResults.filter((r) => r.status === "inapplicable").length; return { totalViolations: violations.length, criticalViolations, seriousViolations, moderateViolations, minorViolations, totalPasses, totalInapplicable }; } /** * Calculate contrast ratio * * @param foreground - Foreground color * @param background - Background color * @returns Contrast ratio */ calculateContrastRatio(foreground, background) { return 4.5; } /** * Add heading structure * * @param content - Content * @param level - Heading level * @returns Content with heading structure */ addHeadingStructure(content, level) { return content; } /** * Add list structure * * @param content - Content * @param type - List type * @returns Content with list structure */ addListStructure(content, type) { return content; } /** * Add table structure * * @param content - Content * @param headers - Table headers * @returns Content with table structure */ addTableStructure(content, headers) { return content; } /** * Add form structure * * @param content - Content * @param labels - Form labels * @returns Content with form structure */ addFormStructure(content, labels) { return content; } }; var ARIAManager = class extends events.EventEmitter { config; roles; attributes; isInitialized; /** * Create a new ARIAManager instance * * @param config - Accessibility configuration */ constructor(config) { super(); this.config = config; this.roles = /* @__PURE__ */ new Map(); this.attributes = /* @__PURE__ */ new Map(); this.isInitialized = false; } /** * Initialize the ARIA manager */ async initialize() { if (this.isInitialized) { console.warn("ARIA manager is already initialized"); return; } try { await this.loadARIARoles(); await this.loadARIAAttributes(); this.isInitialized = true; console.log("ARIA manager initialized successfully"); this.emit("initialized"); } catch (error) { console.error("Failed to initialize ARIA manager:", error); this.emit("error", error); throw error; } } /** * Generate ARIA attributes for an element * * @param element - Element information * @param context - Element context * @returns Generated ARIA attributes */ generateARIA(element, context = {}) { const ariaAttributes = {}; if (context.role) { ariaAttributes["role"] = context.role; } else { const inferredRole = this.inferRole(element.tag, element.content, element.attributes); if (inferredRole) { ariaAttributes["role"] = inferredRole; } } if (context.label) { ariaAttributes["aria-label"] = context.label; } else { const inferredLabel = this.inferLabel(element.content, element.attributes); if (inferredLabel) { ariaAttributes["aria-label"] = inferredLabel; } } if (context.description) { ariaAttributes["aria-describedby"] = context.description; } if (context.state) { for (const [key, value] of Object.entries(context.state)) { ariaAttributes[`aria-${key}`] = String(value); } } if (context.properties) { for (const [key, value] of Object.entries(context.properties)) { ariaAttributes[`aria-${key}`] = String(value); } } if (context.landmark) { ariaAttributes["aria-label"] = ariaAttributes["aria-label"] || this.generateLandmarkLabel(element.tag); } if (context.interactive) { ariaAttributes["tabindex"] = "0"; if (!ariaAttributes["role"]) { ariaAttributes["role"] = "button"; } } return ariaAttributes; } /** * Validate ARIA attributes * * @param attributes - ARIA attributes to validate * @returns Validation result */ validateARIA(attributes) { const errors = []; const warnings = []; const suggestions = []; for (const [key, value] of Object.entries(attributes)) { if (key.startsWith("aria-")) { const attributeName = key.replace("aria-", ""); const attribute = this.attributes.get(attributeName); if (attribute) { if (attribute.allowedValues && !attribute.allowedValues.includes(value)) { errors.push(`Invalid value '${value}' for attribute '${key}'`); } if (attribute.required && !value) { errors.push(`Required attribute '${key}' is missing`); } } else { warnings.push(`Unknown ARIA attribute '${key}'`); } } } const role = attributes["role"]; if (role) { const roleInfo = this.roles.get(role); if (roleInfo) { for (const requiredAttr of roleInfo.requiredAttributes) { const attrKey = `aria-${requiredAttr}`; if (!attributes[attrKey]) { errors.push(`Required attribute '${attrKey}' for role '${role}' is missing`); } } for (const prohibitedAttr of roleInfo.prohibitedAttributes) { const attrKey = `aria-${prohibitedAttr}`; if (attributes[attrKey]) { warnings.push(`Prohibited attribute '${attrKey}' for role '${role}'`); } } } } return { isValid: errors.length === 0, errors, warnings, suggestions }; } /** * Get ARIA role information * * @param role - Role name * @returns Role information or undefined */ getRoleInfo(role) { return this.roles.get(role); } /** * Get ARIA attribute information * * @param attribute - Attribute name * @returns Attribute information or undefined */ getAttributeInfo(attribute) { return this.attributes.get(attribute); } /** * Get all available roles * * @returns Array of role names */ getAvailableRoles() { return Array.from(this.roles.keys()); } /** * Get all available attributes * * @returns Array of attribute names */ getAvailableAttributes() { return Array.from(this.attributes.keys()); } /** * Get roles by type * * @param type - Role type * @returns Array of roles */ getRolesByType(type) { return Array.from(this.roles.values()).filter((role) => role.type === type); } /** * Get ARIA manager statistics * * @returns Statistics */ getStats() { const rolesByType = {}; for (const role of this.roles.values()) { rolesByType[role.type] = (rolesByType[role.type] || 0) + 1; } return { totalRoles: this.roles.size, totalAttributes: this.attributes.size, rolesByType }; } /** * Infer role from element * * @param tag - Element tag * @param content - Element content * @param attributes - Element attributes * @returns Inferred role or undefined */ inferRole(tag, content, attributes) { if (attributes.role) { return attributes.role; } switch (tag.toLowerCase()) { case "button": return "button"; case "a": return "link"; case "input": const type = attributes.type || "text"; switch (type) { case "checkbox": return "checkbox"; case "radio": return "radio"; case "search": return "searchbox"; default: return "textbox"; } case "select": return "combobox"; case "textarea": return "textbox"; case "nav": return "navigation"; case "main": return "main"; case "aside": return "complementary"; case "header": return "banner"; case "footer": return "contentinfo"; case "article": return "article"; case "section": return "region"; case "form": return "form"; case "table": return "table"; case "ul": case "ol": return "list"; case "li": return "listitem"; case "h1": case "h2": case "h3": case "h4": case "h5": case "h6": return "heading"; case "img": return "img"; case "progress": return "progressbar"; case "meter": return "meter"; default: return void 0; } } /** * Infer label from content and attributes * * @param content - Element content * @param attributes - Element attributes * @returns Inferred label or undefined */ inferLabel(content, attributes) { if (attributes["aria-label"]) { return attributes["aria-label"]; } if (attributes.title) { return attributes.title; } if (attributes.alt) { return attributes.alt; } if (attributes.placeholder) { return attributes.placeholder; } const trimmedContent = content.trim(); if (trimmedContent && trimmedContent.length > 0 && trimmedContent.length < 100) { return trimmedContent; } return void 0; } /** * Generate landmark label * * @param tag - Element tag * @returns Landmark label */ generateLandmarkLabel(tag) { switch (tag.toLowerCase()) { case "nav": return "Navigation"; case "main": return "Main content"; case "aside": return "Complementary content"; case "header": return "Header"; case "footer": return "Footer"; case "article": return "Article"; case "section": return "Section"; case "form": return "Form"; default: return "Landmark"; } } /** * Load ARIA roles */ async loadARIARoles() { const commonRoles = { "button": { name: "button", type: "widget", requiredAttributes: [], supportedAttributes: ["aria-expanded", "aria-pressed", "aria-haspopup"], requiredOwnedElements: [], requiredContextRoles: [], prohibitedAttributes: [], description: "An input that allows for user-triggered actions", examples: ["<button>Click me</button>", '<div role="button">Click me</div>'] }, "link": { name: "link", type: "widget", requiredAttributes: [], supportedAttributes: ["aria-expanded", "aria-haspopup"], requiredOwnedElements: [], requiredContextRoles: [], prohibitedAttributes: [], description: "An interactive reference to an internal or external resource", examples: ['<a href="/">Home</a>', '<div role="link">Home</div>'] }, "navigation": { name: "navigation", type: "landmark", requiredAttributes: [], supportedAttributes: ["aria-label", "aria-labelledby"], requiredOwnedElements: [], requiredContextRoles: [], prohibitedAttributes: [], description: "A landmark containing navigation links", examples: ["<nav>Navigation</nav>", '<div role="navigation">Navigation</div>'] }, "main": { name: "main", type: "landmark", requiredAttributes: [], supportedAttributes: ["aria-label", "aria-labelledby"], requiredOwnedElements: [], requiredContextRoles: [], prohibitedAttributes: [], description: "The main content of a document", examples: ["<main>Main content</main>", '<div role="main">Main content</div>'] }, "banner": { name: "banner", type: "landmark", requiredAttributes: [], supportedAttributes: ["aria-label", "aria-labelledby"], requiredOwnedElements: [], requiredContextRoles: [], prohibitedAttributes: [], description: "A landmark that contains site-oriented content", examples: ["<header>Header</header>", '<div role="banner">Header</div>'] }, "contentinfo": { name: "contentinfo", type: "landmark", requiredAttributes: [], supportedAttributes: ["aria-label", "aria-labelledby"], requiredOwnedElements: [], requiredContextRoles: [], prohibitedAttributes: [], description: "A landmark containing information about the parent document", examples: ["<footer>Footer</footer>", '<div role="contentinfo">Footer</div>'] } }; for (const [name, role] of Object.entries(commonRoles)) { this.roles.set(name, role); } } /** * Load ARIA attributes */ async loadARIAAttributes() { const commonAttributes = { "label": { name: "label", value: "", type: "string", required: false, description: "Defines a string value that labels the current element" }, "describedby": { name: "describedby", value: "", type: "idrefs", required: false, description: "Identifies the element that describes the current element" }, "expanded": { name: "expanded", value: "", type: "boolean", required: false, allowedValues: ["true", "false", "undefined"], description: "Indicates whether the element is expanded or collapsed" }, "pressed": { name: "pressed", value: "", type: "boolean", required: false, allowedValues: ["true", "false", "mixed", "undefined"], description: "Indicates the current pressed state of toggle buttons" }, "haspopup": { name: "haspopup", value: "", type: "boolean", required: false, allowedValues: ["true", "false", "menu", "listbox", "tree", "grid", "dialog"], description: "Indicates that the element has a popup context menu or sub-level menu" } }; for (const [name, attribute] of Object.entries(commonAttributes)) { this.attributes.set(name, attribute); } } }; var FocusManager = class extends events.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..."); } }; var ScreenReaderManager = class extends events.EventEmitter { config; announcements; liveRegions; isInitialized; /** * Create a new ScreenReaderManager instance * * @param config - Screen reader configuration */ constructor(config) { super(); this.config = config; this.announcements = /* @__PURE__ */ new Map(); this.liveRegions = /* @__PURE__ */ new Map(); this.isInitialized = false; } /** * Initialize the screen reader manager */ async initialize() { if (this.isInitialized) { console.warn("Screen reader manager is already initialized"); return; } try { await this.initializeScreenReader(); this.isInitialized = true; console.log("Screen reader manager initialized successfully"); this.emit("initialized"); } catch (error) { console.error("Failed to initialize screen reader manager:", error); this.emit("error", error); throw error; } } /** * Announce message to screen readers * * @param message - Message to announce * @param options - Announcement options * @returns Announcement ID */ announce(message, options = {}) { const announcementId = `announcement_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const announcement = { id: announcementId, message, priority: options.priority || "polite", type: options.type || "status", timestamp: /* @__PURE__ */ new Date() }; if (options.duration !== void 0) { announcement.duration = options.duration; } if (options.element) { announcement.element = options.element; } if (options.context) { announcement.context = options.context; } this.announcements.set(announcementId, announcement); this.emit("announcementCreated", announcement); return announcementId; } /** * Create live region * * @param regionId - Region ID * @param element - Element selector * @param options - Region options * @returns Live region */ createLiveRegion(regionId, element, options = {}) { const region = { id: regionId, type: options.type || "status", priority: options.priority || "polite", element, content: "", atomic: options.atomic || false, relevant: options.relevant || "additions", busy: options.busy || false }; if (options.expanded !== void 0) { region.expanded = options.expanded; } if (options.controls) { region.controls = options.controls; } if (options.describedBy) { region.describedBy = options.describedBy; } if (options.label) { region.label = options.label; } this.liveRegions.set(regionId, region); this.emit("liveRegionCreated", region); return region; } /** * Update live region content * * @param regionId - Region ID * @param content - New content * @param options - Update options */ updateLiveRegion(regionId, content, options = {}) { const region = this.liveRegions.get(regionId); if (region) { region.content = content; if (options.busy !== void 0) { region.busy = options.busy; } if (options.expanded !== void 0) { region.expanded = options.expanded; } this.liveRegions.set(regionId, region); this.emit("liveRegionUpdated", region); } } /** * Remove live region * * @param regionId - Region ID */ removeLiveRegion(regionId) { const region = this.liveRegions.get(regionId); if (region) { this.liveRegions.delete(regionId); this.emit("liveRegionRemoved", region); } } /** * Get live region by ID * * @param regionId - Region ID * @returns Live region or undefined */ getLiveRegion(regionId) { return this.liveRegions.get(regionId); } /** * Get all live regions * * @returns Array of live regions */ getAllLiveRegions() { return Array.from(this.liveRegions.values()); } /** * Get announcement by ID * * @param announcementId - Announcement ID * @returns Announcement or undefined */ getAnnouncement(announcementId) { return this.announcements.get(announcementId); } /** * Get all announcements * * @returns Array of announcements */ getAllAnnouncements() { return Array.from(this.announcements.values()); } /** * Clear announcements */ clearAnnouncements() { this.announcements.clear(); this.emit("announcementsCleared"); } /** * Get screen reader manager statistics * * @returns Statistics */ getStats() { const announcementsByType = {}; const liveRegionsByType = {}; for (const announcement of this.announcements.values()) { announcementsByType[announcement.type] = (announcementsByType[announcement.type] || 0) + 1; } for (const region of this.liveRegions.values()) { liveRegionsByType[region.type] = (liveRegionsByType[region.type] || 0) + 1; } return { totalAnnouncements: this.announcements.size, totalLiveRegions: this.liveRegions.size, announcementsByType, liveRegionsByType }; } /** * Initialize screen reader support */ async initializeScreenReader() { console.log("Initializing screen reader support..."); if (this.config.announcements) { this.setupAnnouncements(); } if (this.config.liveRegions) { this.setupLiveRegions(); } if (this.config.ariaLabels || this.config.ariaDescriptions || this.config.ariaLandmarks) { this.setupARIASupport(); } } /** * Setup announcements */ setupAnnouncements() { console.log("Setting up announcements..."); } /** * Setup live regions */ setupLiveRegions() { console.log("Setting up live regions..."); } /** * Setup ARIA support */ setupARIASupport() { console.log("Setting up ARIA support..."); } }; var TestingManager = class extends events.EventEmitter { config; testResults; isInitialized; /** * Create a new TestingManager instance * * @param config - Testing configuration */ constructor(config) { super(); this.config = config; this.testResults = /* @__PURE__ */ new Map(); this.isInitialized = false; } /** * Initialize the testing manager */ async initialize() { if (this.isInitialized) { console.warn("Testing manager is already initialized"); return; } try { await this.initializeTestingFramework(); this.isInitialized = true; console.log("Testing manager initialized successfully"); this.emit("initialized"); } catch (error) { console.error("Failed to initialize testing manager:", error); this.emit("error", error); throw error; } } /** * Run accessibility tests * * @param url - URL to test * @param options - Test options * @returns Test results */ async runTests(url, options = {}) { if (!this.isInitialized) { throw new Error("Testing manager not initialized"); } const results = []; const rules = options.rules || this.config.rules; const ignoreRules = options.ignoreRules || this.config.ignoreRules; const timeout = options.timeout || this.config.timeout; options.retries || this.config.retries; try { console.log(`Running accessibility tests for ${url}...`); switch (this.config.framework) { case "axe-core": results.push(...await this.runAxeCoreTests(url, rules, ignoreRules, timeout)); break; case "puppeteer": results.push(...await this.runPuppeteerTests(url, rules, ignoreRules, timeout)); break; case "jsdom": results.push(...await this.runJSDOMTests(url, rules, ignoreRules, timeout)); break; default: throw new Error(`Unsupported testing framework: ${this.config.framework}`); } for (const result of results) { this.testResults.set(result.id, result); } console.log(`Accessibility tests completed: ${results.length} tests run`); this.emit("testsCompleted", results); return results; } catch (error) { console.error("Failed to run accessibility tests:", error); this.emit("error", error); throw error; } } /** * Run specific accessibility test * * @param testName - Test name * @param url - URL to test * @param options - Test options * @returns Test result */ async runTest(testName, url, options = {}) { const testId = `test_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const startTime = Date.now(); try { console.log(`Running test: ${testName}`); let result; let attempts = 0; const maxAttempts = options.retries || this.config.retries; do { attempts++; result = await this.executeTest(testName, url, options.timeout || this.config.timeout); if (result.status === "pass" || attempts >= maxAttempts) { break; } console.log(`Test failed, retrying (${attempts}/${maxAttempts})...`); await this.delay(1e3); } while (attempts < maxAttempts); result.id = testId; result.duration = Date.now() - startTime; result.timestamp = /* @__PURE__ */ new Date(); this.testResults.set(testId, result); this.emit("testCompleted", result); return result; } catch (error) { console.error(`Test '${testName}' failed:`, error); const errorResult = { id: testId, name: testName, status: "fail", description: `Test failed: ${error instanceof Error ? error.message : String(error)}`, impact: "serious", violations: [], passes: [], inapplicable: [], timestamp: /* @__PURE__ */ new Date(), duration: Date.now() - startTime, url, metadata: { error: error instanceof Error ? error.message : String(error) } }; this.testResults.set(testId, errorResult); this.emit("testFailed", errorResult); throw error; } } /** * Generate accessibility report * * @param results - Test results * @param options - Report options * @returns Report content */ generateReport(results, options = {}) { const format = options.format || this.config.reportFormat; const includePasses = options.includePasses !== false; const includeViolations = options.includeViolations !== false; const includeSuggestions = options.includeSuggestions !== false; switch (format) { case "json": return this.generateJSONReport(results, { includePasses, includeViolations, includeSuggestions }); case "html": return this.generateHTMLReport(results, { includePasses, includeViolations, includeSuggestions }); case "csv": return this.generateCSVReport(results, { includePasses, includeViolations, includeSuggestions }); default: throw new Error(`Unsupported report format: ${format}`); } } /** * Get test result by ID * * @param testId - Test ID * @returns Test result or undefined */ getTestResult(testId) { return this.testResults.get(testId); } /** * Get all test results * * @returns Array of test results */ getAllTestResults() { return Array.from(this.testResults.values()); } /** * Clear test results */ clearTestResults() { this.testResults.clear(); this.emit("testResultsCleared"); } /** * Get testing statistics * * @returns Statistics */ getStats() { const totalTests = this.testResults.size; const passedTests = Array.from(this.testResults.values()).filter((r) => r.status === "pass").length; const failedTests = Array.from(this.testResults.values()).filter((r) => r.status === "fail").length; const totalDuration = Array.from(this.testResults.values()).reduce((sum, r) => sum + r.duration, 0); const averageDuration = totalTests > 0 ? totalDuration / totalTests : 0; return { totalTests, passedTests, failedTests, averageDuration, framework: this.config.framework }; } /** * Initialize testing framework */ async initializeTestingFramework() { console.log(`Initializing ${this.config.framework} testing framework...`); switch (this.config.framework) { case "axe-core": break; case "puppeteer": break; case "jsdom": break; default: throw new Error(`Unsupported testing framework: ${this.config.framework}`); } } /** * Run axe-core tests * * @param url - URL to test * @param rules - Rules to test * @param ignoreRules - Rules to ignore * @param timeout - Test timeout * @returns Test results */ async runAxeCoreTests(url, rules, ignoreRules, timeout) { const results = []; results.push({ id: `axe_${Date.now()}`, name: "Color Contrast", status: "pass", description: "Check color contrast ratios", impact: "serious", violations: [], passes: [], inapplicable: [], timestamp: /* @__PURE__ */ new Date(), duration: 1e3, url, metadata: { framework: "axe-core" } }); return results; } /** * Run puppeteer tests * * @param url - URL to test * @param rules - Rules to test * @param ignoreRules - Rules to ignore * @param timeout - Test timeout * @returns Test results */ async runPuppeteerTests(url, rules, ignoreRules, timeout) { const results = []; results.push({ id: `puppeteer_${Date.now()}`, name: "Keyboard Navigation", status: "pass", description: "Check keyboard navigation functionality", impact: "serious", violations: [], passes: [], inapplicable: [], timestamp: /* @__PURE__ */ new Date(), duration: 2e3, url, metadata: { framework: "puppeteer" } }); return results; } /** * Run JSDOM tests * * @param url - URL to test * @param rules - Rules to test * @param ignoreRules - Rules to ignore * @param timeout - Test timeout * @returns Test results */ async runJSDOMTests(url, rules, ignoreRules, timeout) { const results = []; results.push({ id: `jsdom_${Date.now()}`, name: "Semantic HTML", status: "pass", description: "Check semantic HTML structure", impact: "moderate", violations: [], passes: [], inapplicable: [], timestamp: /* @__PURE__ */ new Date(), duration: 500, url, metadata: { framework: "jsdom" } }); return results; } /** *