diff --git a/.jules/palette.md b/.jules/palette.md
index c05638899..ecba8a642 100644
--- a/.jules/palette.md
+++ b/.jules/palette.md
@@ -1,3 +1,7 @@
## 2024-05-19 - Replace HTML disabled with aria-disabled="true" for Accessible Tooltips
**Learning:** Native HTML `disabled` attributes completely hide elements from screen readers and block all pointer/hover events, preventing tooltips from functioning for disabled elements.
**Action:** Replace `disabled` with `aria-disabled="true"`, enforce block click handlers via `e.preventDefault()`, and add a title tooltip directly to the element to maintain full tooltip accessibility and keyboard focus support for visually impaired and mouse users.
+## 2026-09-24 - Accessible tooltips instead of title
+
+**Learning:** When adding tooltips to icon-only buttons, especially those using `aria-disabled={true}`, do not use the native HTML title attribute as it lacks keyboard/touch support.
+**Action:** Wrap the element in the custom Tooltip components (@/components/ui/tooltip).
diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx
index 3eed386f8..a454a3e58 100644
--- a/apps/desktop/src/App.test.tsx
+++ b/apps/desktop/src/App.test.tsx
@@ -225,7 +225,7 @@ describe("App", () => {
for (const name of ["Import", "Export"]) {
const navButton = within(primaryNav).getByRole("button", { name });
expect(navButton).toHaveAttribute("aria-disabled", "true");
- expect(navButton).toHaveAttribute("title", "Coming soon");
+ expect(navButton).not.toHaveAttribute("title");
expect(navButton).not.toBeDisabled();
}
fireEvent.click(within(primaryNav).getByRole("button", { name: "Import" }));
@@ -236,7 +236,7 @@ describe("App", () => {
for (const name of ["Import", "Export"]) {
const navButton = within(compactNav).getByRole("button", { name: `${name} compact view` });
expect(navButton).toHaveAttribute("aria-disabled", "true");
- expect(navButton).toHaveAttribute("title", "Coming soon");
+ expect(navButton).not.toHaveAttribute("title");
expect(navButton).not.toBeDisabled();
}
fireEvent.click(within(compactNav).getByRole("button", { name: "Import compact view" }));
@@ -1139,11 +1139,13 @@ describe("App", () => {
fireEvent.change(input, { target: { value: "https://youtube.com/watch?v=abc123DEF45" } });
const clearButton = screen.getByRole("button", { name: /Clear YouTube URL/i });
- clearButton.focus();
+ act(() => clearButton.focus());
fireEvent.click(clearButton);
expect(input).toHaveValue("");
- expect(document.activeElement).toBe(input);
+ await waitFor(() => {
+ expect(document.activeElement).toBe(input);
+ });
expect(screen.queryByRole("button", { name: /Clear YouTube URL/i })).toBeNull();
expect(screen.getByRole("alert")).toHaveTextContent(/choose a wav, mp3, flac, or m4a file/i);
expect(input).not.toHaveAttribute("aria-invalid");
diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx
index f3d678454..720966791 100644
--- a/apps/desktop/src/App.tsx
+++ b/apps/desktop/src/App.tsx
@@ -51,6 +51,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { Toaster } from "@/components/ui/sonner";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
const ANALYSIS_POLL_INTERVAL_MS = 250;
const MAX_ERROR_DETAIL_LENGTH = 220;
@@ -566,24 +567,25 @@ export function App() {
const { icon: Icon, view } = item;
return (
-
+