Skip to content

Commit b825063

Browse files
committed
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.
1 parent bb24d00 commit b825063

3 files changed

Lines changed: 30 additions & 5 deletions

File tree

‎packages/studio/src/components/pointerTargetSize.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function mount(element: React.ReactNode) {
3333
return host;
3434
}
3535

36-
describe("pointer target size (WCAG 2.5.8)", () => {
36+
describe("pointer target sizing classes (proxy for WCAG 2.5.8, not a geometry check)", () => {
3737
it("gives the timeline zoom slider a 24px box without changing its 2px track or 10px thumb", () => {
3838
const host = mount(<TimelineToolbar />);
3939
const slider = host.querySelector<HTMLInputElement>('input[aria-label="Timeline zoom"]');

‎packages/studio/src/player/components/TimelineClipDiamonds.test.tsx‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ describe("TimelineClipDiamonds", () => {
641641
act(() => root.unmount());
642642
});
643643

644-
const renderSegmentLane = (lastAmbiguous: boolean) => {
644+
const renderSegmentLane = (lastAmbiguous: boolean, clipWidthPx = 200) => {
645645
const host = document.createElement("div");
646646
document.body.append(host);
647647
const root = createRoot(host);
@@ -660,7 +660,7 @@ describe("TimelineClipDiamonds", () => {
660660
format: "percentage",
661661
keyframes: [kf(0), kf(50), kf(100, { easeAmbiguous: lastAmbiguous })],
662662
}}
663-
clipWidthPx={200}
663+
clipWidthPx={clipWidthPx}
664664
clipHeightPx={48}
665665
accentColor="#4ba3d2"
666666
isSelected
@@ -698,6 +698,21 @@ describe("TimelineClipDiamonds", () => {
698698
expect(segment?.style.pointerEvents).toBe("none");
699699
expect(ease?.style.pointerEvents).toBe("auto");
700700
expect(Number(segment?.style.zIndex)).toBeGreaterThan(Number(diamond?.style.zIndex));
701+
// Room to spare here, so the button carries the 24x24 WCAG 2.5.8 overlay.
702+
expect(ease?.className).toContain("before:h-6");
703+
act(() => root.unmount());
704+
});
705+
706+
it("drops the 24x24 ease overlay when the segment is too narrow to hold it", () => {
707+
// The segment wrapper outranks the diamonds, so an overlay wider than the
708+
// clear span between them would steal their clicks at fit zoom. 40px of clip
709+
// across three keyframes leaves well under 24px of clear span per segment.
710+
const { host, root } = renderSegmentLane(false, 40);
711+
const ease = host.querySelector<HTMLButtonElement>("[data-keyframe-ease-button]");
712+
713+
expect(ease).not.toBeNull();
714+
expect(ease?.className).not.toContain("before:h-6");
715+
expect(ease?.style.width).toBe("16px");
701716
act(() => root.unmount());
702717
});
703718

‎packages/studio/src/player/components/TimelineDiamondConnectors.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ export function TimelineDiamondConnectors({
6262
// no source animation id (runtime-scanned) so there is no tween to target.
6363
const target = keyframeTarget(kf);
6464
const ease = kf.ease ?? globalEase;
65+
// connectorWidth is the clear span between the two diamonds' edges, so a
66+
// 24x24 target centred in it overhangs a diamond as soon as the span is
67+
// narrower than 24. The segment wrapper sits at z-index 3, above the
68+
// diamonds, so that overhang would win the hit test and steal their
69+
// clicks at fit zoom. Grow the target only where the room exists.
70+
const roomForFullTarget = connectorWidth >= 24;
6571
return (
6672
<Fragment key={`line-${i}-${previous.keyframe.percentage}-${kf.percentage}`}>
6773
<div
@@ -105,8 +111,12 @@ export function TimelineDiamondConnectors({
105111
title={`Edit ${ease} easing`}
106112
// A visible 24x24 badge would collide with the diamonds either
107113
// side, so the WCAG 2.2 (2.5.8) target is met with a centered
108-
// transparent ::before overlay; the box stays 16x16.
109-
className="absolute flex items-center justify-center rounded opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']"
114+
// transparent ::before overlay; the box stays 16x16. Where the
115+
// segment is too narrow for that overlay the button keeps its
116+
// 16x16 hit area, which is WCAG's target-spacing exception:
117+
// the neighbouring diamonds are themselves the reason it
118+
// cannot grow, and stealing their clicks is the worse failure.
119+
className={`absolute flex items-center justify-center rounded opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 ${roomForFullTarget ? "before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']" : ""}`}
110120
style={{
111121
left: "50%",
112122
top: "50%",

0 commit comments

Comments
 (0)