UNPKG

@ordojs/accessibility

Version:

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

457 lines (454 loc) 12.8 kB
'use strict'; var events = require('events'); // src/aria/index.ts 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); } } }; exports.ARIAManager = ARIAManager; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map