Chapter 11 User Interface Design and Experience Optimization
Haiyue
58min
Chapter 11: User Interface Design and Experience Optimization
Learning Objectives
- Master Chrome Extension UI design best practices
- Learn to implement responsive and adaptive interfaces
- Optimize user interaction experience and performance
- Implement accessibility and internationalization support
11.1 UI Design Principles
11.1.1 Design Philosophy
🔄 正在渲染 Mermaid 图表...
11.1.2 Modern UI Framework Integration
// ui-framework.js - Lightweight UI framework
class ChromeExtensionUI {
constructor() {
this.components = new Map();
this.themes = new Map();
this.currentTheme = 'light';
this.animations = new Map();
this.init();
}
init() {
this.setupThemes();
this.registerBaseComponents();
this.setupGlobalStyles();
this.bindEvents();
}
setupThemes() {
// Light theme
this.themes.set('light', {
primary: '#4285f4',
secondary: '#34a853',
danger: '#ea4335',
warning: '#fbbc05',
background: '#ffffff',
surface: '#f8f9fa',
'on-background': '#202124',
'on-surface': '#5f6368',
border: '#dadce0',
shadow: '0 2px 4px rgba(0,0,0,0.1)'
});
// Dark theme
this.themes.set('dark', {
primary: '#8ab4f8',
secondary: '#81c995',
danger: '#f28b82',
warning: '#fdd663',
background: '#202124',
surface: '#303134',
'on-background': '#e8eaed',
'on-surface': '#9aa0a6',
border: '#5f6368',
shadow: '0 2px 4px rgba(0,0,0,0.3)'
});
}
registerBaseComponents() {
// Button component
this.registerComponent('button', {
template: `
<button class="ce-button {{variant}} {{size}} {{disabled}}"
{{disabled}} data-ripple="true">
{{#if icon}}<span class="ce-button-icon">{{icon}}</span>{{/if}}
<span class="ce-button-text">{{text}}</span>
</button>
`,
props: {
text: '',
icon: '',
variant: 'primary', // primary, secondary, outline, ghost
size: 'medium', // small, medium, large
disabled: false,
onClick: null
},
mounted() {
this.addRippleEffect();
if (this.props.onClick) {
this.element.addEventListener('click', this.props.onClick);
}
}
});
// Input component
this.registerComponent('input', {
template: `
<div class="ce-input-group {{state}}">
{{#if label}}<label class="ce-input-label">{{label}}</label>{{/if}}
<input type="{{type}}" class="ce-input" placeholder="{{placeholder}}"
value="{{value}}" {{required}} {{disabled}}>
{{#if helperText}}<span class="ce-input-helper">{{helperText}}</span>{{/if}}
{{#if errorText}}<span class="ce-input-error">{{errorText}}</span>{{/if}}
</div>
`,
props: {
type: 'text',
label: '',
placeholder: '',
value: '',
helperText: '',
errorText: '',
required: false,
disabled: false,
state: 'normal' // normal, error, success
},
mounted() {
this.setupValidation();
this.setupFloatingLabel();
}
});
// Card component
this.registerComponent('card', {
template: `
<div class="ce-card {{variant}} {{elevation}}">
{{#if header}}
<div class="ce-card-header">
{{#if title}}<h3 class="ce-card-title">{{title}}</h3>{{/if}}
{{#if subtitle}}<p class="ce-card-subtitle">{{subtitle}}</p>{{/if}}
{{#if actions}}<div class="ce-card-actions">{{actions}}</div>{{/if}}
</div>
{{/if}}
<div class="ce-card-content">{{content}}</div>
{{#if footer}}
<div class="ce-card-footer">{{footer}}</div>
{{/if}}
</div>
`,
props: {
title: '',
subtitle: '',
content: '',
actions: '',
footer: '',
variant: 'default', // default, outlined, filled
elevation: 'medium' // none, low, medium, high
}
});
// Modal component
this.registerComponent('modal', {
template: `
<div class="ce-modal-overlay {{visible}}" role="dialog" aria-modal="true">
<div class="ce-modal-container {{size}}">
<div class="ce-modal-header">
<h2 class="ce-modal-title">{{title}}</h2>
<button class="ce-modal-close" aria-label="Close">×</button>
</div>
<div class="ce-modal-content">{{content}}</div>
<div class="ce-modal-footer">{{footer}}</div>
</div>
</div>
`,
props: {
title: '',
content: '',
footer: '',
size: 'medium', // small, medium, large
visible: false,
onClose: null
},
mounted() {
this.setupCloseHandlers();
this.setupKeyboardNavigation();
this.setupFocusTrap();
}
});
}
registerComponent(name, definition) {
this.components.set(name, definition);
}
createElement(componentName, props = {}) {
const component = this.components.get(componentName);
if (!component) {
throw new Error(`Component ${componentName} does not exist`);
}
const mergedProps = { ...component.props, ...props };
const html = this.renderTemplate(component.template, mergedProps);
const element = this.createElementFromHTML(html);
// Create component instance
const instance = {
element,
props: mergedProps,
component,
// Add component methods
addRippleEffect: this.addRippleEffect.bind({ element }),
setupValidation: this.setupValidation.bind({ element }),
setupFloatingLabel: this.setupFloatingLabel.bind({ element }),
setupCloseHandlers: this.setupCloseHandlers.bind({ element, props: mergedProps }),
setupKeyboardNavigation: this.setupKeyboardNavigation.bind({ element }),
setupFocusTrap: this.setupFocusTrap.bind({ element })
};
// Call mounted lifecycle
if (component.mounted) {
component.mounted.call(instance);
}
return instance;
}
renderTemplate(template, props) {
return template.replace(/\{\{(\#if\s+)?(\#unless\s+)?([^}]+)\}\}/g, (match, ifStatement, unlessStatement, propPath) => {
if (ifStatement) {
const prop = propPath.trim();
return props[prop] ? '' : '<!--';
}
if (unlessStatement) {
const prop = propPath.trim();
return !props[prop] ? '' : '<!--';
}
const value = this.getNestedProp(props, propPath.trim());
return value !== undefined ? value : '';
});
}
getNestedProp(obj, path) {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
createElementFromHTML(html) {
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
}
// Component method implementations
addRippleEffect() {
this.element.addEventListener('click', (e) => {
const ripple = document.createElement('span');
const rect = this.element.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
transform: scale(0);
animation: ripple 0.6s linear;
left: ${x}px;
top: ${y}px;
width: ${size}px;
height: ${size}px;
`;
this.element.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
});
}
setupValidation() {
const input = this.element.querySelector('.ce-input');
if (!input) return;
input.addEventListener('blur', () => {
this.validateInput(input);
});
input.addEventListener('input', () => {
if (this.element.classList.contains('error')) {
this.validateInput(input);
}
});
}
validateInput(input) {
const value = input.value.trim();
const required = input.hasAttribute('required');
let isValid = true;
let errorMessage = '';
if (required && !value) {
isValid = false;
errorMessage = 'This field is required';
} else if (input.type === 'email' && value && !this.isValidEmail(value)) {
isValid = false;
errorMessage = 'Please enter a valid email address';
} else if (input.type === 'url' && value && !this.isValidUrl(value)) {
isValid = false;
errorMessage = 'Please enter a valid URL';
}
this.element.classList.toggle('error', !isValid);
this.element.classList.toggle('success', isValid && value);
const errorElement = this.element.querySelector('.ce-input-error');
if (errorElement) {
errorElement.textContent = errorMessage;
}
return isValid;
}
isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
setupFloatingLabel() {
const input = this.element.querySelector('.ce-input');
const label = this.element.querySelector('.ce-input-label');
if (!input || !label) return;
const updateLabelState = () => {
const hasValue = input.value.trim() !== '';
const isFocused = document.activeElement === input;
this.element.classList.toggle('focused', isFocused);
this.element.classList.toggle('has-value', hasValue);
};
input.addEventListener('focus', updateLabelState);
input.addEventListener('blur', updateLabelState);
input.addEventListener('input', updateLabelState);
// Initial state
updateLabelState();
}
setupCloseHandlers() {
const closeBtn = this.element.querySelector('.ce-modal-close');
const overlay = this.element.querySelector('.ce-modal-overlay');
if (closeBtn && this.props.onClose) {
closeBtn.addEventListener('click', this.props.onClose);
}
if (overlay && this.props.onClose) {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.props.onClose();
}
});
}
}
setupKeyboardNavigation() {
this.element.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.props.onClose) {
this.props.onClose();
}
});
}
setupFocusTrap() {
const focusableElements = this.element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
this.element.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
});
// Auto-focus first element
firstFocusable?.focus();
}
// Theme management
setTheme(themeName) {
const theme = this.themes.get(themeName);
if (!theme) return;
this.currentTheme = themeName;
const root = document.documentElement;
Object.entries(theme).forEach(([property, value]) => {
root.style.setProperty(`--ce-${property}`, value);
});
document.body.setAttribute('data-theme', themeName);
}
getCurrentTheme() {
return this.currentTheme;
}
// Animation management
registerAnimation(name, keyframes, options = {}) {
this.animations.set(name, { keyframes, options });
}
animate(element, animationName, customOptions = {}) {
const animation = this.animations.get(animationName);
if (!animation) return;
const options = { ...animation.options, ...customOptions };
return element.animate(animation.keyframes, options);
}
setupGlobalStyles() {
const styles = `
.ce-button {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
}
.ce-button.primary {
background-color: var(--ce-primary);
color: white;
}
.ce-button.secondary {
background-color: var(--ce-secondary);
color: white;
}
.ce-button.outline {
background-color: transparent;
color: var(--ce-primary);
border: 1px solid var(--ce-primary);
}
.ce-button.ghost {
background-color: transparent;
color: var(--ce-primary);
}
.ce-button:hover:not([disabled]) {
transform: translateY(-1px);
box-shadow: var(--ce-shadow);
}
.ce-button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.ce-input-group {
position: relative;
margin-bottom: 16px;
}
.ce-input {
width: 100%;
padding: 12px;
border: 1px solid var(--ce-border);
border-radius: 4px;
font-size: 14px;
transition: all 0.2s ease;
}
.ce-input:focus {
outline: none;
border-color: var(--ce-primary);
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
}
.ce-input-label {
display: block;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
color: var(--ce-on-surface);
}
.ce-input-helper {
display: block;
margin-top: 4px;
font-size: 12px;
color: var(--ce-on-surface);
}
.ce-input-error {
display: block;
margin-top: 4px;
font-size: 12px;
color: var(--ce-danger);
}
.ce-input-group.error .ce-input {
border-color: var(--ce-danger);
}
.ce-input-group.success .ce-input {
border-color: var(--ce-secondary);
}
.ce-card {
background-color: var(--ce-surface);
border-radius: 8px;
overflow: hidden;
transition: all 0.2s ease;
}
.ce-card.outlined {
border: 1px solid var(--ce-border);
}
.ce-card.elevation-low {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.ce-card.elevation-medium {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.ce-card.elevation-high {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
}
.ce-card-header {
padding: 16px;
border-bottom: 1px solid var(--ce-border);
}
.ce-card-title {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.ce-card-subtitle {
margin: 4px 0 0 0;
font-size: 14px;
color: var(--ce-on-surface);
}
.ce-card-content {
padding: 16px;
}
.ce-card-footer {
padding: 16px;
border-top: 1px solid var(--ce-border);
}
.ce-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.ce-modal-overlay.visible {
opacity: 1;
visibility: visible;
}
.ce-modal-container {
background-color: var(--ce-background);
border-radius: 8px;
max-height: 90vh;
overflow: hidden;
transform: scale(0.9);
transition: transform 0.3s ease;
}
.ce-modal-overlay.visible .ce-modal-container {
transform: scale(1);
}
.ce-modal-container.small {
width: 400px;
}
.ce-modal-container.medium {
width: 600px;
}
.ce-modal-container.large {
width: 800px;
}
.ce-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid var(--ce-border);
}
.ce-modal-title {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.ce-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--ce-on-surface);
}
.ce-modal-content {
padding: 16px;
max-height: 60vh;
overflow-y: auto;
}
.ce-modal-footer {
padding: 16px;
border-top: 1px solid var(--ce-border);
display: flex;
gap: 8px;
justify-content: flex-end;
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
@media (max-width: 480px) {
.ce-modal-container {
width: 90vw !important;
max-height: 80vh;
}
}
`;
const styleElement = document.createElement('style');
styleElement.textContent = styles;
document.head.appendChild(styleElement);
}
bindEvents() {
// Global keyboard events
document.addEventListener('keydown', (e) => {
// Escape key closes modals
if (e.key === 'Escape') {
const visibleModal = document.querySelector('.ce-modal-overlay.visible');
if (visibleModal) {
const closeBtn = visibleModal.querySelector('.ce-modal-close');
closeBtn?.click();
}
}
});
}
}
// Usage example
const ui = new ChromeExtensionUI();
// Create button
const saveButton = ui.createElement('button', {
text: 'Save',
icon: '💾',
variant: 'primary',
onClick: () => console.log('Save button clicked')
});
// Create input
const emailInput = ui.createElement('input', {
type: 'email',
label: 'Email Address',
placeholder: 'Please enter email',
required: true
});
// Create card
const infoCard = ui.createElement('card', {
title: 'Extension Info',
subtitle: 'Version 1.0.0',
content: '<p>This is a powerful Chrome extension.</p>'
});
// Export UI framework
window.ChromeExtensionUI = ChromeExtensionUI;
11.2 Responsive Design
11.2.1 Adaptive Layout System
/* responsive-layout.css - Responsive layout system */
:root {
/* Breakpoint definitions */
--mobile: 480px;
--tablet: 768px;
--desktop: 1024px;
--large: 1200px;
/* Spacing system */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--space-xxl: 48px;
/* Typography system */
--font-xs: 12px;
--font-sm: 14px;
--font-md: 16px;
--font-lg: 18px;
--font-xl: 20px;
--font-xxl: 24px;
}
/* Container system */
.container {
width: 100%;
max-width: var(--large);
margin: 0 auto;
padding: 0 var(--space-md);
}
.container-fluid {
width: 100%;
padding: 0 var(--space-md);
}
/* Grid system */
.grid {
display: grid;
gap: var(--space-md);
}
.grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
.grid-cols-6 { grid-template-columns: repeat(6, 1fr); }
.grid-cols-12 { grid-template-columns: repeat(12, 1fr); }
/* Flexbox layout */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.items-end {
align-items: flex-end;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.flex-1 {
flex: 1;
}
/* Spacing utility classes */
.p-0 { padding: 0; }
.p-xs { padding: var(--space-xs); }
.p-sm { padding: var(--space-sm); }
.p-md { padding: var(--space-md); }
.p-lg { padding: var(--space-lg); }
.p-xl { padding: var(--space-xl); }
.m-0 { margin: 0; }
.m-xs { margin: var(--space-xs); }
.m-sm { margin: var(--space-sm); }
.m-md { margin: var(--space-md); }
.m-lg { margin: var(--space-lg); }
.m-xl { margin: var(--space-xl); }
.mt-auto { margin-top: auto; }
.mb-auto { margin-bottom: auto; }
.ml-auto { margin-left: auto; }
.mr-auto { margin-right: auto; }
/* Text styles */
.text-xs { font-size: var(--font-xs); }
.text-sm { font-size: var(--font-sm); }
.text-md { font-size: var(--font-md); }
.text-lg { font-size: var(--font-lg); }
.text-xl { font-size: var(--font-xl); }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-bold { font-weight: 700; }
.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }
/* Display controls */
.hidden { display: none; }
.block { display: block; }
.inline { display: inline; }
.inline-block { display: inline-block; }
/* Responsive breakpoints */
@media (max-width: 480px) {
.mobile\:hidden { display: none; }
.mobile\:block { display: block; }
.mobile\:flex { display: flex; }
.mobile\:grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
.mobile\:flex-col { flex-direction: column; }
.mobile\:text-center { text-align: center; }
.container {
padding: 0 var(--space-sm);
}
.grid {
gap: var(--space-sm);
}
}
@media (min-width: 481px) and (max-width: 768px) {
.tablet\:hidden { display: none; }
.tablet\:block { display: block; }
.tablet\:flex { display: flex; }
.tablet\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.tablet\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 769px) {
.desktop\:hidden { display: none; }
.desktop\:block { display: block; }
.desktop\:flex { display: flex; }
.desktop\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.desktop\:grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
}
/* Extension-specific responsive classes */
.extension-popup {
width: 400px;
min-height: 300px;
max-height: 600px;
}
@media (max-width: 480px) {
.extension-popup {
width: 320px;
min-height: 250px;
}
}
.extension-options {
max-width: 800px;
margin: 0 auto;
}
@media (max-width: 768px) {
.extension-options {
max-width: 100%;
padding: var(--space-sm);
}
}
/* Scrollable containers */
.scrollable {
overflow-y: auto;
max-height: 400px;
}
.scrollable::-webkit-scrollbar {
width: 6px;
}
.scrollable::-webkit-scrollbar-track {
background: var(--ce-surface);
}
.scrollable::-webkit-scrollbar-thumb {
background: var(--ce-border);
border-radius: 3px;
}
.scrollable::-webkit-scrollbar-thumb:hover {
background: var(--ce-on-surface);
}
/* Loading state */
.loading {
position: relative;
pointer-events: none;
opacity: 0.6;
}
.loading::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid var(--ce-border);
border-top: 2px solid var(--ce-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
z-index: 1;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Transition animations */
.transition {
transition: all 0.2s ease;
}
.transition-fast {
transition: all 0.1s ease;
}
.transition-slow {
transition: all 0.3s ease;
}
/* Hover effects */
.hover-lift:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.hover-scale:hover {
transform: scale(1.02);
}
/* Focus state */
.focus-visible {
outline: 2px solid var(--ce-primary);
outline-offset: 2px;
}
/* Error state */
.error {
border-color: var(--ce-danger) !important;
color: var(--ce-danger);
}
.success {
border-color: var(--ce-secondary) !important;
color: var(--ce-secondary);
}
/* Disabled state */
.disabled {
opacity: 0.6;
pointer-events: none;
cursor: not-allowed;
}
/* Helper classes */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--ce-border: #000000;
--ce-primary: #0000ff;
--ce-danger: #ff0000;
--ce-secondary: #008000;
}
}
/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Print styles */
@media print {
.no-print {
display: none !important;
}
.extension-popup,
.extension-options {
width: auto !important;
max-width: none !important;
max-height: none !important;
}
.scrollable {
max-height: none !important;
overflow: visible !important;
}
}
11.3 Performance Optimization
11.3.1 Rendering Performance Optimization
// performance-optimizer.js - Performance optimization tools
class PerformanceOptimizer {
constructor() {
this.observers = new Map();
this.debounceTimers = new Map();
this.throttleTimers = new Map();
this.virtualScrollers = new Map();
this.imageLoaders = new Map();
this.init();
}
init() {
this.setupIntersectionObserver();
this.setupPerformanceMonitoring();
this.setupResourceOptimization();
}
// Debounce function
debounce(func, wait, immediate = false) {
const key = func.toString();
return (...args) => {
const later = () => {
this.debounceTimers.delete(key);
if (!immediate) func.apply(this, args);
};
const callNow = immediate && !this.debounceTimers.has(key);
if (this.debounceTimers.has(key)) {
clearTimeout(this.debounceTimers.get(key));
}
this.debounceTimers.set(key, setTimeout(later, wait));
if (callNow) func.apply(this, args);
};
}
// Throttle function
throttle(func, wait) {
const key = func.toString();
return (...args) => {
if (this.throttleTimers.has(key)) {
return;
}
this.throttleTimers.set(key, setTimeout(() => {
this.throttleTimers.delete(key);
}, wait));
func.apply(this, args);
};
}
// Virtual scrolling implementation
createVirtualScroller(container, items, renderItem, itemHeight = 50) {
const scroller = {
container,
items,
renderItem,
itemHeight,
visibleStart: 0,
visibleEnd: 0,
scrollTop: 0,
containerHeight: container.clientHeight,
visibleCount: Math.ceil(container.clientHeight / itemHeight),
buffer: 5 // Buffer item count
};
this.virtualScrollers.set(container, scroller);
// Create scroll container
const scrollContainer = document.createElement('div');
scrollContainer.style.height = `${items.length * itemHeight}px`;
scrollContainer.style.position = 'relative';
const visibleContainer = document.createElement('div');
visibleContainer.style.position = 'absolute';
visibleContainer.style.top = '0';
visibleContainer.style.width = '100%';
scrollContainer.appendChild(visibleContainer);
container.appendChild(scrollContainer);
// Scroll event handler
const handleScroll = this.throttle(() => {
this.updateVirtualScroller(container);
}, 16); // 60fps
container.addEventListener('scroll', handleScroll);
// Initial render
this.updateVirtualScroller(container);
return {
update: (newItems) => {
scroller.items = newItems;
scrollContainer.style.height = `${newItems.length * itemHeight}px`;
this.updateVirtualScroller(container);
},
destroy: () => {
container.removeEventListener('scroll', handleScroll);
this.virtualScrollers.delete(container);
}
};
}
updateVirtualScroller(container) {
const scroller = this.virtualScrollers.get(container);
if (!scroller) return;
const scrollTop = container.scrollTop;
const visibleStart = Math.max(0, Math.floor(scrollTop / scroller.itemHeight) - scroller.buffer);
const visibleEnd = Math.min(
scroller.items.length,
visibleStart + scroller.visibleCount + scroller.buffer * 2
);
if (visibleStart !== scroller.visibleStart || visibleEnd !== scroller.visibleEnd) {
scroller.visibleStart = visibleStart;
scroller.visibleEnd = visibleEnd;
const visibleContainer = container.querySelector('div > div');
visibleContainer.innerHTML = '';
visibleContainer.style.top = `${visibleStart * scroller.itemHeight}px`;
for (let i = visibleStart; i < visibleEnd; i++) {
const item = scroller.items[i];
const element = scroller.renderItem(item, i);
element.style.height = `${scroller.itemHeight}px`;
visibleContainer.appendChild(element);
}
}
}
// Image lazy loading
setupLazyLoading(selector = 'img[data-src]') {
const images = document.querySelectorAll(selector);
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
this.loadImage(img);
imageObserver.unobserve(img);
}
});
}, {
rootMargin: '50px'
});
images.forEach(img => imageObserver.observe(img));
this.observers.set('lazyImages', imageObserver);
}
async loadImage(img) {
const src = img.dataset.src;
if (!src) return;
try {
const loadPromise = new Promise((resolve, reject) => {
const tempImg = new Image();
tempImg.onload = () => resolve(tempImg);
tempImg.onerror = reject;
tempImg.src = src;
});
await loadPromise;
img.src = src;
img.classList.add('loaded');
img.removeAttribute('data-src');
} catch (error) {
console.error('Image loading failed:', src, error);
img.classList.add('error');
}
}
// Component lazy rendering
setupLazyComponents(selector = '[data-lazy-component]') {
const components = document.querySelectorAll(selector);
const componentObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const component = entry.target;
this.loadComponent(component);
componentObserver.unobserve(component);
}
});
}, {
rootMargin: '100px'
});
components.forEach(component => componentObserver.observe(component));
this.observers.set('lazyComponents', componentObserver);
}
async loadComponent(element) {
const componentName = element.dataset.lazyComponent;
const componentProps = element.dataset.componentProps;
try {
element.classList.add('loading');
// Dynamic component import
const module = await import(`./components/${componentName}.js`);
const Component = module.default || module[componentName];
// Create component instance
const props = componentProps ? JSON.parse(componentProps) : {};
const instance = new Component(element, props);
// Render component
await instance.render();
element.classList.remove('loading');
element.classList.add('loaded');
} catch (error) {
console.error('Component loading failed:', componentName, error);
element.classList.remove('loading');
element.classList.add('error');
}
}
// Batch DOM updates
batchDOMUpdates(updates) {
return new Promise(resolve => {
requestAnimationFrame(() => {
updates.forEach(update => update());
resolve();
});
});
}
// Memory leak detection
startMemoryMonitoring() {
const memoryInfo = performance.memory;
let lastUsedJSHeapSize = memoryInfo.usedJSHeapSize;
setInterval(() => {
const currentUsedJSHeapSize = performance.memory.usedJSHeapSize;
const memoryDelta = currentUsedJSHeapSize - lastUsedJSHeapSize;
if (memoryDelta > 10 * 1024 * 1024) { // 10MB increase
console.warn('Memory usage growing too fast:', {
delta: `${(memoryDelta / 1024 / 1024).toFixed(2)}MB`,
current: `${(currentUsedJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
limit: `${(memoryInfo.jsHeapSizeLimit / 1024 / 1024).toFixed(2)}MB`
});
}
lastUsedJSHeapSize = currentUsedJSHeapSize;
}, 10000); // Check every 10 seconds
}
// Performance metrics monitoring
setupPerformanceMonitoring() {
// Monitor long tasks
if ('PerformanceObserver' in window) {
const longTaskObserver = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
console.warn('Long task detected:', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name
});
});
});
try {
longTaskObserver.observe({ entryTypes: ['longtask'] });
} catch (error) {
// Some environments may not support longtask
}
}
// Monitor resource loading
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0];
const resources = performance.getEntriesByType('resource');
console.log('Page performance metrics:', {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
resourceCount: resources.length,
totalSize: resources.reduce((sum, resource) => sum + (resource.transferSize || 0), 0)
});
});
}
setupResourceOptimization() {
// Preload critical resources
this.preloadCriticalResources([
{ href: 'icons/icon48.png', as: 'image' },
{ href: 'css/critical.css', as: 'style' }
]);
// Preconnect external domains
this.preconnectDomains([
'https://fonts.googleapis.com',
'https://api.example.com'
]);
}
preloadCriticalResources(resources) {
resources.forEach(resource => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = chrome.runtime.getURL(resource.href);
link.as = resource.as;
if (resource.type) link.type = resource.type;
document.head.appendChild(link);
});
}
preconnectDomains(domains) {
domains.forEach(domain => {
const link = document.createElement('link');
link.rel = 'preconnect';
link.href = domain;
document.head.appendChild(link);
});
}
setupIntersectionObserver() {
// Set up observer for animation elements
const animationObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
}
});
}, {
threshold: 0.1
});
const animatableElements = document.querySelectorAll('[data-animate]');
animatableElements.forEach(el => animationObserver.observe(el));
this.observers.set('animation', animationObserver);
}
// Cleanup resources
cleanup() {
this.observers.forEach(observer => observer.disconnect());
this.observers.clear();
this.debounceTimers.forEach(timer => clearTimeout(timer));
this.debounceTimers.clear();
this.throttleTimers.forEach(timer => clearTimeout(timer));
this.throttleTimers.clear();
this.virtualScrollers.clear();
this.imageLoaders.clear();
}
// Get performance report
getPerformanceReport() {
const navigation = performance.getEntriesByType('navigation')[0];
const resources = performance.getEntriesByType('resource');
const memory = performance.memory;
return {
timing: {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime || 0,
firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0
},
resources: {
count: resources.length,
totalSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0),
slowResources: resources.filter(r => r.duration > 1000)
},
memory: {
used: `${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
total: `${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
limit: `${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)}MB`
},
observers: {
active: this.observers.size,
virtualScrollers: this.virtualScrollers.size
}
};
}
}
// Global performance optimizer instance
const performanceOptimizer = new PerformanceOptimizer();
// Export for use by other modules
window.performanceOptimizer = performanceOptimizer;
11.4 Accessibility
11.4.1 Accessibility Support Implementation
// accessibility.js - Accessibility support
class AccessibilityManager {
constructor() {
this.focusableElements = [
'button',
'input',
'select',
'textarea',
'a[href]',
'[tabindex]:not([tabindex="-1"])',
'[contenteditable="true"]'
];
this.announcements = [];
this.init();
}
init() {
this.setupKeyboardNavigation();
this.setupScreenReaderSupport();
this.setupFocusManagement();
this.setupAriaLiveRegion();
this.enhanceFormAccessibility();
}
setupKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
this.handleGlobalKeyboard(e);
});
// Add focus indicators to all focusable elements
const focusableSelectors = this.focusableElements.join(', ');
document.addEventListener('focusin', (e) => {
if (e.target.matches(focusableSelectors)) {
e.target.classList.add('keyboard-focused');
}
});
document.addEventListener('focusout', (e) => {
e.target.classList.remove('keyboard-focused');
});
}
handleGlobalKeyboard(e) {
// Escape key handling
if (e.key === 'Escape') {
this.handleEscapeKey();
}
// Tab key trap
if (e.key === 'Tab') {
this.handleTabKey(e);
}
// Arrow key navigation
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
this.handleArrowKeys(e);
}
// Shortcut key handling
this.handleShortcuts(e);
}
handleEscapeKey() {
// Close modal
const modal = document.querySelector('.ce-modal-overlay.visible');
if (modal) {
const closeBtn = modal.querySelector('.ce-modal-close');
closeBtn?.click();
return;
}
// Close dropdown menu
const dropdown = document.querySelector('.dropdown.open');
if (dropdown) {
dropdown.classList.remove('open');
return;
}
// Clear focus
if (document.activeElement && document.activeElement !== document.body) {
document.activeElement.blur();
}
}
handleTabKey(e) {
const modal = document.querySelector('.ce-modal-overlay.visible');
if (modal) {
this.trapFocusInModal(e, modal);
}
}
trapFocusInModal(e, modal) {
const focusableElements = modal.querySelectorAll(this.focusableElements.join(', '));
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();
}
}
}
handleArrowKeys(e) {
const target = e.target;
// Menu navigation
if (target.closest('.menu, .dropdown-menu')) {
this.handleMenuNavigation(e);
}
// Table navigation
if (target.closest('table')) {
this.handleTableNavigation(e);
}
// Grid navigation
if (target.closest('.grid[role="grid"]')) {
this.handleGridNavigation(e);
}
}
handleMenuNavigation(e) {
const menu = e.target.closest('.menu, .dropdown-menu');
const items = menu.querySelectorAll('[role="menuitem"]');
const currentIndex = Array.from(items).indexOf(e.target);
let nextIndex;
switch (e.key) {
case 'ArrowDown':
nextIndex = (currentIndex + 1) % items.length;
break;
case 'ArrowUp':
nextIndex = (currentIndex - 1 + items.length) % items.length;
break;
case 'Home':
nextIndex = 0;
break;
case 'End':
nextIndex = items.length - 1;
break;
default:
return;
}
e.preventDefault();
items[nextIndex].focus();
}
handleTableNavigation(e) {
const table = e.target.closest('table');
const cells = table.querySelectorAll('td, th');
const currentCell = e.target.closest('td, th');
const currentIndex = Array.from(cells).indexOf(currentCell);
const row = currentCell.parentElement;
const cellsInRow = row.querySelectorAll('td, th');
const columnIndex = Array.from(cellsInRow).indexOf(currentCell);
let nextCell;
switch (e.key) {
case 'ArrowRight':
nextCell = cells[currentIndex + 1];
break;
case 'ArrowLeft':
nextCell = cells[currentIndex - 1];
break;
case 'ArrowDown':
const nextRow = row.nextElementSibling;
if (nextRow) {
nextCell = nextRow.querySelectorAll('td, th')[columnIndex];
}
break;
case 'ArrowUp':
const prevRow = row.previousElementSibling;
if (prevRow) {
nextCell = prevRow.querySelectorAll('td, th')[columnIndex];
}
break;
default:
return;
}
if (nextCell) {
e.preventDefault();
nextCell.focus();
}
}
handleShortcuts(e) {
// Alt + shortcuts
if (e.altKey) {
switch (e.key) {
case 's':
e.preventDefault();
this.skipToMainContent();
break;
case 'h':
e.preventDefault();
this.skipToHeader();
break;
case 'f':
e.preventDefault();
this.skipToFooter();
break;
}
}
// Ctrl/Cmd + shortcuts
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case '/':
e.preventDefault();
this.focusSearchInput();
break;
case 'k':
e.preventDefault();
this.openCommandPalette();
break;
}
}
}
setupScreenReaderSupport() {
// Dynamic content change notifications
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
this.announceContentChanges(mutation);
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false
});
// Add appropriate ARIA attributes to dynamic content
this.enhanceAriaLabels();
}
announceContentChanges(mutation) {
const addedNodes = Array.from(mutation.addedNodes);
const importantChanges = addedNodes.filter(node => {
return node.nodeType === Node.ELEMENT_NODE &&
(node.matches('.alert, .notification, .error, .success') ||
node.hasAttribute('aria-live'));
});
importantChanges.forEach(node => {
const text = node.textContent.trim();
if (text) {
this.announce(text, 'polite');
}
});
}
enhanceAriaLabels() {
// Add labels to unlabeled form controls
const unlabeledInputs = document.querySelectorAll('input:not([aria-label]):not([aria-labelledby])');
unlabeledInputs.forEach(input => {
const label = input.closest('.form-group, .input-group')?.querySelector('label');
if (label && !label.getAttribute('for')) {
const id = input.id || `input_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
input.id = id;
label.setAttribute('for', id);
} else if (!label) {
const placeholder = input.getAttribute('placeholder');
if (placeholder) {
input.setAttribute('aria-label', placeholder);
}
}
});
// Add descriptive labels to buttons
const iconButtons = document.querySelectorAll('button:not([aria-label]):not([aria-labelledby])');
iconButtons.forEach(button => {
if (!button.textContent.trim()) {
const icon = button.querySelector('.icon, [class*="icon"]');
if (icon) {
const action = this.guessButtonAction(button);
button.setAttribute('aria-label', action);
}
}
});
}
guessButtonAction(button) {
const classes = button.className.toLowerCase();
const iconClasses = button.querySelector('.icon, [class*="icon"]')?.className.toLowerCase() || '';
if (classes.includes('close') || iconClasses.includes('close')) return 'Close';
if (classes.includes('delete') || iconClasses.includes('delete')) return 'Delete';
if (classes.includes('edit') || iconClasses.includes('edit')) return 'Edit';
if (classes.includes('save') || iconClasses.includes('save')) return 'Save';
if (classes.includes('search') || iconClasses.includes('search')) return 'Search';
if (classes.includes('menu') || iconClasses.includes('menu')) return 'Menu';
return 'Button';
}
setupFocusManagement() {
// Set initial focus on page load
window.addEventListener('load', () => {
this.setInitialFocus();
});
// Focus management for dynamic content
document.addEventListener('ce:content-loaded', (e) => {
this.manageDynamicFocus(e.detail.container);
});
}
setInitialFocus() {
// Find first error field
const errorField = document.querySelector('.error input, .error select, .error textarea');
if (errorField) {
errorField.focus();
return;
}
// Find primary action button
const primaryAction = document.querySelector('.btn-primary, [role="button"][data-primary]');
if (primaryAction) {
primaryAction.focus();
return;
}
// Find first form field
const firstInput = document.querySelector('input, select, textarea');
if (firstInput) {
firstInput.focus();
return;
}
}
manageDynamicFocus(container) {
// Set appropriate focus for new content
const focusableElement = container.querySelector(this.focusableElements.join(', '));
if (focusableElement) {
focusableElement.focus();
}
}
setupAriaLiveRegion() {
// Create live region for notifications
const liveRegion = document.createElement('div');
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.className = 'sr-only';
liveRegion.id = 'aria-live-region';
document.body.appendChild(liveRegion);
// Create live region for urgent notifications
const urgentRegion = document.createElement('div');
urgentRegion.setAttribute('aria-live', 'assertive');
urgentRegion.setAttribute('aria-atomic', 'true');
urgentRegion.className = 'sr-only';
urgentRegion.id = 'aria-live-urgent';
document.body.appendChild(urgentRegion);
}
announce(message, priority = 'polite') {
const regionId = priority === 'assertive' ? 'aria-live-urgent' : 'aria-live-region';
const region = document.getElementById(regionId);
if (region) {
// Clear previous message
region.textContent = '';
// Delay setting new message to ensure screen readers detect the change
setTimeout(() => {
region.textContent = message;
}, 100);
// Record announcement history
this.announcements.push({
message,
priority,
timestamp: Date.now()
});
// Limit history length
if (this.announcements.length > 50) {
this.announcements = this.announcements.slice(-25);
}
}
}
enhanceFormAccessibility() {
// Add fieldset and legend to forms
const forms = document.querySelectorAll('form');
forms.forEach(form => {
this.addFormStructure(form);
this.addFormValidation(form);
});
}
addFormStructure(form) {
// Check if there's already appropriate structure
if (form.querySelector('fieldset')) return;
const inputs = form.querySelectorAll('input, select, textarea');
if (inputs.length > 3) {
// Create fieldset to wrap multiple related fields
const fieldset = document.createElement('fieldset');
const legend = document.createElement('legend');
legend.textContent = form.getAttribute('data-form-title') || 'Form';
fieldset.appendChild(legend);
// Move all child elements into fieldset
while (form.firstChild) {
fieldset.appendChild(form.firstChild);
}
form.appendChild(fieldset);
}
}
addFormValidation(form) {
form.addEventListener('submit', (e) => {
const isValid = this.validateForm(form);
if (!isValid) {
e.preventDefault();
this.focusFirstError(form);
this.announce('Form validation failed, please check error fields', 'assertive');
}
});
// Real-time validation
const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
input.addEventListener('blur', () => {
this.validateField(input);
});
});
}
validateForm(form) {
const inputs = form.querySelectorAll('input, select, textarea');
let isValid = true;
inputs.forEach(input => {
if (!this.validateField(input)) {
isValid = false;
}
});
return isValid;
}
validateField(input) {
const value = input.value.trim();
const required = input.hasAttribute('required');
let isValid = true;
let errorMessage = '';
if (required && !value) {
isValid = false;
errorMessage = 'This field is required';
} else if (input.type === 'email' && value && !this.isValidEmail(value)) {
isValid = false;
errorMessage = 'Please enter a valid email address';
}
this.setFieldError(input, isValid ? null : errorMessage);
return isValid;
}
setFieldError(input, errorMessage) {
const container = input.closest('.form-group, .input-group, .ce-input-group');
if (!container) return;
// Remove previous error state
container.classList.remove('error');
const existingError = container.querySelector('.field-error');
if (existingError) {
existingError.remove();
}
if (errorMessage) {
// Add error state
container.classList.add('error');
// Create error message element
const errorElement = document.createElement('div');
errorElement.className = 'field-error';
errorElement.textContent = errorMessage;
errorElement.setAttribute('role', 'alert');
// Associate error message to input field
const errorId = `error_${input.id || Date.now()}`;
errorElement.id = errorId;
input.setAttribute('aria-describedby', errorId);
container.appendChild(errorElement);
} else {
input.removeAttribute('aria-describedby');
}
}
focusFirstError(form) {
const errorField = form.querySelector('.error input, .error select, .error textarea');
if (errorField) {
errorField.focus();
errorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// Convenience methods
skipToMainContent() {
const main = document.querySelector('main, [role="main"], #main');
if (main) {
main.focus();
main.scrollIntoView({ behavior: 'smooth' });
}
}
skipToHeader() {
const header = document.querySelector('header, [role="banner"], .header');
if (header) {
header.focus();
header.scrollIntoView({ behavior: 'smooth' });
}
}
skipToFooter() {
const footer = document.querySelector('footer, [role="contentinfo"], .footer');
if (footer) {
footer.focus();
footer.scrollIntoView({ behavior: 'smooth' });
}
}
focusSearchInput() {
const searchInput = document.querySelector('input[type="search"], input[placeholder*="Search"], #search');
if (searchInput) {
searchInput.focus();
}
}
openCommandPalette() {
// Command palette opening logic
this.announce('Command palette opened', 'polite');
}
isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Get accessibility status report
getAccessibilityReport() {
const issues = this.checkAccessibilityIssues();
return {
score: this.calculateAccessibilityScore(issues),
issues: issues,
announcements: this.announcements.length,
focusableElements: document.querySelectorAll(this.focusableElements.join(', ')).length
};
}
checkAccessibilityIssues() {
const issues = [];
// Check images missing alt attributes
const imagesWithoutAlt = document.querySelectorAll('img:not([alt])');
if (imagesWithoutAlt.length > 0) {
issues.push({
type: 'missing-alt',
count: imagesWithoutAlt.length,
description: 'Images missing alt attributes'
});
}
// Check unlabeled form controls
const unlabeledInputs = document.querySelectorAll('input:not([aria-label]):not([aria-labelledby])');
if (unlabeledInputs.length > 0) {
issues.push({
type: 'unlabeled-input',
count: unlabeledInputs.length,
description: 'Form controls missing labels'
});
}
// Check color contrast (simple check)
const lowContrastElements = this.findLowContrastElements();
if (lowContrastElements.length > 0) {
issues.push({
type: 'low-contrast',
count: lowContrastElements.length,
description: 'Insufficient color contrast'
});
}
return issues;
}
findLowContrastElements() {
// Simple contrast check implementation
const elements = document.querySelectorAll('*');
const lowContrast = [];
elements.forEach(el => {
const style = window.getComputedStyle(el);
const color = style.color;
const backgroundColor = style.backgroundColor;
// This requires a more complex contrast calculation algorithm
// Simplified version for demonstration only
if (color && backgroundColor && this.hasLowContrast(color, backgroundColor)) {
lowContrast.push(el);
}
});
return lowContrast.slice(0, 10); // Limit count
}
hasLowContrast(color, backgroundColor) {
// Simplified contrast check
// Actual implementation needs more complex color analysis
return false; // Placeholder
}
calculateAccessibilityScore(issues) {
let score = 100;
issues.forEach(issue => {
switch (issue.type) {
case 'missing-alt':
score -= issue.count * 5;
break;
case 'unlabeled-input':
score -= issue.count * 10;
break;
case 'low-contrast':
score -= issue.count * 3;
break;
}
});
return Math.max(0, score);
}
}
// Global accessibility manager instance
const accessibilityManager = new AccessibilityManager();
// Export for use by other modules
window.accessibilityManager = accessibilityManager;
warning Accessibility Requirements Ensure Chrome extensions meet WCAG 2.1 AA level standards:
- All interactive elements are keyboard accessible
- Provide appropriate ARIA labels and descriptions
- Ensure sufficient color contrast
- Support screen readers
- Provide skip links and navigation shortcuts
tip Best Practices
- Progressive Enhancement: Ensure core functionality works in all environments
- Performance First: Optimize critical rendering path and interaction response time
- User Testing: Conduct usability testing with real users
- Internationalization: Support multiple languages and different cultural backgrounds
- Error Handling: Provide clear error messages and recovery paths
note Summary This chapter comprehensively introduced user interface design and experience optimization for Chrome extensions, including modern UI framework implementation, responsive design, performance optimization, and accessibility support. Excellent user experience is a key factor in extension success. In the next chapter, we will learn about security and permission management to ensure the safety of the extension and user data.