Spaces:
Sleeping
Sleeping
File size: 6,409 Bytes
d97b8f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | /**
* Accessibility utilities for the asset management module
*/
/**
* Announces content to screen readers
* @param message - The message to announce
* @param priority - The priority level (polite or assertive)
*/
export const announceToScreenReader = (message: string, priority: 'polite' | 'assertive' = 'polite') => {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', priority);
announcement.setAttribute('aria-atomic', 'true');
announcement.setAttribute('class', 'sr-only');
announcement.textContent = message;
document.body.appendChild(announcement);
// Remove the announcement after a short delay
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
};
/**
* Manages focus for modal dialogs and overlays
*/
export class FocusManager {
private previousActiveElement: Element | null = null;
private focusableElements: NodeListOf<HTMLElement> | null = null;
/**
* Traps focus within a container element
* @param container - The container element to trap focus within
*/
trapFocus(container: HTMLElement) {
this.previousActiveElement = document.activeElement;
// Get all focusable elements within the container
this.focusableElements = container.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
);
if (this.focusableElements.length > 0) {
// Focus the first focusable element
this.focusableElements[0].focus();
}
// Add event listener for tab key
container.addEventListener('keydown', this.handleTabKey);
}
/**
* Handles tab key navigation within the focus trap
*/
private handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab' || !this.focusableElements) return;
const firstElement = this.focusableElements[0];
const lastElement = this.focusableElements[this.focusableElements.length - 1];
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
// Tab
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
/**
* Releases the focus trap and restores previous focus
*/
releaseFocus(container: HTMLElement) {
container.removeEventListener('keydown', this.handleTabKey);
if (this.previousActiveElement && this.previousActiveElement instanceof HTMLElement) {
this.previousActiveElement.focus();
}
this.previousActiveElement = null;
this.focusableElements = null;
}
}
/**
* Checks if an element is visible to screen readers
* @param element - The element to check
* @returns True if the element is accessible to screen readers
*/
export const isAccessibleToScreenReaders = (element: HTMLElement): boolean => {
// Check if element is hidden
if (element.hidden || element.style.display === 'none' || element.style.visibility === 'hidden') {
return false;
}
// Check for aria-hidden
if (element.getAttribute('aria-hidden') === 'true') {
return false;
}
// Check if element has accessible text
const accessibleName = getAccessibleName(element);
return accessibleName.length > 0;
};
/**
* Gets the accessible name of an element
* @param element - The element to get the accessible name for
* @returns The accessible name
*/
export const getAccessibleName = (element: HTMLElement): string => {
// Check aria-label
const ariaLabel = element.getAttribute('aria-label');
if (ariaLabel) return ariaLabel.trim();
// Check aria-labelledby
const ariaLabelledBy = element.getAttribute('aria-labelledby');
if (ariaLabelledBy) {
const labelElement = document.getElementById(ariaLabelledBy);
if (labelElement) return labelElement.textContent?.trim() || '';
}
// Check associated label
if (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA') {
const id = element.getAttribute('id');
if (id) {
const label = document.querySelector(`label[for="${id}"]`);
if (label) return label.textContent?.trim() || '';
}
}
// Check title attribute
const title = element.getAttribute('title');
if (title) return title.trim();
// Check text content
return element.textContent?.trim() || '';
};
/**
* Validates keyboard navigation for a container
* @param container - The container to validate
* @returns Array of accessibility issues found
*/
export const validateKeyboardNavigation = (container: HTMLElement): string[] => {
const issues: string[] = [];
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
focusableElements.forEach((element, index) => {
const htmlElement = element as HTMLElement;
// Check if element is keyboard accessible
const tabIndex = htmlElement.getAttribute('tabindex');
if (tabIndex === '-1' && !htmlElement.disabled) {
issues.push(`Element at index ${index} (${htmlElement.tagName}) is not keyboard accessible`);
}
// Check if interactive elements have accessible names
if (['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA'].includes(htmlElement.tagName)) {
const accessibleName = getAccessibleName(htmlElement);
if (!accessibleName) {
issues.push(`Interactive element at index ${index} (${htmlElement.tagName}) lacks accessible name`);
}
}
});
return issues;
};
/**
* Validates color contrast (basic check)
* @param element - The element to check
* @returns True if contrast appears adequate
*/
export const hasAdequateContrast = (element: HTMLElement): boolean => {
const styles = window.getComputedStyle(element);
const backgroundColor = styles.backgroundColor;
const color = styles.color;
// This is a simplified check - in a real implementation,
// you would calculate the actual contrast ratio
return backgroundColor !== color && backgroundColor !== 'transparent';
}; |