diff --git a/apps/desktop/src/features/score/ScoreViewer.test.tsx b/apps/desktop/src/features/score/ScoreViewer.test.tsx
index 3ac2dd605..88f2770ac 100644
--- a/apps/desktop/src/features/score/ScoreViewer.test.tsx
+++ b/apps/desktop/src/features/score/ScoreViewer.test.tsx
@@ -20,7 +20,9 @@ vi.mock("../../i18n", () => ({
scoreViewerPageIndicator: "Page {current} of {total}",
scoreViewerZoomIn: "Zoom in",
scoreViewerZoomOut: "Zoom out",
- scoreViewerFitWidth: "Fit width"
+ scoreViewerFitWidth: "Fit width",
+ scoreViewerFirstPageTooltip: "Already at the first page",
+ scoreViewerLastPageTooltip: "Already at the last page"
})[key] ?? key,
detectPreferredLocale: () => "en"
}));
@@ -120,8 +122,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", "Already at the first page");
+ expect(screen.getByRole("button", { name: "Next page" })).toHaveAttribute("aria-disabled", "false");
+ expect(screen.getByRole("button", { name: "Next page" })).not.toHaveAttribute("title");
});
it("shows the file name when provided", async () => {
@@ -174,14 +178,17 @@ 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");
+ expect(nextButton).toHaveAttribute("title", "Already at the last page");
+ expect(previousButton).toHaveAttribute("aria-disabled", "false");
+ expect(previousButton).not.toHaveAttribute("title");
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..7d6cc8c1f 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}
+ aria-disabled={pageNumber <= 1}
+ title={pageNumber <= 1 ? t("scoreViewerFirstPageTooltip") : undefined}
+ onClick={(e) => {
+ if (pageNumber <= 1) {
+ e.preventDefault();
+ } else {
+ goToPreviousPage();
+ }
+ }}
>
@@ -305,8 +312,15 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
size="icon-lg"
className="size-14"
aria-label={t("scoreViewerNextPage")}
- disabled={pageNumber >= pageCount}
- onClick={goToNextPage}
+ aria-disabled={pageNumber >= pageCount}
+ title={pageNumber >= pageCount ? t("scoreViewerLastPageTooltip") : undefined}
+ onClick={(e) => {
+ if (pageNumber >= pageCount) {
+ e.preventDefault();
+ } else {
+ goToNextPage();
+ }
+ }}
>
diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json
index d803a765e..7f7b76884 100644
--- a/apps/desktop/src/locales/en/common.json
+++ b/apps/desktop/src/locales/en/common.json
@@ -154,5 +154,7 @@
"workspaceFirstRangeClash": "{roleName} sits {lowestNote}–{highestNote} in {sectionLabel}. Hear that clash on your instrument before the {sectionLabel}.",
"workspaceFirstRangeMissing": "Tonight's first range still needs an ear check. Confirm the high and low notes on the selected part before the first section.",
"sectionRangeLabel": "Range",
- "sectionRangeNextAction": "Check this span on your instrument before {sectionLabel}."
-}
+ "sectionRangeNextAction": "Check this span on your instrument before {sectionLabel}.",
+ "scoreViewerFirstPageTooltip": "Already at the first page",
+ "scoreViewerLastPageTooltip": "Already at the last page"
+}
\ No newline at end of file
diff --git a/apps/desktop/src/locales/ko/common.json b/apps/desktop/src/locales/ko/common.json
index 0f6c6c66d..228b39ee5 100644
--- a/apps/desktop/src/locales/ko/common.json
+++ b/apps/desktop/src/locales/ko/common.json
@@ -154,5 +154,7 @@
"workspaceFirstRangeClash": "{sectionLabel}의 {roleName}은 {lowestNote}–{highestNote}이고 다른 파트와 겹칩니다. {sectionLabel} 들어가기 전에 그 충돌을 악기로 들어 보세요.",
"workspaceFirstRangeMissing": "오늘 먼저 볼 음역은 아직 귀로 확인이 필요합니다. 선택한 파트의 최저·최고음을 첫 구간 전에 확인해 보세요.",
"sectionRangeLabel": "음역",
- "sectionRangeNextAction": "{sectionLabel} 들어가기 전에 이 음역을 악기로 확인해 보세요."
-}
+ "sectionRangeNextAction": "{sectionLabel} 들어가기 전에 이 음역을 악기로 확인해 보세요.",
+ "scoreViewerFirstPageTooltip": "첫 번째 페이지입니다",
+ "scoreViewerLastPageTooltip": "마지막 페이지입니다"
+}
\ No newline at end of file