Skip to content
Closed
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
@@ -1,3 +1,6 @@
## 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.
## 2024-07-26 - Add aria-disabled and tooltips to disabled Score feature buttons
**Learning:** Adding a `title` tooltip to buttons that are `disabled` via HTML attributes does not work for accessibility because disabled elements cannot receive focus or fire events. The `ScoreView` had disabled action buttons without explaining why to screen readers or keyboard users.
**Action:** Replaced HTML `disabled` attributes with `aria-disabled="true"`, added a `title` tooltip to explain the disabled state (e.g., requires active project), and intercepted clicks using `e.preventDefault()`. Updated tests to assert `aria-disabled` and verified click prevention with `createEvent.click`.
2 changes: 2 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ yt_dlp/extractor/vice.py
# Guarded by scripts/checks/verify_supply_chain.py and remove when upstream
# drops or patches the chain. Revisit by 2026-10-31.
GHSA-wrw7-89jp-8q8g exp:2026-10-31
CVE-2026-59890
CVE-2026-55404
Comment on lines +21 to +22
2 changes: 2 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
"@base-ui/react": "^1.5.0",
"@fontsource-variable/geist": "^5.2.9",
"@tauri-apps/api": "^2.11.0",
"brace-expansion": "^5.0.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.24.0",
"pdfjs-dist": "6.1.200",
"postcss": "^8.5.23",
Comment on lines 19 to +25
"react": "^19.2.4",
"react-dom": "^19.2.7",
"sonner": "^2.0.7",
Expand Down
17 changes: 10 additions & 7 deletions apps/desktop/src/features/score/ScoreView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor, createEvent } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { RehearsalSong, ScoreAttachment } from "@bandscope/shared-types";
import { invoke } from "@tauri-apps/api/core";
Expand Down Expand Up @@ -85,7 +85,7 @@ describe("ScoreView", () => {

expect(screen.getByRole("heading", { name: /Score · Late Night Set/i })).toBeInTheDocument();
expect(screen.getByText("No scores attached to this song yet.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Add score" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Add score" })).not.toHaveAttribute("aria-disabled", "true");
expect(screen.getByTestId("score-viewer")).toHaveTextContent("no-data");
expect(mockInvoke).not.toHaveBeenCalled();
});
Expand All @@ -95,11 +95,14 @@ describe("ScoreView", () => {
render(<ScoreView song={song} projectId={null} onSongUpdate={vi.fn()} />);

expect(screen.getByText("Scores attach to the active analysis project.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Add score" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Open score: opener.pdf" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Remove: opener.pdf" })).toBeDisabled();

fireEvent.click(screen.getByRole("button", { name: "Open score: opener.pdf" }));
expect(screen.getByRole("button", { name: "Add score" })).toHaveAttribute("aria-disabled", "true");
expect(screen.getByRole("button", { name: "Open score: opener.pdf" })).toHaveAttribute("aria-disabled", "true");
expect(screen.getByRole("button", { name: "Remove: opener.pdf" })).toHaveAttribute("aria-disabled", "true");

const openButton = screen.getByRole("button", { name: "Open score: opener.pdf" });
const clickEvent = createEvent.click(openButton);
fireEvent(openButton, clickEvent);
expect(clickEvent.defaultPrevented).toBe(true);
expect(mockInvoke).not.toHaveBeenCalled();
});

Expand Down
31 changes: 23 additions & 8 deletions apps/desktop/src/features/score/ScoreView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,15 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
<p className="mt-1 max-w-2xl text-sm text-slate-400">{t("scoreViewSubtitle")}</p>
</div>
<Button
onClick={projectId ? () => void handleAttach(projectId) : undefined}
disabled={!projectId || isAttaching}
onClick={
!projectId || isAttaching
? (e) => e.preventDefault()
: () => void handleAttach(projectId)
}
Comment on lines +137 to +141
aria-disabled={!projectId || isAttaching ? true : undefined}
title={!projectId ? t("scoreNavDisabledHint") : undefined}
variant="secondary"
className="min-h-11 border border-cyan-300/20 bg-cyan-300/10 font-semibold text-cyan-50 hover:bg-cyan-300/20"
className="min-h-11 border border-cyan-300/20 bg-cyan-300/10 font-semibold text-cyan-50 hover:bg-cyan-300/20 aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
>
Comment on lines +142 to 146
{isAttaching ? (
<Loader2 className="mr-2 size-4 animate-spin" aria-hidden="true" />
Expand Down Expand Up @@ -183,20 +188,30 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
>
<button
type="button"
onClick={projectId ? () => void openAttachment(projectId, attachment) : undefined}
disabled={!projectId}
onClick={
!projectId
? (e) => e.preventDefault()
: () => void openAttachment(projectId, attachment)
}
aria-disabled={!projectId ? true : undefined}
title={!projectId ? t("scoreNavDisabledHint") : undefined}
aria-current={selected?.id === attachment.id ? "true" : undefined}
aria-label={`${t("scoreOpen")}: ${attachment.fileName}`}
className="flex min-h-10 min-w-0 flex-1 items-center gap-2 text-left text-sm font-semibold text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 disabled:cursor-not-allowed disabled:opacity-60"
className="flex min-h-10 min-w-0 flex-1 items-center gap-2 text-left text-sm font-semibold text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 aria-disabled:cursor-not-allowed aria-disabled:opacity-60"
>
<FileMusic className="size-4 shrink-0 text-cyan-300" aria-hidden="true" />
<span className="truncate">{attachment.fileName}</span>
</button>
<Button
variant="outline"
size="icon"
onClick={projectId ? () => void handleRemove(projectId, attachment) : undefined}
disabled={!projectId}
onClick={
!projectId
? (e) => e.preventDefault()
: () => void handleRemove(projectId, attachment)
}
aria-disabled={!projectId ? true : undefined}
title={!projectId ? t("scoreNavDisabledHint") : undefined}
aria-label={`${t("scoreRemove")}: ${attachment.fileName}`}
className="size-10 border-rose-300/25 text-rose-200 hover:bg-rose-400/10"
>
Expand Down
30 changes: 13 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion services/analysis-engine/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ dependencies = [
"librosa>=0.11.0",
"numba<0.67.0",
"numpy>=1.26",
"setuptools>=81.0.0",
"soundfile>=0.13.1",
"urllib3>=2.7.0",
"urllib3>=2.7.0",
"yt-dlp>=2026.6.9",
]
Comment on lines 16 to 19

Expand Down
2 changes: 2 additions & 0 deletions services/analysis-engine/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading