From 2d3dd9e35105ab4fba5929a52a77930b0dbf3085 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Wed, 22 Jan 2025 03:54:13 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20a11y:=20Enhance=20Accessib?= =?UTF-8?q?ility=20in=20ToolSelectDialog,=20ThemeSelector=20and=20ChatGrou?= =?UTF-8?q?pItem=20(#5395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add keyboard shortcut for theme switching and improve accessibility announcements * fix: Improve accessibility of ToolSelectDialog close button * feat: Enhance accessibility in ChatGroupItem component --- .../Prompts/Groups/ChatGroupItem.tsx | 38 +++++++--- .../src/components/Tools/ToolSelectDialog.tsx | 7 +- client/src/components/ui/ThemeSelector.tsx | 75 ++++++++++++++----- 3 files changed, 89 insertions(+), 31 deletions(-) diff --git a/client/src/components/Prompts/Groups/ChatGroupItem.tsx b/client/src/components/Prompts/Groups/ChatGroupItem.tsx index 2ac6fbcfda..b372f8a59b 100644 --- a/client/src/components/Prompts/Groups/ChatGroupItem.tsx +++ b/client/src/components/Prompts/Groups/ChatGroupItem.tsx @@ -27,6 +27,7 @@ function ChatGroupItem({ const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false); const [isVariableDialogOpen, setVariableDialogOpen] = useState(false); const onEditClick = useCustomLink(`/d/prompts/${group._id}`); + const groupIsGlobal = useMemo( () => instanceProjectId != null && group.projectIds?.includes(instanceProjectId), [group, instanceProjectId], @@ -34,13 +35,14 @@ function ChatGroupItem({ const isOwner = useMemo(() => user?.id === group.author, [user, group]); const onCardClick: React.MouseEventHandler = () => { - const text = group.productionPrompt?.prompt ?? ''; - if (!text) { + const text = group.productionPrompt?.prompt; + if (!text?.trim()) { return; } - const hasVariables = detectVariables(text); - if (hasVariables) { - return setVariableDialogOpen(true); + + if (detectVariables(text)) { + setVariableDialogOpen(true); + return; } submitPrompt(text); @@ -59,33 +61,47 @@ function ChatGroupItem({ } >
- {groupIsGlobal === true && } + {groupIsGlobal === true && ( + + )} { e.stopPropagation(); setPreviewDialogOpen(true); }} className="w-full cursor-pointer rounded-lg text-text-secondary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed" > - + {isOwner && ( @@ -98,7 +114,7 @@ function ChatGroupItem({ onEditClick(e); }} > - +
diff --git a/client/src/components/ui/ThemeSelector.tsx b/client/src/components/ui/ThemeSelector.tsx index 30339bea8b..e643f05d87 100644 --- a/client/src/components/ui/ThemeSelector.tsx +++ b/client/src/components/ui/ThemeSelector.tsx @@ -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: , @@ -9,32 +15,55 @@ const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) = light: , }; + 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 ( -
- -
+ ); }; 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 ; } @@ -55,6 +91,11 @@ const ThemeSelector = ({ returnThemeOnly }: { returnThemeOnly?: boolean }) => {
+ {announcement && ( +
+ {announcement} +
+ )} ); };