This release significantly expands internationalization support and adds comprehensive identity management features. Internationalization (i18n): - Add 5 new languages: Spanish, Italian, German, Dutch, Portuguese - Expand from 3 to 8 total supported languages - Redesign language switcher for better scalability (dropdown UI) - Complete translations for all features across all languages Identity Management: - Multiple sender identities with per-identity signatures - Sub-addressing support (user+tag@domain.com) - Context-aware tag suggestions for sub-addresses - Identity badges in email viewer and list - Full CRUD operations for managing identities Newsletter Management: - RFC 2369 List-Unsubscribe support (one-click unsubscribe) - HTTP and mailto unsubscribe methods - Security validation prevents XSS attacks - Two-step confirmation with persistent dismissal Security & Accessibility: - Dark mode email readability (intelligent color transformation) - WCAG 2.0 Level AA color contrast compliance - Comprehensive XSS prevention with validation utilities - Unit test coverage for security-critical code (57 validation tests) Testing: - Add unit tests for validation utilities - Add unit tests for email sanitization - Add unit tests for color transformation - Full test coverage for XSS attack vectors
84 lines
2.3 KiB
TypeScript
84 lines
2.3 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
|
|
interface UseFocusTrapOptions {
|
|
isActive: boolean;
|
|
onEscape?: () => void;
|
|
restoreFocus?: boolean;
|
|
}
|
|
|
|
export function useFocusTrap({
|
|
isActive,
|
|
onEscape,
|
|
restoreFocus = true,
|
|
}: UseFocusTrapOptions) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const previousActiveElement = useRef<HTMLElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isActive || !containerRef.current) return;
|
|
|
|
// Store the element that had focus before modal opened
|
|
previousActiveElement.current = document.activeElement as HTMLElement;
|
|
|
|
const container = containerRef.current;
|
|
|
|
// Get all focusable elements
|
|
const getFocusableElements = () => {
|
|
return container.querySelectorAll<HTMLElement>(
|
|
'button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"]):not(:disabled)'
|
|
);
|
|
};
|
|
|
|
// Focus first element
|
|
const focusableElements = getFocusableElements();
|
|
const firstElement = focusableElements[0];
|
|
if (firstElement) {
|
|
firstElement.focus();
|
|
}
|
|
|
|
// Handle Tab key to trap focus
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
// Handle Escape
|
|
if (e.key === 'Escape' && onEscape) {
|
|
onEscape();
|
|
return;
|
|
}
|
|
|
|
// Handle Tab
|
|
if (e.key === 'Tab') {
|
|
const focusableElements = getFocusableElements();
|
|
const firstElement = focusableElements[0];
|
|
const lastElement = focusableElements[focusableElements.length - 1];
|
|
|
|
if (e.shiftKey) {
|
|
// Shift+Tab
|
|
if (document.activeElement === firstElement) {
|
|
lastElement?.focus();
|
|
e.preventDefault();
|
|
}
|
|
} else {
|
|
// Tab
|
|
if (document.activeElement === lastElement) {
|
|
firstElement?.focus();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
container.addEventListener('keydown', handleKeyDown);
|
|
|
|
// Cleanup
|
|
return () => {
|
|
container.removeEventListener('keydown', handleKeyDown);
|
|
|
|
// Restore focus to previous element
|
|
if (restoreFocus && previousActiveElement.current) {
|
|
previousActiveElement.current.focus();
|
|
}
|
|
};
|
|
}, [isActive, onEscape, restoreFocus]);
|
|
|
|
return containerRef;
|
|
}
|