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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user