From d1659c724b36db945568c33cfa6d2e00eccef3c3 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 30 Aug 2026 04:04:28 +0000 Subject: [PATCH 01/13] feat: improve score viewer disabled button accessibility --- .../src/features/score/ScoreViewer.test.tsx | 24 +++++++++++++++---- .../src/features/score/ScoreViewer.tsx | 22 +++++++++++++---- apps/desktop/src/locales/en/common.json | 2 ++ apps/desktop/src/locales/ko/common.json | 2 ++ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/features/score/ScoreViewer.test.tsx b/apps/desktop/src/features/score/ScoreViewer.test.tsx index 3ac2dd605..67d2b6181 100644 --- a/apps/desktop/src/features/score/ScoreViewer.test.tsx +++ b/apps/desktop/src/features/score/ScoreViewer.test.tsx @@ -16,7 +16,9 @@ vi.mock("../../i18n", () => ({ scoreViewerFailedTitle: "Could not display the score", scoreViewerRetry: "Retry", scoreViewerPrevPage: "Previous page", + scoreViewerPrevPageDisabled: "Already on the first page", scoreViewerNextPage: "Next page", + scoreViewerNextPageDisabled: "Already on the last page", scoreViewerPageIndicator: "Page {current} of {total}", scoreViewerZoomIn: "Zoom in", scoreViewerZoomOut: "Zoom out", @@ -120,8 +122,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" })).toHaveAttribute("aria-disabled", "false"); }); it("shows the file name when provided", async () => { @@ -174,14 +176,28 @@ 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"); + + const eventSpy = vi.spyOn(Event.prototype, "preventDefault"); + + // clicking an aria-disabled button calls preventDefault and ignores the action + fireEvent.click(previousButton); + expect(eventSpy).toHaveBeenCalled(); + expect(screen.getByText("Page 1 of 3")).toBeInTheDocument(); + eventSpy.mockClear(); 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"); + + // clicking an aria-disabled button calls preventDefault and ignores the action + fireEvent.click(nextButton); + expect(eventSpy).toHaveBeenCalled(); + expect(screen.getByText("Page 3 of 3")).toBeInTheDocument(); + eventSpy.mockRestore(); 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 82692469e..bb9bd5490 100644 --- a/apps/desktop/src/features/score/ScoreViewer.tsx +++ b/apps/desktop/src/features/score/ScoreViewer.tsx @@ -292,8 +292,15 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps size="icon-lg" className="size-14" aria-label={t("scoreViewerPrevPage")} - disabled={pageNumber <= 1} - onClick={goToPreviousPage} + title={pageNumber <= 1 ? t("scoreViewerPrevPageDisabled") : undefined} + aria-disabled={pageNumber <= 1} + onClick={(e) => { + if (pageNumber <= 1) { + e.preventDefault(); + return; + } + goToPreviousPage(); + }} >