diff --git a/.jules/palette.md b/.jules/palette.md index c0563889..83b77647 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. +## 2024-07-28 - Validate `aria-disabled` styling on UI components +**Learning:** When switching from native `disabled` to `aria-disabled` for better tooltip support and screen reader context, it's crucial to verify if the UI component definitions (e.g., using `cva` in Tailwind) actually support the `aria-disabled:` variants. In `button.tsx`, `aria-disabled:opacity-50` and `aria-disabled:cursor-not-allowed` were already present, ensuring visual regressions didn't occur. Wrapping elements in `` is not a good practice as it creates invalid nested interactive elements. +**Action:** Always inspect the underlying CSS utility variants (like `cva` configurations) to ensure `aria-disabled:hover` and `aria-disabled:opacity` are explicitly handled when updating accessible disabled states. diff --git a/apps/desktop/src/features/score/ScoreViewer.test.tsx b/apps/desktop/src/features/score/ScoreViewer.test.tsx index 3ac2dd60..d8a0faa0 100644 --- a/apps/desktop/src/features/score/ScoreViewer.test.tsx +++ b/apps/desktop/src/features/score/ScoreViewer.test.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { act, createEvent, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { PDFDocumentLoadingTask, PDFDocumentProxy } from "pdfjs-dist"; import { ScoreViewer } from "./ScoreViewer"; @@ -120,8 +120,15 @@ 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(); + + const previousButton = screen.getByRole("button", { name: "Previous page" }); + expect(previousButton).toHaveAttribute("aria-disabled", "true"); + + const previousClickEvent = createEvent.click(previousButton); + fireEvent(previousButton, previousClickEvent); + expect(previousClickEvent.defaultPrevented).toBe(true); + + expect(screen.getByRole("button", { name: "Next page" })).not.toHaveAttribute("aria-disabled", "true"); }); it("shows the file name when provided", async () => { @@ -174,14 +181,22 @@ 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 previousClickEvent = createEvent.click(previousButton); + fireEvent(previousButton, previousClickEvent); + expect(previousClickEvent.defaultPrevented).toBe(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"); + const nextClickEvent = createEvent.click(nextButton); + fireEvent(nextButton, nextClickEvent); + expect(nextClickEvent.defaultPrevented).toBe(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..24a54e8f 100644 --- a/apps/desktop/src/features/score/ScoreViewer.tsx +++ b/apps/desktop/src/features/score/ScoreViewer.tsx @@ -152,12 +152,20 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps }, [status, pdfDocument, pageNumber, zoom, fitWidth, containerWidth]); /** Move to the previous page, clamped at the first page. */ - const goToPreviousPage = () => { + const goToPreviousPage = (e: React.MouseEvent) => { + if (pageNumber <= 1) { + e.preventDefault(); + return; + } setPageNumber((current) => Math.max(1, current - 1)); }; /** Move to the next page, clamped at the last page. */ - const goToNextPage = () => { + const goToNextPage = (e: React.MouseEvent) => { + if (pageNumber >= pageCount) { + e.preventDefault(); + return; + } setPageNumber((current) => Math.min(pageCount, current + 1)); }; @@ -258,6 +266,7 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps size="icon-lg" className="size-12" aria-label={t("scoreViewerZoomOut")} + title={t("scoreViewerZoomOut")} onClick={zoomOut} >