fix(a11y): Batch E part 1 — labels, valid nesting, touch targets

- EmptyState: render <Link> as the button (a <button> inside <Link> was invalid
  and double-focusable); AboutPage <a href> -> <Link> (no full reload)
- aria-label on icon-only controls: settings Back, conversation Refresh/Send,
  crew chat Send
- Crew invite Accept/Decline raised to 44px touch targets
This commit is contained in:
SkateSpots 2026-09-26 18:46:51 +07:00
parent 0468ed1c1b
commit 09e1612db7
6 changed files with 13 additions and 9 deletions

View file

@ -431,6 +431,7 @@ export function CrewChat({
type="submit" type="submit"
size="icon" size="icon"
disabled={isPending || !draft.trim()} disabled={isPending || !draft.trim()}
aria-label="Send message"
className="bg-teal-500 hover:bg-teal-600 text-black shrink-0" className="bg-teal-500 hover:bg-teal-600 text-black shrink-0"
> >
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />} {isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}

View file

@ -118,7 +118,7 @@ export function CrewInviteCard({
size="sm" size="sm"
onClick={handleAccept} onClick={handleAccept}
disabled={busy} disabled={busy}
className="h-8 flex-1 gap-1.5 bg-teal-500 font-bold text-black hover:bg-teal-600" className="min-h-11 flex-1 gap-1.5 bg-teal-500 font-bold text-black hover:bg-teal-600"
> >
{accepting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />} {accepting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />}
Accept Accept
@ -128,7 +128,7 @@ export function CrewInviteCard({
variant="outline" variant="outline"
onClick={handleDecline} onClick={handleDecline}
disabled={busy} disabled={busy}
className="h-8 gap-1.5 border-white/15 text-gray-400 hover:bg-white/10 hover:text-white" className="min-h-11 gap-1.5 border-white/15 text-gray-400 hover:bg-white/10 hover:text-white"
> >
{declining ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />} {declining ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />}
Decline Decline

View file

@ -63,11 +63,11 @@ function ActionButton({ action }: { action: EmptyStateAction }) {
); );
if (action.to) { if (action.to) {
// Single interactive element — a <button> nested in a <Link> was invalid
// and double-focusable for assistive tech.
return ( return (
<Link to={action.to} className="w-full sm:w-auto"> <Link to={action.to} className={classes}>
<button type="button" className={classes}> {inner}
{inner}
</button>
</Link> </Link>
); );
} }

View file

@ -156,11 +156,11 @@ Version: v0.1.0`;
<p className="text-gray-300 mb-4 text-sm"> <p className="text-gray-300 mb-4 text-sm">
SkateSpots is open source and community-driven. Contribute code, share spots, or help others get started. SkateSpots is open source and community-driven. Contribute code, share spots, or help others get started.
</p> </p>
<a href="/onboarding"> <Link to="/onboarding">
<Button className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase w-full"> <Button className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase w-full">
Get Started Get Started
</Button> </Button>
</a> </Link>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -217,7 +217,7 @@ export default function ConversationPage() {
{/* Header */} {/* Header */}
<div className="flex items-center gap-2 sm:gap-3 pb-3 sm:pb-4 border-b border-gray-800 flex-shrink-0"> <div className="flex items-center gap-2 sm:gap-3 pb-3 sm:pb-4 border-b border-gray-800 flex-shrink-0">
<Link to="/messages"> <Link to="/messages">
<Button variant="ghost" size="icon" className="text-gray-400 hover:text-white"> <Button variant="ghost" size="icon" aria-label="Back" className="text-gray-400 hover:text-white">
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</Button> </Button>
</Link> </Link>
@ -239,6 +239,7 @@ export default function ConversationPage() {
size="icon" size="icon"
onClick={() => refetch()} onClick={() => refetch()}
disabled={isRefetching} disabled={isRefetching}
aria-label="Refresh"
className="text-gray-400 hover:text-white" className="text-gray-400 hover:text-white"
> >
<RefreshCw className={cn("h-4 w-4", isRefetching && "animate-spin")} /> <RefreshCw className={cn("h-4 w-4", isRefetching && "animate-spin")} />
@ -303,6 +304,7 @@ export default function ConversationPage() {
<Button <Button
onClick={handleSend} onClick={handleSend}
disabled={!newMessage.trim() || isSending} disabled={!newMessage.trim() || isSending}
aria-label="Send message"
className="bg-[#ff0040] hover:bg-[#ff0040]/90 text-white h-[44px] w-[44px] sm:h-auto sm:w-auto p-0 sm:px-4 flex-shrink-0" className="bg-[#ff0040] hover:bg-[#ff0040]/90 text-white h-[44px] w-[44px] sm:h-auto sm:w-auto p-0 sm:px-4 flex-shrink-0"
> >
{isSending ? ( {isSending ? (

View file

@ -76,6 +76,7 @@ function BackHeader({ title, onBack }: { title: string; onBack: () => void }) {
<div className="flex items-center gap-3 mb-5"> <div className="flex items-center gap-3 mb-5">
<button <button
onClick={onBack} onClick={onBack}
aria-label="Back"
className="flex items-center justify-center w-8 h-8 rounded-xl bg-white/5 hover:bg-white/10 active:scale-95 transition-all" className="flex items-center justify-center w-8 h-8 rounded-xl bg-white/5 hover:bg-white/10 active:scale-95 transition-all"
> >
<ChevronLeft className="h-4 w-4 text-white" /> <ChevronLeft className="h-4 w-4 text-white" />