From 8d20916a2bd5b97584e1385537cd2e983ac64c9d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:09:07 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Improve=20accessibili?= =?UTF-8?q?ty=20and=20loading=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change implements several micro-UX improvements focused on accessibility and user feedback: 1. Added \`aria-label\` and \`title\` to multiple icon-only buttons across the application (Header, ChatPanel, MobileIconsBar, History) to improve accessibility and provide tooltips. 2. Implemented a loading state (spinner) for the chat submission action on both desktop and mobile, providing immediate visual feedback during async operations. 3. Ensured branding elements like the logo have descriptive labels when functioning as interactive controls. 4. Added a Palette journal for tracking critical UX learnings. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- .Jules/palette.md | 3 ++ components/chat-panel.tsx | 33 ++++++++++++--- components/chat.tsx | 9 ++++- components/header-search-button.tsx | 1 + components/header.tsx | 34 ++++++++++++++-- components/history.tsx | 2 + components/mobile-icons-bar.tsx | 62 +++++++++++++++++++++++++---- 7 files changed, 125 insertions(+), 19 deletions(-) create mode 100644 .Jules/palette.md diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 00000000..b174b782 --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2025-05-14 - [Icon-only Button Accessibility] +**Learning:** Many icon-only buttons in the application were missing ARIA labels and titles, making them inaccessible to screen readers and providing no tooltips for sighted users. The logo functioning as a history toggle was particularly non-obvious. +**Action:** Always provide both `aria-label` and `title` for icon-only buttons. Ensure that branding elements used as interactive controls have clear descriptive labels. diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index ca2fbc6f..b999803b 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -7,6 +7,7 @@ import { cn } from '@/lib/utils' import { UserMessage } from './user-message' import { Button } from './ui/button' import { ArrowRight, Plus, Paperclip, X, Sprout } from 'lucide-react' +import { Spinner } from './ui/spinner' import Textarea from 'react-textarea-autosize' import { nanoid } from '@/lib/utils' import { useSettingsStore } from '@/lib/store/settings' @@ -20,6 +21,7 @@ interface ChatPanelProps { input: string setInput: (value: string) => void onSuggestionsChange?: (suggestions: PartialRelated | null) => void + onSubmitting?: (isSubmitting: boolean) => void } export interface ChatPanelRef { @@ -27,12 +29,13 @@ export interface ChatPanelRef { submitForm: () => void } -export const ChatPanel = forwardRef(({ messages, input, setInput, onSuggestionsChange }, ref) => { +export const ChatPanel = forwardRef(({ messages, input, setInput, onSuggestionsChange, onSubmitting }, ref) => { const [, setMessages] = useUIState() const { submit, clearChat } = useActions() const { mapProvider } = useSettingsStore() const [isMobile, setIsMobile] = useState(false) const [selectedFile, setSelectedFile] = useState(null) + const [isSubmitting, setIsSubmitting] = useState(false) const [suggestions, setSuggestionsState] = useState(null) const setSuggestions = useCallback((s: PartialRelated | null) => { setSuggestionsState(s) @@ -121,8 +124,15 @@ export const ChatPanel = forwardRef(({ messages, i setInput('') clearAttachment() - const responseMessage = await submit(formData) - setMessages(currentMessages => [...currentMessages, responseMessage as any]) + setIsSubmitting(true) + onSubmitting?.(true) + try { + const responseMessage = await submit(formData) + setMessages(currentMessages => [...currentMessages, responseMessage as any]) + } finally { + setIsSubmitting(false) + onSubmitting?.(false) + } } const handleClear = async () => { @@ -177,6 +187,7 @@ export const ChatPanel = forwardRef(({ messages, i onClick={() => handleClear()} data-testid="new-chat-button" title="New Chat" + aria-label="New Chat" > @@ -225,6 +236,8 @@ export const ChatPanel = forwardRef(({ messages, i )} onClick={handleAttachmentClick} data-testid="desktop-attachment-button" + aria-label="Attach File" + title="Attach File" > @@ -281,11 +294,12 @@ export const ChatPanel = forwardRef(({ messages, i 'absolute top-1/2 transform -translate-y-1/2', isMobile ? 'right-1' : 'right-2' )} - disabled={input.length === 0 && !selectedFile} + disabled={(input.length === 0 && !selectedFile) || isSubmitting} aria-label="Send message" + title="Send Message" data-testid="chat-submit" > - + {isSubmitting ? : } @@ -295,7 +309,14 @@ export const ChatPanel = forwardRef(({ messages, i {selectedFile.name} - diff --git a/components/chat.tsx b/components/chat.tsx index e675f124..1c617891 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -38,6 +38,7 @@ export function Chat({ id }: ChatProps) { const [input, setInput] = useState('') const [showEmptyScreen, setShowEmptyScreen] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) + const [isSubmittingAction, setIsSubmittingAction] = useState(false) const [suggestions, setSuggestions] = useState(null) const chatPanelRef = useRef(null); @@ -132,7 +133,11 @@ export function Chat({ id }: ChatProps) { {activeView ? : isUsageOpen ? : }
- +
@@ -185,6 +191,7 @@ export function Chat({ id }: ChatProps) { input={input} setInput={setInput} onSuggestionsChange={setSuggestions} + onSubmitting={setIsSubmittingAction} />
diff --git a/components/header-search-button.tsx b/components/header-search-button.tsx index 04896457..e65e86f3 100644 --- a/components/header-search-button.tsx +++ b/components/header-search-button.tsx @@ -154,6 +154,7 @@ export function HeaderSearchButton() { onClick={handleResolutionSearch} disabled={isAnalyzing || !map || !actions} title="Analyze current map view" + aria-label="Analyze current map view" > {isAnalyzing ? (
diff --git a/components/header.tsx b/components/header.tsx index fd80bc44..97f92471 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -52,7 +52,14 @@ export const Header = () => {
-
- @@ -89,7 +109,13 @@ export const Header = () => { {/* Mobile menu buttons */}
- diff --git a/components/history.tsx b/components/history.tsx index 5bae1a39..b81d5f05 100644 --- a/components/history.tsx +++ b/components/history.tsx @@ -19,6 +19,8 @@ export function History({ location }: HistoryProps) { })} data-testid="history-button" onClick={toggleHistory} + aria-label="Toggle History" + title="Toggle History" > {location === 'header' ? : } diff --git a/components/mobile-icons-bar.tsx b/components/mobile-icons-bar.tsx index d0db2cfa..8fe259ff 100644 --- a/components/mobile-icons-bar.tsx +++ b/components/mobile-icons-bar.tsx @@ -19,13 +19,15 @@ import { MapToggle } from './map-toggle' import { ModeToggle } from './mode-toggle' import { ProfileToggle } from './profile-toggle' import { useCalendarToggle } from './calendar-toggle-context' +import { Spinner } from './ui/spinner' interface MobileIconsBarProps { onAttachmentClick: () => void; onSubmitClick: () => void; + isSubmitting?: boolean; } -export const MobileIconsBar: React.FC = ({ onAttachmentClick, onSubmitClick }) => { +export const MobileIconsBar: React.FC = ({ onAttachmentClick, onSubmitClick, isSubmitting }) => { const [, setMessages] = useUIState() const { clearChat } = useActions() const { toggleCalendar } = useCalendarToggle() @@ -37,27 +39,71 @@ export const MobileIconsBar: React.FC = ({ onAttachmentClic return (
- - - - - -