@ordojs/accessibility
Version:
Comprehensive accessibility system for OrdoJS with ARIA generation, automated testing, and screen reader support
457 lines (454 loc) • 12.8 kB
JavaScript
'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