From 4127cd7d7c541321849006c071602ed8f4d22977 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 4 Sep 2026 04:05:34 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20titles=20to=20d?= =?UTF-8?q?isabled=20navigation=20buttons=20in=20ScoreViewer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/palette.md | 3 +++ apps/desktop/src/features/score/ScoreViewer.test.tsx | 12 ++++++++---- apps/desktop/src/features/score/ScoreViewer.tsx | 10 ++++++---- apps/desktop/src/locales/en/common.json | 4 +++- apps/desktop/src/locales/ko/common.json | 4 +++- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index c05638899..a01eb1d66 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -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. +## 2023-10-27 - Added disabled titles to ScoreViewer navigation +**Learning:** Added tooltips explicitly explaining *why* navigation buttons in ScoreViewer are disabled (e.g. "Already at the first page" or "Already at the last page"), which provides significantly better a11y context than leaving them silently disabled, matching the UX guidance pattern. Also modified tests accordingly to assert for the right translated keys. +**Action:** When converting HTML `disabled` attributes to `aria-disabled="true"` for interactive elements, always consider if there is an opportunity to communicate the reason for the disabled state to the user via a `title` or `aria-describedby` attribute, and ensure translations and testing are properly set up for this specific state. diff --git a/apps/desktop/src/features/score/ScoreViewer.test.tsx b/apps/desktop/src/features/score/ScoreViewer.test.tsx index 3ac2dd605..84ec67fd3 100644 --- a/apps/desktop/src/features/score/ScoreViewer.test.tsx +++ b/apps/desktop/src/features/score/ScoreViewer.test.tsx @@ -120,8 +120,10 @@ 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: "Previous page" })).toHaveAttribute("title", "scoreViewerAlreadyFirstPage"); + expect(screen.getByRole("button", { name: "Next page" })).not.toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("button", { name: "Next page" })).not.toHaveAttribute("title", "scoreViewerAlreadyLastPage"); }); it("shows the file name when provided", async () => { @@ -174,14 +176,16 @@ 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"); + expect(previousButton).toHaveAttribute("title", "scoreViewerAlreadyFirstPage"); 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"); + expect(nextButton).toHaveAttribute("title", "scoreViewerAlreadyLastPage"); 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..638e4ba95 100644 --- a/apps/desktop/src/features/score/ScoreViewer.tsx +++ b/apps/desktop/src/features/score/ScoreViewer.tsx @@ -292,8 +292,9 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps size="icon-lg" className="size-14" aria-label={t("scoreViewerPrevPage")} - disabled={pageNumber <= 1} - onClick={goToPreviousPage} + aria-disabled={pageNumber <= 1 ? true : undefined} + title={pageNumber <= 1 ? t("scoreViewerAlreadyFirstPage") : undefined} + onClick={pageNumber <= 1 ? undefined : goToPreviousPage} >