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';
};