️ a11y: Enhance Accessibility in ToolSelectDialog, ThemeSelector and ChatGroupItem (#5395)

* feat: Add keyboard shortcut for theme switching and improve accessibility announcements

* fix: Improve accessibility of ToolSelectDialog close button

* feat: Enhance accessibility in ChatGroupItem component
This commit is contained in:
Marco Beretta 2025-01-22 03:54:13 +01:00 committed by GitHub
parent 199e5e6eaf
commit 2d3dd9e351
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 89 additions and 31 deletions

View file

@ -1,7 +1,13 @@
import React, { useContext, useCallback, useEffect } from 'react';
import React, { useContext, useCallback, useEffect, useState } from 'react';
import { Sun, Moon, Monitor } from 'lucide-react';
import { ThemeContext } from '~/hooks';
declare global {
interface Window {
lastThemeChange?: number;
}
}
const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) => void }) => {
const themeIcons = {
system: <Monitor />,
@ -9,32 +15,55 @@ const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) =
light: <Sun />,
};
const nextTheme = theme === 'dark' ? 'light' : 'dark';
const label = `Switch to ${nextTheme} theme`;
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 't') {
e.preventDefault();
onChange(nextTheme);
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, [nextTheme, onChange]);
return (
<div className="flex items-center justify-between">
<button
className="cursor-pointer"
onClick={() => onChange(theme === 'dark' ? 'light' : 'dark')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onChange(theme === 'dark' ? 'light' : 'dark');
}
}}
role="switch"
aria-checked={theme === 'dark'}
tabIndex={0}
>
{themeIcons[theme]}
</button>
</div>
<button
className="flex items-center gap-2 rounded-lg p-2 transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
aria-label={label}
aria-keyshortcuts="Ctrl+Shift+T"
onClick={(e) => {
e.preventDefault();
onChange(nextTheme);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onChange(nextTheme);
}
}}
>
{themeIcons[theme]}
</button>
);
};
const ThemeSelector = ({ returnThemeOnly }: { returnThemeOnly?: boolean }) => {
const { theme, setTheme } = useContext(ThemeContext);
const [announcement, setAnnouncement] = useState('');
const changeTheme = useCallback(
(value: string) => {
const now = Date.now();
if (typeof window.lastThemeChange === 'number' && now - window.lastThemeChange < 500) {
return;
}
window.lastThemeChange = now;
setTheme(value);
setAnnouncement(value === 'dark' ? 'Dark theme enabled' : 'Light theme enabled');
},
[setTheme],
);
@ -46,6 +75,13 @@ const ThemeSelector = ({ returnThemeOnly }: { returnThemeOnly?: boolean }) => {
}
}, [theme, setTheme]);
useEffect(() => {
if (announcement) {
const timeout = setTimeout(() => setAnnouncement(''), 1000);
return () => clearTimeout(timeout);
}
}, [announcement]);
if (returnThemeOnly === true) {
return <Theme theme={theme} onChange={changeTheme} />;
}
@ -55,6 +91,11 @@ const ThemeSelector = ({ returnThemeOnly }: { returnThemeOnly?: boolean }) => {
<div className="absolute bottom-0 left-0 m-4">
<Theme theme={theme} onChange={changeTheme} />
</div>
{announcement && (
<div aria-live="polite" className="sr-only">
{announcement}
</div>
)}
</div>
);
};