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
+28 -6
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useRef } from "react";
import { CreditCard, Building2, ScrollText, Crown, Sparkles } from "lucide-react";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Reveal } from "@/components/ui/Reveal";
@@ -14,12 +14,27 @@ interface PricingProps {
export function Pricing({ data: pricing }: PricingProps) {
const [activeTab, setActiveTab] = useState<Tab>("prices");
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: "prices", label: "Абонементы", icon: <CreditCard size={16} /> },
{ id: "rental", label: "Аренда зала", icon: <Building2 size={16} /> },
{ id: "rules", label: "Правила", icon: <ScrollText size={16} /> },
];
function handleTabKeyDown(e: React.KeyboardEvent, index: number) {
let nextIndex: number | null = null;
if (e.key === "ArrowRight") {
nextIndex = (index + 1) % tabs.length;
} else if (e.key === "ArrowLeft") {
nextIndex = (index - 1 + tabs.length) % tabs.length;
}
if (nextIndex !== null) {
e.preventDefault();
setActiveTab(tabs[nextIndex].id);
tabRefs.current[nextIndex]?.focus();
}
}
// Split items: featured (big card) vs regular
const featuredItem = pricing.items.find((item) => item.featured);
const regularItems = pricing.items.filter((item) => !item.featured);
@@ -34,11 +49,18 @@ export function Pricing({ data: pricing }: PricingProps) {
{/* Tabs */}
<Reveal>
<div className="mt-12 flex flex-wrap justify-center gap-2">
{tabs.map((tab) => (
<div role="tablist" aria-label="Разделы цен" className="mt-12 flex flex-wrap justify-center gap-2">
{tabs.map((tab, index) => (
<button
key={tab.id}
ref={(el) => { tabRefs.current[index] = el; }}
role="tab"
aria-selected={activeTab === tab.id}
aria-controls={`tabpanel-${tab.id}`}
id={`tab-${tab.id}`}
tabIndex={activeTab === tab.id ? 0 : -1}
onClick={() => setActiveTab(tab.id)}
onKeyDown={(e) => handleTabKeyDown(e, index)}
className={`inline-flex items-center gap-2 rounded-full px-6 py-2.5 text-sm font-medium transition-all duration-300 cursor-pointer ${
activeTab === tab.id
? "bg-gold text-black shadow-lg shadow-gold/25"
@@ -53,7 +75,7 @@ export function Pricing({ data: pricing }: PricingProps) {
</Reveal>
{/* Prices tab */}
<div className={activeTab === "prices" ? "block" : "hidden"}>
<div id="tabpanel-prices" role="tabpanel" aria-labelledby="tab-prices" className={activeTab === "prices" ? "block" : "hidden"}>
<div className="mx-auto mt-10 max-w-4xl">
<p className="mb-8 text-center text-sm text-neutral-500 dark:text-neutral-400">
{pricing.subtitle}
@@ -132,7 +154,7 @@ export function Pricing({ data: pricing }: PricingProps) {
</div>
{/* Rental tab */}
<div className={activeTab === "rental" ? "block" : "hidden"}>
<div id="tabpanel-rental" role="tabpanel" aria-labelledby="tab-rental" className={activeTab === "rental" ? "block" : "hidden"}>
<div className="mx-auto mt-10 max-w-2xl space-y-3">
{pricing.rentalItems.map((item, i) => (
<div
@@ -158,7 +180,7 @@ export function Pricing({ data: pricing }: PricingProps) {
</div>
{/* Rules tab */}
<div className={activeTab === "rules" ? "block" : "hidden"}>
<div id="tabpanel-rules" role="tabpanel" aria-labelledby="tab-rules" className={activeTab === "rules" ? "block" : "hidden"}>
<div className="mx-auto mt-10 max-w-2xl space-y-3">
{pricing.rules.map((rule, i) => (
<div