diff --git a/tradein-mvp/frontend/src/components/trade-in/MapPicker.tsx b/tradein-mvp/frontend/src/components/trade-in/MapPicker.tsx index d96764eb..194ce389 100644 --- a/tradein-mvp/frontend/src/components/trade-in/MapPicker.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/MapPicker.tsx @@ -9,6 +9,8 @@ import { API_BASE_URL } from "@/lib/api"; const LEAFLET_VER = "1.9.4"; const LEAFLET_CSS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.css`; const LEAFLET_JS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.js`; +const LEAFLET_CSS_SRI = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="; +const LEAFLET_JS_SRI = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="; const EKB_CENTER: [number, number] = [56.8389, 60.6057]; /** Подгружает Leaflet с CDN один раз, резолвит window.L. */ @@ -23,6 +25,8 @@ function loadLeaflet(): Promise { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = LEAFLET_CSS; + link.integrity = LEAFLET_CSS_SRI; + link.crossOrigin = "anonymous"; link.setAttribute("data-leaflet", "1"); document.head.appendChild(link); } @@ -34,6 +38,8 @@ function loadLeaflet(): Promise { } const script = document.createElement("script"); script.src = LEAFLET_JS; + script.integrity = LEAFLET_JS_SRI; + script.crossOrigin = "anonymous"; script.setAttribute("data-leaflet", "1"); script.onload = () => resolve(w.L); script.onerror = () => reject(new Error("leaflet load failed")); @@ -96,6 +102,44 @@ export function MapPicker({ onPick, onClose }: Props) { }; }, []); + // Focus management: initial focus to close button, restore on unmount, Esc closes + const closeBtnRef = useRef(null); + const previouslyFocused = useRef(null); + + useEffect(() => { + previouslyFocused.current = document.activeElement as HTMLElement | null; + closeBtnRef.current?.focus(); + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + window.addEventListener("keydown", onKey); + return () => { + window.removeEventListener("keydown", onKey); + previouslyFocused.current?.focus(); + }; + }, [onClose]); + + // Focus trap: keep Tab/Shift+Tab inside the modal + const dialogRef = useRef(null); + const onDialogKeyDown = (e: React.KeyboardEvent) => { + if (e.key !== "Tab" || !dialogRef.current) return; + const focusables = dialogRef.current.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' + ); + if (focusables.length === 0) return; + const first = focusables[0]; + const last = focusables[focusables.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + }; + return (
e.stopPropagation()} + onKeyDown={onDialogKeyDown} style={{ background: "#fff", borderRadius: 12, @@ -131,8 +180,9 @@ export function MapPicker({ onPick, onClose }: Props) { borderBottom: "1px solid #e5e5e5", }} > - Выбор адреса на карте ЕКБ + Выбор адреса на карте ЕКБ