mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
♿️ 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:
parent
199e5e6eaf
commit
2d3dd9e351
3 changed files with 89 additions and 31 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue