fix: comprehensive UI/UX accessibility and usability improvements

Public site: skip-to-content link, mobile menu focus trap + Escape key,
aria-current on nav, keyboard navigation for carousels/tabs/articles,
ARIA roles (tablist/tab/tabpanel, combobox/listbox, region, dialog),
form labels + aria-describedby, 44px touch targets, semantic HTML
(<time>, <del>), prefers-reduced-motion on Hero scroll hijack,
mobile schedule filters, URL hash sync on scroll for correct refresh.

Admin panel: password toggle aria-label, toast aria-live regions,
SelectField keyboard navigation (Arrow/Enter/Escape), aria-invalid
on validation errors, sidebar hamburger aria-label/expanded,
nav aria-label, ArrayEditor aria-expanded on collapsible items.
This commit is contained in:
2026-03-29 20:42:14 +03:00
parent 024424c578
commit 77ad2a6b68
30 changed files with 538 additions and 418 deletions
+9 -2
View File
@@ -24,7 +24,12 @@ export function Hero({ data: hero }: HeroProps) {
const centerVideo = videos[Math.floor(videos.length / 2)] || videos[0];
const totalVideos = videos.slice(0, 3).length + 1; // desktop (3) + mobile (1)
useEffect(() => { setMounted(true); }, []);
const prefersReducedMotion = useRef(false);
useEffect(() => {
setMounted(true);
prefersReducedMotion.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}, []);
const handleVideoReady = useCallback(() => {
readyCount.current += 1;
@@ -48,6 +53,7 @@ export function Hero({ data: hero }: HeroProps) {
if (!el) return;
function handleWheel(e: WheelEvent) {
if (prefersReducedMotion.current) return;
if (e.deltaY <= 0 || scrolledRef.current) return;
if (window.scrollY > 10) return;
scrolledRef.current = true;
@@ -60,6 +66,7 @@ export function Hero({ data: hero }: HeroProps) {
}
function handleTouchEnd(e: TouchEvent) {
if (prefersReducedMotion.current) return;
const startY = Number((el as HTMLElement).dataset.touchY);
const endY = e.changedTouches[0].clientY;
if (startY - endY > 50 && !scrolledRef.current && window.scrollY < 10) {
@@ -80,7 +87,7 @@ export function Hero({ data: hero }: HeroProps) {
}, [scrollToNext]);
return (
<section id="hero" ref={sectionRef} className="relative flex min-h-svh items-center justify-center overflow-hidden bg-neutral-950">
<section id="hero" ref={sectionRef} aria-label="Главный баннер" className="relative flex min-h-svh items-center justify-center overflow-hidden bg-neutral-950">
{/* Videos render only after hydration to avoid SSR mismatch */}
{mounted && (
<>