Skip to content
Closed
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 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.

## 2025-02-19 - Do not replace disabled prop in shared UI components
**Learning:** Native `<button>` accessibility requires `aria-disabled` and conditional `onClick` handlers, but shared design system `<Button>` components (which wrap external primitives) may break if their native `disabled` prop is replaced.
**Action:** When updating elements for accessibility, exclusively modify native HTML buttons, leaving existing shared `<Button>` elements (e.g., from `@base-ui`) intact to prevent visual regressions.
2 changes: 1 addition & 1 deletion apps/desktop/src/features/score/ScoreView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe("ScoreView", () => {

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: "Open score: opener.pdf" })).toHaveAttribute("aria-disabled", "true");
expect(screen.getByRole("button", { name: "Remove: opener.pdf" })).toBeDisabled();

fireEvent.click(screen.getByRole("button", { name: "Open score: opener.pdf" }));
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/features/score/ScoreView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,11 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
<button
type="button"
onClick={projectId ? () => void openAttachment(projectId, attachment) : undefined}
disabled={!projectId}
aria-disabled={!projectId ? "true" : 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"
title={!projectId ? t("scoreRequiresProject") : undefined}
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 aria-disabled:cursor-not-allowed aria-disabled:opacity-60"
>
<FileMusic className="size-4 shrink-0 text-cyan-300" aria-hidden="true" />
<span className="truncate">{attachment.fileName}</span>
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/features/workspace/SectionRoadmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
<button
type="button"
aria-label={editChordLabel(role, section.label)}
className={`-ml-2 rounded px-2 py-0.5 text-lg font-black tracking-tight transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${
className={`-ml-2 rounded px-2 py-0.5 text-lg font-black tracking-tight transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 ${
onSongUpdate
? "cursor-pointer hover:bg-white/10"
: "cursor-default"
Expand All @@ -161,9 +161,9 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
? "bg-indigo-300/15 text-indigo-200"
: "text-cyan-100"
}`}
onClick={() => handleChordEdit(section.id, role)}
onClick={onSongUpdate ? () => handleChordEdit(section.id, role) : undefined}
title={onSongUpdate ? t("chordEditTitle") : undefined}
disabled={!onSongUpdate}
aria-disabled={!onSongUpdate ? "true" : undefined}
>
{role.harmony.chord}
</button>
Expand Down
Loading