Spaces:
Sleeping
Sleeping
| /** | |
| * 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'; | |
| }; |