Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -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).
10 changes: 6 additions & 4 deletions apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" }));
Expand All @@ -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" }));
Expand Down Expand Up @@ -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");
Expand Down
141 changes: 75 additions & 66 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -566,24 +567,25 @@ export function App() {
const { icon: Icon, view } = item;

return (
<button
key={item.labelKey}
type="button"
aria-current={active ? "page" : undefined}
aria-disabled={enabled ? undefined : true}
title={title}
onClick={enabled && view ? () => handleNavSelect(view) : blockInactiveNavActivation}
className={`flex min-h-11 w-full items-center gap-3 rounded-xl px-3 text-left text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${
active
? "bg-blue-600/70 text-white shadow-[0_12px_30px_rgba(37,99,235,0.32)]"
: enabled
? "text-slate-200 hover:bg-white/5"
: "cursor-not-allowed text-slate-500 opacity-70"
}`}
>
<Icon className="size-5" aria-hidden="true" />
{label}
</button>
<Tooltip key={item.labelKey}>
<TooltipTrigger
type="button"
aria-current={active ? "page" : undefined}
aria-disabled={enabled ? undefined : true}
onClick={enabled && view ? () => handleNavSelect(view) : blockInactiveNavActivation}
className={`flex min-h-11 w-full items-center gap-3 rounded-xl px-3 text-left text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${
active
? "bg-blue-600/70 text-white shadow-[0_12px_30px_rgba(37,99,235,0.32)]"
: enabled
? "text-slate-200 hover:bg-white/5"
: "cursor-not-allowed text-slate-500 opacity-70"
}`}
>
<Icon className="size-5" aria-hidden="true" />
{label}
</TooltipTrigger>
{title ? <TooltipContent side="right">{title}</TooltipContent> : null}
</Tooltip>
);
})}
</nav>
Expand Down Expand Up @@ -611,26 +613,30 @@ export function App() {
</div>

<div className="flex items-center justify-between text-slate-400">
<button
type="button"
aria-disabled={true}
aria-label={t("settingsComingSoon")}
title={t("settingsComingSoon")}
onClick={preventUnavailableAction}
className="inline-flex cursor-not-allowed items-center justify-center rounded-xl p-2 text-slate-600 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<Settings className="size-5" aria-hidden="true" />
</button>
<button
type="button"
aria-disabled={true}
aria-label={t("helpComingSoon")}
title={t("helpComingSoon")}
onClick={preventUnavailableAction}
className="inline-flex cursor-not-allowed items-center justify-center rounded-xl p-2 text-slate-600 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<CircleHelp className="size-5" aria-hidden="true" />
</button>
<Tooltip>
<TooltipTrigger
type="button"
aria-disabled={true}
aria-label={t("settingsComingSoon")}
onClick={preventUnavailableAction}
className="inline-flex cursor-not-allowed items-center justify-center rounded-xl p-2 text-slate-600 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<Settings className="size-5" aria-hidden="true" />
</TooltipTrigger>
<TooltipContent>{t("settingsComingSoon")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
type="button"
aria-disabled={true}
aria-label={t("helpComingSoon")}
onClick={preventUnavailableAction}
className="inline-flex cursor-not-allowed items-center justify-center rounded-xl p-2 text-slate-600 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<CircleHelp className="size-5" aria-hidden="true" />
</TooltipTrigger>
<TooltipContent>{t("helpComingSoon")}</TooltipContent>
</Tooltip>
</div>
</div>
</aside>
Expand All @@ -642,25 +648,26 @@ export function App() {
const { icon: Icon, view } = item;

return (
<button
key={item.labelKey}
type="button"
aria-current={active ? "page" : undefined}
aria-label={`${label} ${t("compactViewSuffix")}`}
aria-disabled={enabled ? undefined : true}
title={title}
onClick={enabled && view ? () => handleNavSelect(view) : blockInactiveNavActivation}
className={`inline-flex min-h-10 shrink-0 items-center gap-2 rounded-xl px-3 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${
active
? "bg-blue-600/70 text-white"
: enabled
? "text-slate-200 hover:bg-white/5"
: "cursor-not-allowed text-slate-500 opacity-70"
}`}
>
<Icon className="size-4" aria-hidden="true" />
{label}
</button>
<Tooltip key={item.labelKey}>
<TooltipTrigger
type="button"
aria-current={active ? "page" : undefined}
aria-label={`${label} ${t("compactViewSuffix")}`}
aria-disabled={enabled ? undefined : true}
onClick={enabled && view ? () => handleNavSelect(view) : blockInactiveNavActivation}
className={`inline-flex min-h-10 shrink-0 items-center gap-2 rounded-xl px-3 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${
active
? "bg-blue-600/70 text-white"
: enabled
? "text-slate-200 hover:bg-white/5"
: "cursor-not-allowed text-slate-500 opacity-70"
}`}
>
<Icon className="size-4" aria-hidden="true" />
{label}
</TooltipTrigger>
{title ? <TooltipContent side="bottom">{title}</TooltipContent> : null}
</Tooltip>
);
})}
</nav>
Expand Down Expand Up @@ -709,15 +716,17 @@ export function App() {
aria-describedby={selectionError && selectionErrorSource === "youtube" ? "selection-error" : undefined}
/>
{youtubeUrl && !analysisInFlight && !isStarting && !isImporting ? (
<button
type="button"
onClick={handleClearYoutubeUrl}
className="absolute right-1 top-1/2 inline-flex size-8 -translate-y-1/2 items-center justify-center rounded-md text-slate-400 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
aria-label={t("clearYoutubeUrl")}
title={t("clearYoutubeUrl")}
>
<X className="size-4" aria-hidden="true" />
</button>
<Tooltip>
<TooltipTrigger
type="button"
onClick={handleClearYoutubeUrl}
className="absolute right-1 top-1/2 inline-flex size-8 -translate-y-1/2 items-center justify-center rounded-md text-slate-400 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
aria-label={t("clearYoutubeUrl")}
>
<X className="size-4" aria-hidden="true" />
</TooltipTrigger>
<TooltipContent>{t("clearYoutubeUrl")}</TooltipContent>
</Tooltip>
) : null}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1275,9 +1275,7 @@ def test_workflow_concurrency_cancels_only_superseded_pr_heads() -> None:
workflow = (workflows_dir / workflow_name).read_text(encoding="utf-8")
assert "concurrency:" in workflow, workflow_name
assert "cancel-in-progress: false" in workflow, workflow_name
assert "contents: read" in workflow or "permissions: read-all" in workflow, (
workflow_name
)
assert "contents: read" in workflow or "permissions: read-all" in workflow, workflow_name

assert "pull_request:" not in (workflows_dir / "release.yml").read_text(encoding="utf-8")

Expand Down
Loading