From b82506363f08bf9b2efbd37dce94c0eaec71844e Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Tue, 28 Jul 2026 18:01:46 +0200 Subject: [PATCH] fix(studio): grow the ease target only where the segment has room The 24x24 WCAG 2.5.8 overlay sits on a wrapper that outranks the diamonds, so on a segment narrower than 24px it overhung them and won their hit test at fit zoom. Gate the overlay on the clear span between the two diamonds and let the button keep its 16x16 box below that. Also renames the pointer target suite to say it asserts the classes that produce the size, not the measured geometry, which happy-dom cannot see. --- .../src/components/pointerTargetSize.test.tsx | 2 +- .../components/TimelineClipDiamonds.test.tsx | 19 +++++++++++++++++-- .../components/TimelineDiamondConnectors.tsx | 14 ++++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/studio/src/components/pointerTargetSize.test.tsx b/packages/studio/src/components/pointerTargetSize.test.tsx index 54f7519f8..fd1923533 100644 --- a/packages/studio/src/components/pointerTargetSize.test.tsx +++ b/packages/studio/src/components/pointerTargetSize.test.tsx @@ -33,7 +33,7 @@ function mount(element: React.ReactNode) { return host; } -describe("pointer target size (WCAG 2.5.8)", () => { +describe("pointer target sizing classes (proxy for WCAG 2.5.8, not a geometry check)", () => { it("gives the timeline zoom slider a 24px box without changing its 2px track or 10px thumb", () => { const host = mount(); const slider = host.querySelector('input[aria-label="Timeline zoom"]'); diff --git a/packages/studio/src/player/components/TimelineClipDiamonds.test.tsx b/packages/studio/src/player/components/TimelineClipDiamonds.test.tsx index aff08c082..fc00b55c0 100644 --- a/packages/studio/src/player/components/TimelineClipDiamonds.test.tsx +++ b/packages/studio/src/player/components/TimelineClipDiamonds.test.tsx @@ -641,7 +641,7 @@ describe("TimelineClipDiamonds", () => { act(() => root.unmount()); }); - const renderSegmentLane = (lastAmbiguous: boolean) => { + const renderSegmentLane = (lastAmbiguous: boolean, clipWidthPx = 200) => { const host = document.createElement("div"); document.body.append(host); const root = createRoot(host); @@ -660,7 +660,7 @@ describe("TimelineClipDiamonds", () => { format: "percentage", keyframes: [kf(0), kf(50), kf(100, { easeAmbiguous: lastAmbiguous })], }} - clipWidthPx={200} + clipWidthPx={clipWidthPx} clipHeightPx={48} accentColor="#4ba3d2" isSelected @@ -698,6 +698,21 @@ describe("TimelineClipDiamonds", () => { expect(segment?.style.pointerEvents).toBe("none"); expect(ease?.style.pointerEvents).toBe("auto"); expect(Number(segment?.style.zIndex)).toBeGreaterThan(Number(diamond?.style.zIndex)); + // Room to spare here, so the button carries the 24x24 WCAG 2.5.8 overlay. + expect(ease?.className).toContain("before:h-6"); + act(() => root.unmount()); + }); + + it("drops the 24x24 ease overlay when the segment is too narrow to hold it", () => { + // The segment wrapper outranks the diamonds, so an overlay wider than the + // clear span between them would steal their clicks at fit zoom. 40px of clip + // across three keyframes leaves well under 24px of clear span per segment. + const { host, root } = renderSegmentLane(false, 40); + const ease = host.querySelector("[data-keyframe-ease-button]"); + + expect(ease).not.toBeNull(); + expect(ease?.className).not.toContain("before:h-6"); + expect(ease?.style.width).toBe("16px"); act(() => root.unmount()); }); diff --git a/packages/studio/src/player/components/TimelineDiamondConnectors.tsx b/packages/studio/src/player/components/TimelineDiamondConnectors.tsx index 6298dc62c..4b155ef71 100644 --- a/packages/studio/src/player/components/TimelineDiamondConnectors.tsx +++ b/packages/studio/src/player/components/TimelineDiamondConnectors.tsx @@ -62,6 +62,12 @@ export function TimelineDiamondConnectors({ // no source animation id (runtime-scanned) so there is no tween to target. const target = keyframeTarget(kf); const ease = kf.ease ?? globalEase; + // connectorWidth is the clear span between the two diamonds' edges, so a + // 24x24 target centred in it overhangs a diamond as soon as the span is + // narrower than 24. The segment wrapper sits at z-index 3, above the + // diamonds, so that overhang would win the hit test and steal their + // clicks at fit zoom. Grow the target only where the room exists. + const roomForFullTarget = connectorWidth >= 24; return (