diff --git a/widget.js b/widget.js index 13c853f..8d98075 100644 --- a/widget.js +++ b/widget.js @@ -1,6 +1,6 @@ /* =========================================== - ACCESSIBILITY WIDGET + ACCESSIBILITY WIDGET - SHADOW DOM VERSION A comprehensive web accessibility tool =========================================== */ @@ -168,14 +168,24 @@ const WIDGET_CONFIG = mergeConfigs(DEFAULT_WIDGET_CONFIG, window.ACCESSIBILITY_W // STYLES & VISUAL ASSETS // =========================================== -// Generate styles using configuration variables -const styles = ` +// Widget styles (will go inside Shadow DOM - NOT affected by page styles or accessibility features) +const widgetStyles = ` + :host { + all: initial; + font-family: ${WIDGET_CONFIG.typography.fontFamily}; + } + + * { + box-sizing: border-box; + } + #snn-accessibility-fixed-button { position: fixed !important; ${WIDGET_CONFIG.widgetPosition.side}: ${WIDGET_CONFIG.widgetPosition[WIDGET_CONFIG.widgetPosition.side]} !important; bottom: ${WIDGET_CONFIG.widgetPosition.bottom} !important; z-index: 9999; } + #snn-accessibility-button { background: ${WIDGET_CONFIG.colors.primary}; border: none; @@ -189,19 +199,23 @@ const styles = ` justify-content: center; align-items: center; } + #snn-accessibility-button:hover { transform: scale(${WIDGET_CONFIG.animation.hoverScale}); } + #snn-accessibility-button:focus { outline: 2px solid ${WIDGET_CONFIG.colors.textLight}; outline-offset: 2px; } + #snn-accessibility-button svg { width: ${WIDGET_CONFIG.button.iconSize}; height: ${WIDGET_CONFIG.button.iconSize}; fill: ${WIDGET_CONFIG.colors.textLight}; pointer-events: none; } + #snn-accessibility-menu { position: fixed; top: 0; @@ -216,6 +230,7 @@ const styles = ` z-index: 999999; scrollbar-width: thin; } + .snn-accessibility-option { font-size: ${WIDGET_CONFIG.menu.fontSize}; display: flex; @@ -230,23 +245,38 @@ const styles = ` transition: background-color ${WIDGET_CONFIG.animation.transition}, border-color ${WIDGET_CONFIG.animation.transition}; line-height: ${WIDGET_CONFIG.typography.lineHeight} !important; } + .snn-accessibility-option:hover { border-color: ${WIDGET_CONFIG.colors.primary}; } + .snn-accessibility-option.active { border-color: ${WIDGET_CONFIG.colors.primary}; } + + .snn-accessibility-option:disabled { + opacity: 0.5; + cursor: not-allowed; + } + .snn-icon { margin-right: 12px; width: ${WIDGET_CONFIG.button.iconSize}; height: ${WIDGET_CONFIG.button.iconSize}; fill: ${WIDGET_CONFIG.colors.primary}; + flex-shrink: 0; } + .snn-icon svg { width: 100%; height: 100%; fill: currentColor; } + + .snn-button-text { + flex: 1; + } + .snn-close { background: none; border: none; @@ -260,6 +290,7 @@ const styles = ` height: ${WIDGET_CONFIG.menu.closeButtonSize}; position: relative; } + .snn-close::before { content: '×'; position: absolute; @@ -269,13 +300,16 @@ const styles = ` font-size: ${WIDGET_CONFIG.menu.closeButtonSize}; line-height: 1; } + .snn-close:focus { outline: solid 2px ${WIDGET_CONFIG.colors.textLight}; } + .snn-close:hover { color: ${WIDGET_CONFIG.colors.text}; - background:${WIDGET_CONFIG.colors.secondary}; + background: ${WIDGET_CONFIG.colors.secondary}; } + .snn-header { display: flex; align-items: center; @@ -323,7 +357,6 @@ const styles = ` margin-bottom: 20px; } - .snn-title { margin: 0; font-size: ${WIDGET_CONFIG.menu.titleFontSize}; @@ -332,44 +365,41 @@ const styles = ` margin-left: 5px; font-weight: ${WIDGET_CONFIG.typography.titleFontWeight}; } - /* Accessibility feature styles */ +`; + +// Page accessibility styles (will go in main document - these affect the page, NOT the widget) +const pageStyles = ` + /* High Contrast Modes */ .snn-high-contrast-medium { filter: contrast(1.3) !important; } - .snn-high-contrast-medium *{ + .snn-high-contrast-medium * { filter: contrast(1.3) !important; } - .snn-high-contrast-medium #snn-accessibility-menu{ - filter: contrast(0.8) !important; - } .snn-high-contrast-high { background-color: #000 !important; color: #fff !important; filter: contrast(1.5) !important; } - .snn-high-contrast-high *{ + .snn-high-contrast-high * { background-color: #000 !important; color: #fff !important; filter: contrast(1.5) !important; } - .snn-high-contrast-high #snn-accessibility-menu{ - filter: contrast(0.7) !important; - } .snn-high-contrast-ultra { background-color: #000 !important; color: #ffff00 !important; filter: contrast(2.0) !important; } - .snn-high-contrast-ultra *{ + .snn-high-contrast-ultra * { background-color: #000 !important; color: #ffff00 !important; filter: contrast(2.0) !important; } - .snn-high-contrast-ultra #snn-accessibility-menu{ - filter: contrast(0.6) !important; - } + + /* Text Size */ .snn-bigger-text-medium * { font-size: 20px !important; } @@ -379,32 +409,44 @@ const styles = ` .snn-bigger-text-xlarge * { font-size: 28px !important; } - .snn-text-spacing *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) { + + /* Text Spacing */ + .snn-text-spacing * { letter-spacing: 0.2em !important; word-spacing: 0.3em !important; } + + /* Pause Animations */ .snn-pause-animations * { animation: none !important; transition: none !important; } + + /* Dyslexia Font */ .snn-dyslexia-font { - font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', Brush Script MT, fantasy !important; + font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', 'Brush Script MT', fantasy !important; } .snn-dyslexia-font * { - font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', Brush Script MT, fantasy !important; + font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', 'Brush Script MT', fantasy !important; } - .snn-line-height *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) { + + /* Line Height */ + .snn-line-height * { line-height: 2.5 !important; } - .snn-text-align-left *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) { + + /* Text Alignment */ + .snn-text-align-left * { text-align: left !important; } - .snn-text-align-center *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) { + .snn-text-align-center * { text-align: center !important; } - .snn-text-align-right *:not(#snn-accessibility-menu *, #snn-accessibility-fixed-button *, #snn-accessibility-button *, .snn-accessibility-option *) { + .snn-text-align-right * { text-align: right !important; } + + /* Bigger Cursor */ .snn-bigger-cursor { cursor: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNzIiIHZpZXdCb3g9IjAgMCA0OCA3MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNCAyVjcwTDIwIDU0SDM2TDQgMloiIGZpbGw9IiMwMDAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSI0Ii8+PC9zdmc+'), auto !important; } @@ -462,7 +504,6 @@ const styles = ` // SVG ICONS // =========================================== -// SVG icons const icons = { buttonsvg: ``, highContrast: ``, @@ -483,16 +524,19 @@ const icons = { }; // =========================================== -// CORE UTILITY FUNCTIONS +// SHADOW DOM SETUP // =========================================== -// Inject styles and SVG filters into the document -function injectStyles() { +let shadowRoot = null; + +// Inject styles into the page (NOT the widget) +function injectPageStyles() { const styleSheet = document.createElement('style'); - styleSheet.innerText = styles; + styleSheet.innerText = pageStyles; + styleSheet.id = 'snn-accessibility-page-styles'; document.head.appendChild(styleSheet); - // Add SVG color blindness filters + // Add SVG color blindness filters to main document const svgFilters = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svgFilters.style.position = 'absolute'; svgFilters.style.width = '0'; @@ -513,8 +557,25 @@ function injectStyles() { document.body.appendChild(svgFilters); } +// Create shadow DOM container +function createShadowContainer() { + const container = document.createElement('div'); + container.id = 'snn-accessibility-widget-container'; + document.body.appendChild(container); + + // Create shadow root + shadowRoot = container.attachShadow({ mode: 'open' }); + + // Add widget styles to shadow DOM + const styleElement = document.createElement('style'); + styleElement.textContent = widgetStyles; + shadowRoot.appendChild(styleElement); + + return shadowRoot; +} + // =========================================== -// PERFORMANCE OPTIMIZATION +// CORE UTILITY FUNCTIONS // =========================================== // Cache for DOM elements to improve performance @@ -668,7 +729,7 @@ function createAccessibilityButton() { }); buttonContainer.appendChild(button); - document.body.appendChild(buttonContainer); + shadowRoot.appendChild(buttonContainer); } // Reset all accessibility settings @@ -709,13 +770,26 @@ function resetAccessibilitySettings() { const documentClasses = [ 'snn-high-contrast', + 'snn-high-contrast-medium', + 'snn-high-contrast-high', + 'snn-high-contrast-ultra', 'snn-filter-protanopia', 'snn-filter-deuteranopia', 'snn-filter-tritanopia', - 'snn-filter-grayscale' + 'snn-filter-grayscale', + 'snn-text-align-left', + 'snn-text-align-center', + 'snn-text-align-right' ]; documentClasses.forEach(cls => document.documentElement.classList.remove(cls)); + const textSizeClasses = [ + 'snn-bigger-text-medium', + 'snn-bigger-text-large', + 'snn-bigger-text-xlarge' + ]; + textSizeClasses.forEach(cls => document.body.classList.remove(cls)); + domCache.getImages().forEach((img) => (img.style.display = '')); if (screenReader.active) { @@ -728,7 +802,7 @@ function resetAccessibilitySettings() { applySettings(); - const buttons = document.querySelectorAll('#snn-accessibility-menu .snn-accessibility-option'); + const buttons = shadowRoot.querySelectorAll('#snn-accessibility-menu .snn-accessibility-option'); buttons.forEach((button) => { button.classList.remove('active'); button.setAttribute('aria-pressed', 'false'); @@ -1143,7 +1217,7 @@ const voiceControl = { try { // Check for show menu commands - if (WIDGET_CONFIG.voiceCommands.showMenu.includes(command)) { + if (WIDGET_CONFIG.voiceCommands.showMenu.some(cmd => command.includes(cmd))) { if (!menuCache.button) menuCache.init(); if (menuCache.button) { menuCache.button.click(); @@ -1152,7 +1226,7 @@ const voiceControl = { } // Check for reset all commands - if (WIDGET_CONFIG.voiceCommands.resetAll.includes(command)) { + if (WIDGET_CONFIG.voiceCommands.resetAll.some(cmd => command.includes(cmd))) { resetAccessibilitySettings(); return; } @@ -1161,27 +1235,27 @@ const voiceControl = { let localStorageKey = null; // Check each command group - if (WIDGET_CONFIG.voiceCommands.highContrast.includes(command)) { + if (WIDGET_CONFIG.voiceCommands.highContrast.some(cmd => command.includes(cmd))) { localStorageKey = 'highContrast'; - } else if (WIDGET_CONFIG.voiceCommands.biggerText.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.biggerText.some(cmd => command.includes(cmd))) { localStorageKey = 'biggerText'; - } else if (WIDGET_CONFIG.voiceCommands.textSpacing.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.textSpacing.some(cmd => command.includes(cmd))) { localStorageKey = 'textSpacing'; - } else if (WIDGET_CONFIG.voiceCommands.pauseAnimations.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.pauseAnimations.some(cmd => command.includes(cmd))) { localStorageKey = 'pauseAnimations'; - } else if (WIDGET_CONFIG.voiceCommands.hideImages.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.hideImages.some(cmd => command.includes(cmd))) { localStorageKey = 'hideImages'; - } else if (WIDGET_CONFIG.voiceCommands.dyslexiaFont.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.dyslexiaFont.some(cmd => command.includes(cmd))) { localStorageKey = 'dyslexiaFont'; - } else if (WIDGET_CONFIG.voiceCommands.biggerCursor.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.biggerCursor.some(cmd => command.includes(cmd))) { localStorageKey = 'biggerCursor'; - } else if (WIDGET_CONFIG.voiceCommands.lineHeight.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.lineHeight.some(cmd => command.includes(cmd))) { localStorageKey = 'lineHeight'; - } else if (WIDGET_CONFIG.voiceCommands.textAlign.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.textAlign.some(cmd => command.includes(cmd))) { localStorageKey = 'textAlign'; - } else if (WIDGET_CONFIG.voiceCommands.screenReader.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.screenReader.some(cmd => command.includes(cmd))) { localStorageKey = 'screenReader'; - } else if (WIDGET_CONFIG.voiceCommands.voiceControl.includes(command)) { + } else if (WIDGET_CONFIG.voiceCommands.voiceControl.some(cmd => command.includes(cmd))) { localStorageKey = 'voiceControl'; } @@ -1428,7 +1502,7 @@ function createAccessibilityMenu() { // Add content to menu menu.appendChild(content); - document.body.appendChild(menu); + shadowRoot.appendChild(menu); } // =========================================== @@ -1441,8 +1515,8 @@ const menuCache = { button: null, closeButton: null, init: function () { - this.menu = document.getElementById('snn-accessibility-menu'); - this.button = document.getElementById('snn-accessibility-button'); + this.menu = shadowRoot.getElementById('snn-accessibility-menu'); + this.button = shadowRoot.getElementById('snn-accessibility-button'); this.closeButton = this.menu?.querySelector('.snn-close'); } }; @@ -1535,7 +1609,7 @@ function handleMenuKeyboard(e) { if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); - const currentIndex = elements.options.indexOf(document.activeElement); + const currentIndex = elements.options.indexOf(shadowRoot.activeElement); let nextIndex; if (e.key === 'ArrowDown') { @@ -1554,8 +1628,16 @@ function handleMenuKeyboard(e) { // Initialize the widget function initAccessibilityWidget() { - injectStyles(); + // Create shadow DOM first + createShadowContainer(); + + // Inject page styles (for accessibility features) + injectPageStyles(); + + // Apply saved settings applySettings(); + + // Create widget UI inside shadow DOM createAccessibilityButton(); createAccessibilityMenu(); } @@ -1569,5 +1651,4 @@ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAccessibilityWidget); } else { initAccessibilityWidget(); -} - +} \ No newline at end of file