Implement Shadow DOM for accessibility widget and enhance styling for improved isolation and performance

This commit is contained in:
sinanisler
2025-12-21 23:26:59 +03:00
parent f3d4c03fe6
commit ee7e38c7f7
+136 -55
View File
@@ -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: `<svg xmlns="http://www.w3.org/2000/svg" style="fill:white;" viewBox="0 0 24 24" width="30px" height="30px"><path d="M0 0h24v24H0V0z" fill="none"></path><path d="M20.5 6c-2.61.7-5.67 1-8.5 1s-5.89-.3-8.5-1L3 8c1.86.5 4 .83 6 1v13h2v-6h2v6h2V9c2-.17 4.14-.5 6-1l-.5-2zM12 6c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"></path></svg>`,
highContrast: `<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#000"/><path d="M32 2a30 30 0 000 60V2z" fill="#fff"/></svg>`,
@@ -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();
}
}