From 31f0bcd7f12dd119d636052d6968f07d48cabf45 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Mon, 20 Jul 2026 14:30:28 +0000
Subject: [PATCH] =?UTF-8?q?feat:=20=EC=95=85=EB=B3=B4=20=EB=B7=B0=EC=96=B4?=
=?UTF-8?q?=EC=9D=98=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EB=B2=84=ED=8A=BC?=
=?UTF-8?q?=EC=97=90=20=ED=88=B4=ED=8C=81=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?=
=?UTF-8?q?=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20=EC=83=81=ED=83=9C=20?=
=?UTF-8?q?=EC=86=8D=EC=84=B1=EC=9D=84=20=EC=A0=91=EA=B7=BC=EC=84=B1?=
=?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.Jules/palette.md | 4 ++++
apps/desktop/src/features/score/ScoreView.test.tsx | 6 +++---
apps/desktop/src/features/score/ScoreView.tsx | 13 ++++++++-----
.../desktop/src/features/score/ScoreViewer.test.tsx | 8 ++++----
apps/desktop/src/features/score/ScoreViewer.tsx | 13 +++++++++----
5 files changed, 28 insertions(+), 16 deletions(-)
diff --git a/.Jules/palette.md b/.Jules/palette.md
index 5c1c1698..033000b7 100644
--- a/.Jules/palette.md
+++ b/.Jules/palette.md
@@ -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 ` `) 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 `` component or native `` 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.
diff --git a/apps/desktop/src/features/score/ScoreView.test.tsx b/apps/desktop/src/features/score/ScoreView.test.tsx
index de4ccb95..371e99f2 100644
--- a/apps/desktop/src/features/score/ScoreView.test.tsx
+++ b/apps/desktop/src/features/score/ScoreView.test.tsx
@@ -95,9 +95,9 @@ describe("ScoreView", () => {
render( );
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();
diff --git a/apps/desktop/src/features/score/ScoreView.tsx b/apps/desktop/src/features/score/ScoreView.tsx
index 72732450..cf1a4348 100644
--- a/apps/desktop/src/features/score/ScoreView.tsx
+++ b/apps/desktop/src/features/score/ScoreView.tsx
@@ -134,8 +134,9 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
{t("scoreViewSubtitle")}
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"
>
@@ -184,10 +185,11 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
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"
>
{attachment.fileName}
@@ -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"
>
diff --git a/apps/desktop/src/features/score/ScoreViewer.test.tsx b/apps/desktop/src/features/score/ScoreViewer.test.tsx
index 3ac2dd60..9b9d89db 100644
--- a/apps/desktop/src/features/score/ScoreViewer.test.tsx
+++ b/apps/desktop/src/features/score/ScoreViewer.test.tsx
@@ -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 () => {
@@ -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);
diff --git a/apps/desktop/src/features/score/ScoreViewer.tsx b/apps/desktop/src/features/score/ScoreViewer.tsx
index 82692469..d09decd7 100644
--- a/apps/desktop/src/features/score/ScoreViewer.tsx
+++ b/apps/desktop/src/features/score/ScoreViewer.tsx
@@ -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}
>
@@ -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}
>
@@ -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}
>
@@ -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}
>
@@ -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}
>