a11y: skip link exists in App, ChatInput aria-label, sidebar focus trap, MiniPlayer aria-live

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
senke 2026-02-12 21:55:25 +01:00
parent 9f7a42cdb5
commit e5fd019edf
3 changed files with 32 additions and 10 deletions

View file

@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import { useLocation, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
@ -13,6 +13,7 @@ import { useSidebarNavigation } from '@/hooks/useSidebarNavigation';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Tooltip } from '@/components/ui/tooltip';
import { FocusTrap } from '@/components/ui/focus-trap';
interface SidebarProps {
currentView?: string;
@ -90,12 +91,25 @@ const navItemInactiveClasses =
const navItemActiveClasses = 'bg-primary/10 text-primary sidebar-active-indicator';
const LG_BREAKPOINT = 1024;
export const Sidebar: React.FC<SidebarProps> = ({ currentView }) => {
const { t } = useTranslation();
const location = useLocation();
const { sidebarOpen, setSidebarOpen } = useUIStore();
const { handleMobileNav, handleLogout } = useSidebarNavigation();
const navItems = useMemo(() => buildNavItems(t), [t]);
const [isMobile, setIsMobile] = useState(() =>
typeof window !== 'undefined' ? window.innerWidth < LG_BREAKPOINT : false,
);
useEffect(() => {
const mq = window.matchMedia(`(max-width: ${LG_BREAKPOINT - 1}px)`);
const handler = () => setIsMobile(mq.matches);
handler();
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
const activeView =
currentView ||
@ -113,7 +127,11 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView }) => {
/>
)}
<aside
<FocusTrap
active={sidebarOpen && isMobile}
onEscape={() => setSidebarOpen(false)}
>
<aside
data-testid="app-sidebar"
className={cn(
'fixed left-sidebar bottom-sidebar top-sidebar rounded-xl flex flex-col transition-shell z-sidebar overflow-hidden',
@ -301,6 +319,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView }) => {
</Tooltip>
</div>
</aside>
</FocusTrap>
</>
);
};

View file

@ -217,6 +217,7 @@ export const ChatInput: React.FC = () => {
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Broadcast message..."
aria-label="Type a message"
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:border-border/50 focus:ring-1 focus:ring-border/50 transition-all font-mono text-sm"
disabled={!currentConversationId || isUploading}
/>

View file

@ -5,6 +5,7 @@
import { ChevronUp, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTranslation } from '@/hooks/useTranslation';
import { Tooltip } from '@/components/ui/tooltip';
import { usePlayer } from '../hooks/usePlayer';
import { TrackInfo } from './TrackInfo';
@ -28,6 +29,7 @@ export function MiniPlayer({
className,
position = 'bottom',
}: MiniPlayerProps) {
const { t } = useTranslation();
const player = usePlayer();
if (!isVisible || !player.currentTrack) {
@ -55,12 +57,12 @@ export function MiniPlayer({
className,
)}
role="region"
aria-label="Mini lecteur audio"
aria-label={t('player.miniPlayerAriaLabel')}
>
<div className="container mx-auto px-4 py-2">
<div className="flex items-center gap-4">
{/* Track Info - Compact */}
<div className="flex-1 min-w-0">
<div className="flex-1 min-w-0" aria-live="polite">
<TrackInfo
track={player.currentTrack}
showCover={true}
@ -112,7 +114,7 @@ export function MiniPlayer({
</div>
{/* Toggle Button */}
<Tooltip content="Agrandir le lecteur">
<Tooltip content={t('player.expandPlayer')}>
<button
type="button"
onClick={onToggle}
@ -122,16 +124,16 @@ export function MiniPlayer({
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
'transition-colors duration-[var(--sumi-duration-fast)]',
)}
aria-label="Agrandir le lecteur"
aria-label={t('player.expandPlayer')}
>
<ChevronUp className="h-5 w-5" aria-hidden="true" />
<span className="sr-only">Agrandir le lecteur</span>
<span className="sr-only">{t('player.expandPlayer')}</span>
</button>
</Tooltip>
{/* Close Button (optional) */}
{onClose && (
<Tooltip content="Fermer le mini lecteur">
<Tooltip content={t('player.closeMiniPlayer')}>
<button
type="button"
onClick={onClose}
@ -141,10 +143,10 @@ export function MiniPlayer({
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
'transition-colors duration-[var(--sumi-duration-fast)]',
)}
aria-label="Fermer le mini lecteur"
aria-label={t('player.closeMiniPlayer')}
>
<X className="h-5 w-5" aria-hidden="true" />
<span className="sr-only">Fermer le mini lecteur</span>
<span className="sr-only">{t('player.closeMiniPlayer')}</span>
</button>
</Tooltip>
)}