Skip to content

Commit e2ee540

Browse files
committed
fix(zoom): point the zoom row at the visible reason its levels are greyed out
1 parent cf38de3 commit e2ee540

3 files changed

Lines changed: 27 additions & 4 deletions

File tree

‎src/components/ai-edition/RightPanes.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3972,10 +3972,12 @@ export function ChoiceRow<T extends string | number>({
39723972
columns,
39733973
tiles,
39743974
display,
3975+
describedBy,
39753976
}: {
39763977
label: string;
39773978
/** `null` leaves a hole in the grid: the middle of the camera's position grid. An option
3978-
* can be `disabled` on its own, with the reason as its `title`. */
3979+
* can be `disabled` on its own: a disabled button takes no focus, so its `title` is only a
3980+
* mouse hint, and the reason must also be visible text the row points at (`describedBy`). */
39793981
options: ReadonlyArray<{
39803982
value: T;
39813983
label: string;
@@ -3993,13 +3995,16 @@ export function ChoiceRow<T extends string | number>({
39933995
/** Ce que montre un bouton. Par défaut l'icône s'il y en a une, et alors `label` lui sert de
39943996
* nom ; sinon le texte. */
39953997
display?: "text" | "icon" | "both";
3998+
/** Id of the visible text that explains the row, typically why some options are disabled. */
3999+
describedBy?: string;
39964000
}) {
39974001
const buttonsRef = useRef<Array<HTMLButtonElement | null>>([]);
39984002
const mode = display ?? (options.some((o) => o?.icon) ? "icon" : "text");
39994003
return (
40004004
<div
40014005
role="group"
40024006
aria-label={label}
4007+
aria-describedby={describedBy}
40034008
className={`${styles.choiceRow} ${tiles ? styles.choiceRowTiles : ""}`}
40044009
style={{ gridTemplateColumns: `repeat(${columns ?? options.length}, minmax(0, 1fr))` }}
40054010
onKeyDown={(e) => {

‎src/components/ai-edition/v4/FloatingInspector.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
ZoomIn,
2727
} from "lucide-react";
2828
import type { ComponentProps } from "react";
29-
import { useEffect, useMemo, useRef, useState } from "react";
29+
import { useEffect, useId, useMemo, useRef, useState } from "react";
3030
import { toast } from "sonner";
3131
import { parseCustomPlaybackSpeedInput } from "@/components/video-editor/customPlaybackSpeed";
3232
import {
@@ -544,6 +544,8 @@ export function ZoomLevelControl({
544544
};
545545

546546
const tooDeep = ts("zoom.levelBlurs", { max: maxScale });
547+
const tooDeepId = useId();
548+
const limited = maxScale < MAX_ZOOM_SCALE;
547549
const presets = ZOOM_PRESETS.map((preset) =>
548550
preset.value > maxScale ? { ...preset, disabled: true, title: tooDeep } : preset,
549551
);
@@ -558,9 +560,15 @@ export function ZoomLevelControl({
558560
options={presets}
559561
value={requested}
560562
onChange={setScale}
563+
describedBy={limited ? tooDeepId : undefined}
561564
/>,
562565
)}
563-
{maxScale < MAX_ZOOM_SCALE ? <p className={shell.hint}>{tooDeep}</p> : null}
566+
{/* Visible, not only a `title`: a disabled preset cannot take keyboard focus. */}
567+
{limited ? (
568+
<p id={tooDeepId} className={shell.hint}>
569+
{tooDeep}
570+
</p>
571+
) : null}
564572
{paneRow(
565573
ts("zoom.customScale"),
566574
<input

‎src/components/ai-edition/v4/ZoomLevelControl.test.tsx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,14 @@ describe("ZoomLevelControl", () => {
135135
const buttons = screen.getAllByRole("button") as HTMLButtonElement[];
136136
expect(buttons.map((b) => b.disabled)).toEqual([false, false, false, true]);
137137
expect(buttons[3]).toHaveAttribute("title", "zoom.levelBlurs:2.5");
138-
expect(screen.getByText("zoom.levelBlurs:2.5")).toBeInTheDocument();
138+
// The reason is visible text the row points at, not only a title on a button that a
139+
// keyboard can never focus.
140+
const hint = screen.getByText("zoom.levelBlurs:2.5");
141+
expect(screen.getByRole("group", { name: "zoom.level" })).toHaveAttribute(
142+
"aria-describedby",
143+
hint.id,
144+
);
145+
expect(hint.id).not.toBe("");
139146

140147
const field = screen.getByRole("textbox", { name: "zoom.customScale" });
141148
fireEvent.change(field, { target: { value: "3" } });
@@ -148,6 +155,9 @@ describe("ZoomLevelControl", () => {
148155
it("says nothing when every level is within reach", () => {
149156
renderControl(3);
150157
expect(screen.queryByText(/zoom\.levelBlurs/)).not.toBeInTheDocument();
158+
expect(screen.getByRole("group", { name: "zoom.level" })).not.toHaveAttribute(
159+
"aria-describedby",
160+
);
151161
});
152162

153163
it("ignores an unparseable draft without touching the region", () => {

0 commit comments

Comments
 (0)