fix(clip): clip mode keeps its range, its focus and its keys through the player's own moves
- A mark on the very end seeks a moment short of it; while a clip is marked the end of the item neither closes the player nor steps the queue, and Plex's auto-skip waits. - Plex: Escape and Back peel what is over the picture (info, up-next, the countdown) before the clip; Back under a dialog is the dialog's; no range opens while the length is unknown. - YouTube: the focus goes back to the scissors the user can see, else the player; the panel keeps its name, Loop and focus when the view is maximised or restored under it. - A typed time stays in its field until taken, then shows where it landed; unreadable stays marked invalid; a decimal comma is read (ui/TimecodeField, the editor's fields too). - RangeTrack: a drag is its pointer's, a cancelled drag reports final, an adornment sits beside the slider, a caller-chosen focus ring (amber on the clip strip). CropFrame: a drag is its pointer's. - The editor's remove-cut is a real button at the cut's foot; a segment's keep toggle sits over the cut's hit area.
This commit is contained in:
@@ -49,6 +49,7 @@ function Harness({
|
||||
onClose = () => {},
|
||||
onPlayFrom = () => {},
|
||||
onSeek = () => {},
|
||||
duration = 3600,
|
||||
}: {
|
||||
src?: ClipSource;
|
||||
start?: ClipRange;
|
||||
@@ -56,6 +57,7 @@ function Harness({
|
||||
onClose?: () => void;
|
||||
onPlayFrom?: (t: number) => void;
|
||||
onSeek?: (t: number, final: boolean) => void;
|
||||
duration?: number;
|
||||
}) {
|
||||
const [clip, setClip] = useState(start);
|
||||
const [crop, setCrop] = useState<ClipCrop | null>(null);
|
||||
@@ -67,7 +69,7 @@ function Harness({
|
||||
src={src}
|
||||
clip={clip}
|
||||
onChange={setClip}
|
||||
duration={3600}
|
||||
duration={duration}
|
||||
current={current}
|
||||
picture={16 / 9}
|
||||
crop={crop}
|
||||
@@ -99,6 +101,53 @@ describe("ClipPanel", () => {
|
||||
expect(range()).toBe("140-170");
|
||||
});
|
||||
|
||||
it("keeps the focus in a time field that took a typed time on Enter", () => {
|
||||
render(<Harness />);
|
||||
const field = () => screen.getAllByRole("textbox", { name: "clip.in" })[0]!;
|
||||
field().focus();
|
||||
fireEvent.change(field(), { target: { value: "2:20" } });
|
||||
fireEvent.keyDown(field(), { key: "Enter" });
|
||||
expect(range()).toBe("140-170");
|
||||
expect(document.activeElement).toBe(field());
|
||||
expect((field() as HTMLInputElement).value).toBe("0:02:20.0");
|
||||
});
|
||||
|
||||
it("shows the mark it holds, not a time it could not take", () => {
|
||||
render(<Harness start={{ start: 3500, end: 3600 }} />);
|
||||
const out = () => screen.getAllByRole("textbox", { name: "clip.out" })[0] as HTMLInputElement;
|
||||
// Past the end: clamped back to the end, and the field says so.
|
||||
fireEvent.change(out(), { target: { value: "1:15:00" } });
|
||||
fireEvent.blur(out());
|
||||
expect(range()).toBe("3500-3600");
|
||||
expect(out().value).toBe("1:00:00.0");
|
||||
expect(out().getAttribute("aria-invalid")).toBeNull();
|
||||
// Not a time at all: kept for fixing, and marked as not taken.
|
||||
fireEvent.change(out(), { target: { value: "abc" } });
|
||||
fireEvent.blur(out());
|
||||
expect(out().value).toBe("abc");
|
||||
expect(out().getAttribute("aria-invalid")).toBe("true");
|
||||
expect(range()).toBe("3500-3600");
|
||||
});
|
||||
|
||||
it("takes a decimal comma, as a Hungarian hand types it", () => {
|
||||
render(<Harness />);
|
||||
const field = screen.getAllByRole("textbox", { name: "clip.in" })[0]!;
|
||||
fireEvent.change(field, { target: { value: "1:42,5" } });
|
||||
fireEvent.keyDown(field, { key: "Enter" });
|
||||
expect(range()).toBe("102.5-130");
|
||||
});
|
||||
|
||||
it("never seeks the player onto the item's very end (that would end it, and the clip with it)", () => {
|
||||
const onSeek = vi.fn();
|
||||
render(<Harness onSeek={onSeek} duration={600} start={{ start: 590, end: 595 }} />);
|
||||
fireEvent.keyDown(screen.getByRole("slider", { name: "clip.out" }), { key: "End" });
|
||||
expect(range()).toBe("590-600");
|
||||
const [time, final] = onSeek.mock.lastCall!;
|
||||
expect(final).toBe(true);
|
||||
expect(time).toBeLessThan(600);
|
||||
expect(time).toBeGreaterThan(599);
|
||||
});
|
||||
|
||||
it("marks an end at the playhead", () => {
|
||||
render(<Harness current={115.5} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "clip.outHere" }));
|
||||
|
||||
Reference in New Issue
Block a user