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
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@
## 2026-07-02 - Inline clear buttons preserve focus
**Learning:** Inline clear buttons often unmount immediately after clearing state, which can drop keyboard focus to the document body.
**Action:** Move focus back to the owning input before clearing state, and cover the behavior with a DOM focus test.

## 2026-07-20 - Adding aria-disabled styles to UI primitive buttons
**Learning:** While replacing the native `disabled` attribute with `aria-disabled="true"` correctly enables pointer events for tooltips and preserves keyboard tab order, custom primitive components (like `<Button />`) often rely on the native `disabled:` pseudoclass for their visual state (e.g., opacity and cursor). If the base component styles aren't updated, they may visually appear fully enabled when only `aria-disabled` is used.
**Action:** When updating a custom `<Button>` component or native `<button>` element to use `aria-disabled="true"`, ensure its base Tailwind styles or variants also include corresponding `aria-disabled:` classes (like `aria-disabled:opacity-50 aria-disabled:cursor-not-allowed`) so that it visually reflects its disabled state.
6 changes: 3 additions & 3 deletions apps/desktop/src/features/score/ScoreView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ 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();
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");

fireEvent.click(screen.getByRole("button", { name: "Open score: opener.pdf" }));
expect(mockInvoke).not.toHaveBeenCalled();
Expand Down
13 changes: 8 additions & 5 deletions apps/desktop/src/features/score/ScoreView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ 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 ? () => void handleAttach(projectId) : undefined}
aria-disabled={!projectId || isAttaching ? "true" : undefined}
title={!projectId ? t("scoreRequiresProject") : 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"
>
Expand Down Expand Up @@ -184,10 +185,11 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
<button
type="button"
onClick={projectId ? () => void openAttachment(projectId, attachment) : undefined}
disabled={!projectId}
aria-disabled={!projectId ? "true" : undefined}
title={!projectId ? t("scoreRequiresProject") : 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>
Expand All @@ -196,7 +198,8 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
variant="outline"
size="icon"
onClick={projectId ? () => void handleRemove(projectId, attachment) : undefined}
disabled={!projectId}
aria-disabled={!projectId ? "true" : undefined}
title={!projectId ? t("scoreRequiresProject") : undefined}
aria-label={`${t("scoreRemove")}: ${attachment.fileName}`}
className="size-10 border-rose-300/25 text-rose-200 hover:bg-rose-400/10"
>
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/features/score/ScoreViewer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ describe("ScoreViewer", () => {
expect(page.render).toHaveBeenCalled();
});
expect(page.getViewport).toHaveBeenCalledWith({ scale: 1 });
expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Next page" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Previous page" })).toHaveAttribute("aria-disabled", "true");
expect(screen.getByRole("button", { name: "Next page" })).not.toHaveAttribute("aria-disabled");
});

it("shows the file name when provided", async () => {
Expand Down Expand Up @@ -174,14 +174,14 @@ describe("ScoreViewer", () => {
expect(await screen.findByText("Page 1 of 3")).toBeInTheDocument();
const previousButton = screen.getByRole("button", { name: "Previous page" });
const nextButton = screen.getByRole("button", { name: "Next page" });
expect(previousButton).toBeDisabled();
expect(previousButton).toHaveAttribute("aria-disabled", "true");

fireEvent.click(nextButton);
expect(screen.getByText("Page 2 of 3")).toBeInTheDocument();

fireEvent.click(nextButton);
expect(screen.getByText("Page 3 of 3")).toBeInTheDocument();
expect(nextButton).toBeDisabled();
expect(nextButton).toHaveAttribute("aria-disabled", "true");

await waitFor(() => {
expect(doc.getPage).toHaveBeenCalledWith(3);
Expand Down
13 changes: 9 additions & 4 deletions apps/desktop/src/features/score/ScoreViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
size="icon-lg"
className="size-12"
aria-label={t("scoreViewerZoomOut")}
title={t("scoreViewerZoomOut")}
onClick={zoomOut}
>
<ZoomOut aria-hidden="true" />
Expand All @@ -267,6 +268,7 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
size="icon-lg"
className="size-12"
aria-label={t("scoreViewerZoomIn")}
title={t("scoreViewerZoomIn")}
onClick={zoomIn}
>
<ZoomIn aria-hidden="true" />
Expand All @@ -275,6 +277,7 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
variant={fitWidth ? "secondary" : "outline"}
className="h-12 px-4 text-base"
aria-label={t("scoreViewerFitWidth")}
title={t("scoreViewerFitWidth")}
aria-pressed={fitWidth}
onClick={fitToWidth}
>
Expand All @@ -292,8 +295,9 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
size="icon-lg"
className="size-14"
aria-label={t("scoreViewerPrevPage")}
disabled={pageNumber <= 1}
onClick={goToPreviousPage}
title={t("scoreViewerPrevPage")}
aria-disabled={pageNumber <= 1 ? "true" : undefined}
onClick={pageNumber <= 1 ? undefined : goToPreviousPage}
>
<ChevronLeft className="size-6" aria-hidden="true" />
</Button>
Expand All @@ -305,8 +309,9 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
size="icon-lg"
className="size-14"
aria-label={t("scoreViewerNextPage")}
disabled={pageNumber >= pageCount}
onClick={goToNextPage}
title={t("scoreViewerNextPage")}
aria-disabled={pageNumber >= pageCount ? "true" : undefined}
onClick={pageNumber >= pageCount ? undefined : goToNextPage}
>
<ChevronRight className="size-6" aria-hidden="true" />
</Button>
Expand Down
Loading