Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
| /** | |
| * ============================================ | |
| * ACCESSIBILITY ENHANCEMENTS | |
| * Keyboard navigation, focus management, announcements | |
| * ============================================ | |
| */ | |
| class AccessibilityManager { | |
| constructor() { | |
| this.init(); | |
| } | |
| init() { | |
| this.detectInputMethod(); | |
| this.setupKeyboardNavigation(); | |
| this.setupAnnouncements(); | |
| this.setupFocusManagement(); | |
| console.log('[A11y] Accessibility manager initialized'); | |
| } | |
| /** | |
| * Detect if user is using keyboard or mouse | |
| */ | |
| detectInputMethod() { | |
| // Track mouse usage | |
| document.addEventListener('mousedown', () => { | |
| document.body.classList.add('using-mouse'); | |
| }); | |
| // Track keyboard usage | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Tab') { | |
| document.body.classList.remove('using-mouse'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Setup keyboard navigation shortcuts | |
| */ | |
| setupKeyboardNavigation() { | |
| document.addEventListener('keydown', (e) => { | |
| // Ctrl/Cmd + K: Focus search | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| e.preventDefault(); | |
| const searchInput = document.querySelector('[role="searchbox"], input[type="search"]'); | |
| if (searchInput) searchInput.focus(); | |
| } | |
| // Escape: Close modals/dropdowns | |
| if (e.key === 'Escape') { | |
| this.closeAllModals(); | |
| this.closeAllDropdowns(); | |
| } | |
| // Arrow keys for tab navigation | |
| if (e.target.getAttribute('role') === 'tab') { | |
| this.handleTabNavigation(e); | |
| } | |
| }); | |
| } | |
| /** | |
| * Handle tab navigation with arrow keys | |
| */ | |
| handleTabNavigation(e) { | |
| const tabs = Array.from(document.querySelectorAll('[role="tab"]')); | |
| const currentIndex = tabs.indexOf(e.target); | |
| let nextIndex; | |
| if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { | |
| nextIndex = (currentIndex + 1) % tabs.length; | |
| } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { | |
| nextIndex = (currentIndex - 1 + tabs.length) % tabs.length; | |
| } | |
| if (nextIndex !== undefined) { | |
| e.preventDefault(); | |
| tabs[nextIndex].focus(); | |
| tabs[nextIndex].click(); | |
| } | |
| } | |
| /** | |
| * Setup screen reader announcements | |
| */ | |
| setupAnnouncements() { | |
| // Create announcement regions if they don't exist | |
| if (!document.getElementById('aria-live-polite')) { | |
| const polite = document.createElement('div'); | |
| polite.id = 'aria-live-polite'; | |
| polite.setAttribute('aria-live', 'polite'); | |
| polite.setAttribute('aria-atomic', 'true'); | |
| polite.className = 'sr-only'; | |
| document.body.appendChild(polite); | |
| } | |
| if (!document.getElementById('aria-live-assertive')) { | |
| const assertive = document.createElement('div'); | |
| assertive.id = 'aria-live-assertive'; | |
| assertive.setAttribute('aria-live', 'assertive'); | |
| assertive.setAttribute('aria-atomic', 'true'); | |
| assertive.className = 'sr-only'; | |
| document.body.appendChild(assertive); | |
| } | |
| } | |
| /** | |
| * Announce message to screen readers | |
| */ | |
| announce(message, priority = 'polite') { | |
| const region = document.getElementById(`aria-live-${priority}`); | |
| if (!region) return; | |
| // Clear and set new message | |
| region.textContent = ''; | |
| setTimeout(() => { | |
| region.textContent = message; | |
| }, 100); | |
| } | |
| /** | |
| * Setup focus management | |
| */ | |
| setupFocusManagement() { | |
| // Trap focus in modals | |
| document.addEventListener('focusin', (e) => { | |
| const modal = document.querySelector('.modal-backdrop'); | |
| if (!modal) return; | |
| const focusableElements = modal.querySelectorAll( | |
| 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' | |
| ); | |
| if (focusableElements.length === 0) return; | |
| const firstElement = focusableElements[0]; | |
| const lastElement = focusableElements[focusableElements.length - 1]; | |
| if (!modal.contains(e.target)) { | |
| firstElement.focus(); | |
| } | |
| }); | |
| // Handle Tab key in modals | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key !== 'Tab') return; | |
| const modal = document.querySelector('.modal-backdrop'); | |
| if (!modal) return; | |
| const focusableElements = modal.querySelectorAll( | |
| 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' | |
| ); | |
| if (focusableElements.length === 0) return; | |
| const firstElement = focusableElements[0]; | |
| const lastElement = focusableElements[focusableElements.length - 1]; | |
| if (e.shiftKey) { | |
| if (document.activeElement === firstElement) { | |
| e.preventDefault(); | |
| lastElement.focus(); | |
| } | |
| } else { | |
| if (document.activeElement === lastElement) { | |
| e.preventDefault(); | |
| firstElement.focus(); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Close all modals | |
| */ | |
| closeAllModals() { | |
| document.querySelectorAll('.modal-backdrop').forEach(modal => { | |
| modal.remove(); | |
| }); | |
| } | |
| /** | |
| * Close all dropdowns | |
| */ | |
| closeAllDropdowns() { | |
| document.querySelectorAll('[aria-expanded="true"]').forEach(element => { | |
| element.setAttribute('aria-expanded', 'false'); | |
| }); | |
| } | |
| /** | |
| * Set page title (announces to screen readers) | |
| */ | |
| setPageTitle(title) { | |
| document.title = title; | |
| this.announce(`Page: ${title}`); | |
| } | |
| /** | |
| * Add skip link | |
| */ | |
| addSkipLink() { | |
| const skipLink = document.createElement('a'); | |
| skipLink.href = '#main-content'; | |
| skipLink.className = 'skip-link'; | |
| skipLink.textContent = 'Skip to main content'; | |
| document.body.insertBefore(skipLink, document.body.firstChild); | |
| // Add id to main content if it doesn't exist | |
| const mainContent = document.querySelector('.main-content, main'); | |
| if (mainContent && !mainContent.id) { | |
| mainContent.id = 'main-content'; | |
| } | |
| } | |
| /** | |
| * Mark element as loading | |
| */ | |
| markAsLoading(element, label = 'Loading') { | |
| element.setAttribute('aria-busy', 'true'); | |
| element.setAttribute('aria-label', label); | |
| } | |
| /** | |
| * Unmark element as loading | |
| */ | |
| unmarkAsLoading(element) { | |
| element.setAttribute('aria-busy', 'false'); | |
| element.removeAttribute('aria-label'); | |
| } | |
| } | |
| // Export singleton | |
| window.a11y = new AccessibilityManager(); | |
| // Utility functions | |
| window.announce = (message, priority) => window.a11y.announce(message, priority); | |