diff --git a/src/app/globals.css b/src/app/globals.css index d03deda..3a74890 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -105,11 +105,12 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); } cursor: pointer; transition: background 150ms ease; } -.lang-switcher-btn:hover { - background: rgba(255, 255, 255, 0.1); +.lang-switcher-btn:hover, +.lang-switcher-btn:focus-visible { + background: rgba(255, 255, 255, 0.15); } .lang-switcher-btn.current { - background: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.25); } /* Markdown content */ @@ -526,7 +527,7 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); } .lang-switcher { display: inline-flex; gap: 0.25rem; - background: var(--color-red); + background: transparent; color: #fff; border-radius: 999px; padding: 0.25rem; @@ -536,9 +537,15 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); } color: #fff; padding: 0.375rem 0.625rem; border-radius: 999px; + background: transparent; + transition: background 150ms ease; +} +.lang-switcher a:hover, +.lang-switcher a:focus-visible { + background: rgba(255,255,255,0.15); } .lang-switcher a.current { - background: rgba(255,255,255,0.2); + background: rgba(255,255,255,0.25); } /* Footer */ diff --git a/src/components/layout/ConstructionModal.tsx b/src/components/layout/ConstructionModal.tsx index 4418fcc..ec0cc65 100644 --- a/src/components/layout/ConstructionModal.tsx +++ b/src/components/layout/ConstructionModal.tsx @@ -1,5 +1,5 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; type ConstructionModalProps = { isOpen: boolean; @@ -8,6 +8,8 @@ type ConstructionModalProps = { export function ConstructionModal({ isOpen, onClose }: ConstructionModalProps) { const [mounted, setMounted] = useState(false); + const modalRef = useRef(null); + const previousActiveElement = useRef(null); useEffect(() => { setMounted(true); @@ -16,20 +18,80 @@ export function ConstructionModal({ isOpen, onClose }: ConstructionModalProps) { useEffect(() => { if (isOpen) { document.body.style.overflow = "hidden"; + previousActiveElement.current = document.activeElement as HTMLElement; + + // Focus the button inside the modal + setTimeout(() => { + const button = modalRef.current?.querySelector("button"); + button?.focus(); + }, 0); } else { document.body.style.overflow = ""; + // Restore focus to the element that opened the modal + previousActiveElement.current?.focus(); } return () => { document.body.style.overflow = ""; }; }, [isOpen]); + // Focus trap: prevent Tab from leaving the modal + useEffect(() => { + if (!isOpen || !modalRef.current) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key !== "Tab") return; + + const focusableElements = modalRef.current?.querySelectorAll( + "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])" + ); + if (!focusableElements || focusableElements.length === 0) return; + + const firstElement = focusableElements[0] as HTMLElement; + const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement; + const activeElement = document.activeElement; + + if (e.shiftKey) { + // Shift+Tab on first element -> focus last element + if (activeElement === firstElement) { + e.preventDefault(); + lastElement.focus(); + } + } else { + // Tab on last element -> focus first element + if (activeElement === lastElement) { + e.preventDefault(); + firstElement.focus(); + } + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [isOpen]); + + // Close on Escape key + useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onClose(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose]); + if (!mounted || !isOpen) return null; return ( <>