Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-05-23 - [Standardizing Icon Button Accessibility and Tooltips]
**Learning:** Icon-only buttons without labels or tooltips create "mystery meat navigation" and are inaccessible to screen readers. Standardizing on `Tooltip` for visual labels and `aria-label` for parity ensures a consistent and accessible UX across desktop and mobile.
**Action:** Always wrap new icon-only buttons in `Tooltip` (desktop) and ensure `aria-label` is present for all icon-only interactions.
Comment on lines +1 to +3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Markdown linting violations flagged by markdownlint-cli2

Two valid linter warnings apply:

  • MD041: The first line should be a top-level (#) heading; currently uses ##.
  • MD022: A blank line is required after a heading before body content.
♻️ Proposed fix
-## 2025-05-23 - [Standardizing Icon Button Accessibility and Tooltips]
-**Learning:** Icon-only buttons without labels or tooltips create "mystery meat navigation" and are inaccessible to screen readers. Standardizing on `Tooltip` for visual labels and `aria-label` for parity ensures a consistent and accessible UX across desktop and mobile.
-**Action:** Always wrap new icon-only buttons in `Tooltip` (desktop) and ensure `aria-label` is present for all icon-only interactions.
+# 2025-05-23 - [Standardizing Icon Button Accessibility and Tooltips]
+
+**Learning:** Icon-only buttons without labels or tooltips create "mystery meat navigation" and are inaccessible to screen readers. Standardizing on `Tooltip` for visual labels and `aria-label` for parity ensures a consistent and accessible UX across desktop and mobile.
+**Action:** Always wrap new icon-only buttons in `Tooltip` (desktop) and ensure `aria-label` is present for all icon-only interactions.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 2025-05-23 - [Standardizing Icon Button Accessibility and Tooltips]
**Learning:** Icon-only buttons without labels or tooltips create "mystery meat navigation" and are inaccessible to screen readers. Standardizing on `Tooltip` for visual labels and `aria-label` for parity ensures a consistent and accessible UX across desktop and mobile.
**Action:** Always wrap new icon-only buttons in `Tooltip` (desktop) and ensure `aria-label` is present for all icon-only interactions.
# 2025-05-23 - [Standardizing Icon Button Accessibility and Tooltips]
**Learning:** Icon-only buttons without labels or tooltips create "mystery meat navigation" and are inaccessible to screen readers. Standardizing on `Tooltip` for visual labels and `aria-label` for parity ensures a consistent and accessible UX across desktop and mobile.
**Action:** Always wrap new icon-only buttons in `Tooltip` (desktop) and ensure `aria-label` is present for all icon-only interactions.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 1-1: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.jules/palette.md around lines 1 - 3, Change the first heading from a
level-2 to a top-level heading and insert a blank line after it: replace "##
2025-05-23 - [Standardizing Icon Button Accessibility and Tooltips]" with "#
2025-05-23 - [Standardizing Icon Button Accessibility and Tooltips]" and add an
empty line between that heading and the following paragraph to satisfy MD041 and
MD022 for the content under the "Standardizing Icon Button Accessibility and
Tooltips" entry.

15 changes: 9 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { HistorySidebar } from '@/components/history-sidebar'
import { MapLoadingProvider } from '@/components/map-loading-context';
import ConditionalLottie from '@/components/conditional-lottie';
import { MapProvider as MapContextProvider } from '@/components/map/map-context'
import { TooltipProvider } from '@/components/ui/tooltip'

const fontSans = FontSans({
subsets: ['latin'],
Expand Down Expand Up @@ -109,12 +110,14 @@ export default function RootLayout({
>
<MapContextProvider>
<MapLoadingProvider>
<Header />
<ConditionalLottie />
{children}
<HistorySidebar />
<Footer />
<Toaster />
<TooltipProvider>
<Header />
<ConditionalLottie />
{children}
<HistorySidebar />
<Footer />
<Toaster />
</TooltipProvider>
</MapLoadingProvider>
</MapContextProvider>
</ThemeProvider>
Expand Down
69 changes: 43 additions & 26 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { PartialRelated } from '@/lib/schema/related'
import { getSuggestions } from '@/lib/actions/suggest'
import { useMapData } from './map/map-data-context'
import SuggestionsDropdown from './suggestions-dropdown'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'

interface ChatPanelProps {
messages: UIState
Expand Down Expand Up @@ -169,17 +170,22 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
'fixed bottom-4 left-4 flex justify-start items-center pointer-events-none z-50'
)}
>
<Button
type="button"
variant={'ghost'}
size={'icon'}
className="rounded-full transition-all hover:scale-110 pointer-events-auto text-primary"
onClick={() => handleClear()}
data-testid="new-chat-button"
title="New Chat"
>
<Sprout size={28} className="fill-primary/20" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant={'ghost'}
size={'icon'}
className="rounded-full transition-all hover:scale-110 pointer-events-auto text-primary"
onClick={() => handleClear()}
data-testid="new-chat-button"
aria-label="New Chat"
>
<Sprout size={28} className="fill-primary/20" />
</Button>
</TooltipTrigger>
<TooltipContent>New Chat</TooltipContent>
</Tooltip>
</div>
)
}
Expand Down Expand Up @@ -216,18 +222,24 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
accept="text/plain,image/png,image/jpeg,image/webp"
/>
{!isMobile && (
<Button
type="button"
variant={'ghost'}
size={'icon'}
className={cn(
'absolute top-1/2 transform -translate-y-1/2 left-3'
)}
onClick={handleAttachmentClick}
data-testid="desktop-attachment-button"
>
<Paperclip size={isMobile ? 18 : 20} />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant={'ghost'}
size={'icon'}
className={cn(
'absolute top-1/2 transform -translate-y-1/2 left-3'
)}
onClick={handleAttachmentClick}
data-testid="desktop-attachment-button"
aria-label="Attach File"
>
<Paperclip size={isMobile ? 18 : 20} />
</Button>
</TooltipTrigger>
<TooltipContent>Attach File</TooltipContent>
</Tooltip>
)}
<Textarea
ref={inputRef}
Expand Down Expand Up @@ -295,9 +307,14 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
<span className="text-sm text-muted-foreground truncate max-w-xs">
{selectedFile.name}
</span>
<Button variant="ghost" size="icon" onClick={clearAttachment} data-testid="clear-attachment-button">
<X size={16} />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={clearAttachment} data-testid="clear-attachment-button" aria-label="Clear attachment">
<X size={16} />
</Button>
</TooltipTrigger>
<TooltipContent>Clear attachment</TooltipContent>
</Tooltip>
Comment on lines +310 to +317
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Capitalization inconsistency: "Clear attachment" vs "Attach File"

TooltipContent and aria-label for the clear button use "Clear attachment" (lowercase "a") while the attach button uses "Attach File" (title case). Consider aligning to a single convention across all tooltips in this file:

-              <Button ... aria-label="Clear attachment">
+              <Button ... aria-label="Clear Attachment">
...
-              <TooltipContent>Clear attachment</TooltipContent>
+              <TooltipContent>Clear Attachment</TooltipContent>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={clearAttachment} data-testid="clear-attachment-button" aria-label="Clear attachment">
<X size={16} />
</Button>
</TooltipTrigger>
<TooltipContent>Clear attachment</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={clearAttachment} data-testid="clear-attachment-button" aria-label="Clear Attachment">
<X size={16} />
</Button>
</TooltipTrigger>
<TooltipContent>Clear Attachment</TooltipContent>
</Tooltip>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/chat-panel.tsx` around lines 310 - 317, The TooltipContent and
aria-label for the clear-attachment button are using "Clear attachment"
(lowercase "a") which is inconsistent with the attach button's "Attach File";
update the text to match the file's chosen convention (e.g., change
TooltipContent and aria-label in the Button that calls clearAttachment to "Clear
Attachment") so TooltipContent, the Button's aria-label and any related text use
the same capitalization style across the component.

</div>
</div>
)}
Expand Down
48 changes: 32 additions & 16 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useUsageToggle } from './usage-toggle-context'
import { useProfileToggle } from './profile-toggle-context'
import { useHistoryToggle } from './history-toggle-context'
import { useState, useEffect } from 'react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'

export const Header = () => {
const { toggleCalendar } = useCalendarToggle()
Expand Down Expand Up @@ -52,15 +53,20 @@ export const Header = () => {
</div>

<div className="absolute left-1 flex items-center">
<Button variant="ghost" size="icon" onClick={toggleHistory} data-testid="logo-history-toggle">
<Image
src="/images/logo.svg"
alt="Logo"
width={20}
height={20}
className="h-5 w-auto"
/>
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={toggleHistory} data-testid="logo-history-toggle" aria-label="Toggle history">
<Image
src="/images/logo.svg"
alt="Logo"
width={20}
height={20}
className="h-5 w-auto"
/>
</Button>
</TooltipTrigger>
<TooltipContent>History</TooltipContent>
</Tooltip>
Comment on lines +56 to +69

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tooltip content for the logo button is History, while the aria-label is Toggle history. This mismatch is minor but it can create inconsistent mental models (and makes it harder to keep analytics/testing documentation aligned). Prefer the tooltip text and aria-label to reflect the same action/label unless there's a strong reason to differ.

Suggestion

Align the tooltip label and aria-label so they describe the same control. For example:

<Button
  ...
  aria-label="History"
>
  ...
</Button>
<TooltipContent>History</TooltipContent>

Or, if the intent is explicitly toggling:

aria-label="Toggle history"
...
<TooltipContent>Toggle history</TooltipContent>

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that standardizes these labels across the header.

Comment on lines +56 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent pairing of aria-label and tooltip text

Two of the three wrapped buttons have mismatched accessible names and tooltip labels:

Button aria-label TooltipContent
Logo/history "Toggle history" "History"
Calendar "Open Calendar" "Calendar"
Usage "Usage" "Usage"

WCAG 2.5.3 (Label in Name) requires that the visible label text be present as a substring in the accessible name; both current cases technically satisfy this, but the inconsistency makes the pattern harder to follow and maintain. Recommend either aligning tooltip and aria-label verbatim, or adopting a consistent verb-noun form ("Toggle History", "Open Calendar") across both:

♻️ Proposed alignment
-            <Button ... aria-label="Toggle history">
+            <Button ... aria-label="History">
...
-          <TooltipContent>History</TooltipContent>
+          <TooltipContent>History</TooltipContent>
-            <Button ... aria-label="Open Calendar">
+            <Button ... aria-label="Calendar">
...
-          <TooltipContent>Calendar</TooltipContent>
+          <TooltipContent>Calendar</TooltipContent>

Also applies to: 80-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/header.tsx` around lines 56 - 69, The Tooltip/Button pairs use
inconsistent accessible names: align the visible tooltip text (TooltipContent)
with the button aria-labels for consistency; update the Logo/history
TooltipContent to match the Button aria-label "Toggle history" (or change the
aria-label to "History") and likewise make the Calendar pair consistent between
"Open Calendar" and "Calendar" so all TooltipContent strings and Button
aria-label values follow the same verb-noun pattern; locate the elements using
Tooltip, TooltipTrigger, Button (the logo's onClick toggleHistory) and
TooltipContent to make the changes.

<h1 className="text-2xl font-poppins font-semibold text-primary">
QCX
</h1>
Expand All @@ -71,15 +77,25 @@ export const Header = () => {

<MapToggle />

<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="calendar-toggle">
<CalendarDays className="h-[1.2rem] w-[1.2rem]" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={toggleCalendar} data-testid="calendar-toggle" aria-label="Open Calendar">
<CalendarDays className="h-[1.2rem] w-[1.2rem]" />
</Button>
</TooltipTrigger>
<TooltipContent>Calendar</TooltipContent>
</Tooltip>

<div id="header-search-portal" className="contents" />

<Button variant="ghost" size="icon" onClick={handleUsageToggle}>
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={handleUsageToggle} aria-label="Usage">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
</TooltipTrigger>
<TooltipContent>Usage</TooltipContent>
</Tooltip>

<ModeToggle />

Expand All @@ -89,7 +105,7 @@ export const Header = () => {
{/* Mobile menu buttons */}
<div className="flex md:hidden gap-2">

<Button variant="ghost" size="icon" onClick={handleUsageToggle}>
<Button variant="ghost" size="icon" onClick={handleUsageToggle} aria-label="Usage">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
<ProfileToggle/>
Expand Down
12 changes: 6 additions & 6 deletions components/mobile-icons-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,26 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic

return (
<div className="mobile-icons-bar-content">
<Button variant="ghost" size="icon" onClick={handleNewChat} data-testid="mobile-new-chat-button">
<Button variant="ghost" size="icon" onClick={handleNewChat} data-testid="mobile-new-chat-button" aria-label="New Chat">
<Plus className="h-[1.2rem] w-[1.2rem]" />
</Button>
<ProfileToggle />
<MapToggle />
<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="mobile-calendar-button">
<Button variant="ghost" size="icon" onClick={toggleCalendar} aria-label="Open Calendar" data-testid="mobile-calendar-button">
<CalendarDays className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<Button variant="ghost" size="icon" data-testid="mobile-search-button">
<Button variant="ghost" size="icon" data-testid="mobile-search-button" aria-label="Search">
<Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
Comment on lines +48 to 50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Search button is non-functional — labelled but missing onClick

The button has aria-label="Search" but no onClick handler. Keyboard and screen reader users will encounter a reachable control that silently does nothing when activated, which is worse from an accessibility perspective than an unlabelled button that at least fails visually. Either wire up the search handler here or remove the button until the feature is implemented.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/mobile-icons-bar.tsx` around lines 48 - 50, The Search Button in
mobile-icons-bar.tsx is labelled but has no onClick, causing an accessible
control that does nothing; either wire it to the search-opening handler or
remove it until implemented — add an onClick to the Button (e.g.,
onClick={props.onOpenSearch} or onClick={() => openSearchModal()}), ensure the
component accepts/forwards a handler prop (e.g., onOpenSearch) if needed, and
keep the aria-label and testid intact so keyboard and screen-reader users
activate the real action instead of a no-op.

<a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<Button variant="ghost" size="icon" aria-label="Purchase Credits">
<TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
</a>
Comment on lines 51 to 55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purchase credits button is wrapped in an <a> with target="_blank", and the inner Button now has an aria-label. Make sure the accessible name is attached to the actual interactive element receiving focus/click events (often the anchor), and avoid nested interactive semantics if your Button renders a <button> by default. If Button renders a <button>, this becomes invalid/awkward semantics inside an <a>.

Suggestion

If Button supports asChild, consider moving the anchor inside the button for correct semantics:

<Button asChild variant="ghost" size="icon" aria-label="Purchase credits">
  <a href="..." target="_blank" rel="noopener noreferrer">
    <TentTree ... />
  </a>
</Button>

Alternatively, add the aria-label to the <a> and ensure the inner component is not a <button>. Reply with "@CharlieHelps yes please" if you'd like me to adjust this safely based on how Button is implemented in this repo.

Comment on lines 51 to 55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

aria-label is on the inner <Button>, not on the wrapping <a> — both elements are focusable

The new aria-label="Purchase Credits" is on the <button> element rendered by <Button>, but the outer <a> is also a focusable landmark with no label. Some assistive technology + browser combinations will tab-stop on both elements separately. The idiomatic fix is to use the anchor itself as the interactive element (e.g., via Button with asChild wrapping an <a>) so there is a single focusable node:

♻️ Suggested structure
-      <a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer">
-        <Button variant="ghost" size="icon" aria-label="Purchase Credits">
-          <TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
-        </Button>
-      </a>
+      <Button variant="ghost" size="icon" aria-label="Purchase Credits" asChild>
+        <a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer">
+          <TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
+        </a>
+      </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/mobile-icons-bar.tsx` around lines 51 - 55, The anchor and the
Button are both focusable, leaving the outer <a> unlabeled and causing duplicate
tab stops; make the anchor the single interactive element by using the Button’s
asChild prop (or otherwise ensuring only the <a> is focusable) so the accessible
name is on the anchor. Specifically, replace the current <a><Button ...
aria-label="Purchase Credits">...</Button></a> pattern with <Button asChild
variant="ghost" size="icon"><a href="..." target="_blank" rel="noopener
noreferrer" aria-label="Purchase Credits"><TentTree ... /></a></Button>, so the
anchor carries the aria-label and there is only one focusable element.

<Button variant="ghost" size="icon" onClick={onAttachmentClick} data-testid="mobile-attachment-button">
<Button variant="ghost" size="icon" onClick={onAttachmentClick} data-testid="mobile-attachment-button" aria-label="Attach File">
<Paperclip className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<Button variant="ghost" size="icon" data-testid="mobile-submit-button" onClick={onSubmitClick}>
<Button variant="ghost" size="icon" data-testid="mobile-submit-button" onClick={onSubmitClick} aria-label="Send Message">
<ArrowRight className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<History location="header" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, confirm History component has no aria-label or role annotation
echo "=== Checking for aria-label in History-related files ==="
rg -n "aria-label" components/history.tsx components/history-container.tsx 2>/dev/null || echo "No aria-label found"
echo ""
echo "=== Examining History component structure ==="
cat -n components/history.tsx | head -100

Repository: QueueLab/QCX

Length of output: 1045


🏁 Script executed:

#!/bin/bash
# Check the mobile-icons-bar.tsx file to see other buttons and their aria-labels
echo "=== Mobile Icons Bar - checking icon buttons structure ==="
cat -n components/mobile-icons-bar.tsx

Repository: QueueLab/QCX

Length of output: 3176


🏁 Script executed:

#!/bin/bash
# Check HistoryContainer implementation
echo "=== History Container ==="
cat -n components/history-container.tsx

Repository: QueueLab/QCX

Length of output: 538


Add aria-label support to the History component

The History component renders a <Button> icon with no aria-label prop support. Every other icon button in MobileIconsBar has an aria-label, but the History button at line 62 is missing one. Update the History component to accept and apply an aria-label prop to make it accessible to screen readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/mobile-icons-bar.tsx` at line 62, The History icon usage in
MobileIconsBar lacks an aria-label because the History component doesn't accept
or forward one; update the History component to accept an optional ariaLabel (or
aria-label) prop in its props signature and pass it through to the rendered
<Button> (or IconButton) element (e.g., <Button aria-label={ariaLabel} ...>) so
screen readers receive the label; ensure the prop name matches other icon
components and update any internal prop destructuring in the History component
to forward the aria attribute.

Expand Down