mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
fix(studio): meet the 24x24 pointer target minimum on toolbar and lane controls
This commit is contained in:
@@ -154,10 +154,10 @@ export function StudioLeftSidebar({
|
||||
onAddAssetToTimeline={onAddAssetToTimeline}
|
||||
onAddCompositionToTimeline={onAddCompositionToTimeline}
|
||||
/>
|
||||
{/* Vertical resize divider: 3px visible seam, 8px pointer-capture zone via
|
||||
{/* Vertical resize divider: 3px visible seam, 13px pointer-capture zone via
|
||||
the absolutely-positioned inner hit area. The outer element is w-[3px] so
|
||||
it contributes only 3px of gap in the flex row; the inner -left-[2.5px]
|
||||
element widens the hit area to 8px without affecting layout. */}
|
||||
it contributes only 3px of gap in the flex row; the inner -left-[2px]
|
||||
element widens the hit area without affecting layout. */}
|
||||
<div
|
||||
role="separator"
|
||||
aria-label="Resize sidebar"
|
||||
@@ -176,8 +176,13 @@ export function StudioLeftSidebar({
|
||||
adjustPanelWidth("left", delta);
|
||||
}}
|
||||
>
|
||||
{/* Expanded hit zone: 8px wide, centered on the 3px seam */}
|
||||
<div className="absolute inset-y-0 -left-[2.5px] w-2" />
|
||||
{/* Expanded hit zone, deliberately asymmetric: 2px into the sidebar card,
|
||||
the 3px seam, then 8px into the preview pane's p-2 stage gutter — the
|
||||
only dead space adjacent to this seam. It stops at 13px rather than the
|
||||
24px WCAG 2.2 (2.5.8) target because the next pixel on either side is
|
||||
live: the sidebar's scrolling tab content on the left, the preview
|
||||
stage on the right. Silently stealing their clicks is the worse bug. */}
|
||||
<div className="absolute inset-y-0 -left-[2px] w-[13px]" />
|
||||
{/* Visible hairline */}
|
||||
<div className="absolute top-1/2 left-0 h-[52px] w-[3px] -translate-y-1/2 bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
|
||||
</div>
|
||||
|
||||
@@ -446,7 +446,7 @@ export function StudioRightPanel({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Vertical resize divider: 3px visible seam, 8px pointer-capture zone via
|
||||
{/* Vertical resize divider: 3px visible seam, 13px pointer-capture zone via
|
||||
the absolutely-positioned inner hit area. */}
|
||||
<div
|
||||
role="separator"
|
||||
@@ -467,8 +467,12 @@ export function StudioRightPanel({
|
||||
adjustPanelWidth("right", delta);
|
||||
}}
|
||||
>
|
||||
{/* Expanded hit zone: 8px wide, centered on the 3px seam */}
|
||||
<div className="absolute inset-y-0 -left-[2.5px] w-2" />
|
||||
{/* Expanded hit zone, deliberately asymmetric: 8px into the preview pane's
|
||||
p-2 stage gutter (the only dead space here), the 3px seam, then 2px
|
||||
into the inspector card. It stops at 13px rather than the 24px WCAG 2.2
|
||||
(2.5.8) target because the next pixel on either side is live: the
|
||||
preview stage on the left, the inspector's own controls on the right. */}
|
||||
<div className="absolute inset-y-0 -left-[8px] w-[13px]" />
|
||||
{/* Visible hairline */}
|
||||
<div className="absolute top-1/2 left-0 h-[52px] w-[3px] -translate-y-1/2 bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
|
||||
</div>
|
||||
|
||||
@@ -449,7 +449,9 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
||||
setZoomMode("manual");
|
||||
setManualZoomPercent(timelineSliderToZoomPercent(Number(e.target.value)));
|
||||
}}
|
||||
className="mx-1 w-[96px] cursor-pointer appearance-none bg-transparent [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-neutral-700 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-[10px] [&::-webkit-slider-thumb]:h-[10px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#0a0a0a,0_1px_3px_rgba(0,0,0,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb:active]:cursor-grabbing"
|
||||
// h-6 on the input is the 24x24 WCAG 2.2 (2.5.8) target: the visible
|
||||
// track stays 2px and the thumb 10px, only the pointer box grows.
|
||||
className="mx-1 h-6 w-[96px] cursor-pointer appearance-none bg-transparent [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-neutral-700 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-[10px] [&::-webkit-slider-thumb]:h-[10px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#0a0a0a,0_1px_3px_rgba(0,0,0,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb:active]:cursor-grabbing"
|
||||
/>
|
||||
<Tooltip label="Zoom in">
|
||||
<button
|
||||
|
||||
@@ -167,6 +167,11 @@ export const KeyframeNavigation = memo(function KeyframeNavigation({
|
||||
};
|
||||
|
||||
return (
|
||||
// The two 12x20 steppers sit gap-0.5 apart with a 9px diamond between them,
|
||||
// so they stay below the 24px WCAG 2.2 (2.5.8) minimum under that criterion's
|
||||
// own spacing/inline exception: centred 24px targets here would overlap each
|
||||
// other AND the diamond, and one control swallowing its neighbour's clicks is
|
||||
// a worse 2.5.8 failure than a small target. Do not "fix" these to 24.
|
||||
<div className="flex h-5 items-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -189,7 +189,9 @@ export function SliderControl({
|
||||
onMouseUp={() => commitDraft(draft)}
|
||||
onTouchEnd={() => commitDraft(draft)}
|
||||
onBlur={() => commitDraft(draft)}
|
||||
className="h-4 min-w-0 w-full cursor-pointer appearance-none bg-transparent disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-panel-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-[10px] [&::-webkit-slider-thumb]:h-[10px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#0C0C0E,0_1px_3px_rgba(0,0,0,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb:active]:cursor-grabbing"
|
||||
// h-6 is the 24x24 WCAG 2.2 (2.5.8) target: the visible track stays 2px
|
||||
// and the thumb 10px, only the pointer box grows.
|
||||
className="h-6 min-w-0 w-full cursor-pointer appearance-none bg-transparent disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-panel-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-[10px] [&::-webkit-slider-thumb]:h-[10px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#0C0C0E,0_1px_3px_rgba(0,0,0,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb:active]:cursor-grabbing"
|
||||
/>
|
||||
<div className="min-w-[44px] rounded-md bg-panel-input px-2 py-1.5 text-right text-[11px] font-medium text-panel-text-1 tabular-nums">
|
||||
{formatDisplayValue?.(draft) ?? displayValue}
|
||||
|
||||
@@ -73,9 +73,9 @@ export function TimelineResizeDivider({
|
||||
);
|
||||
|
||||
return (
|
||||
// Horizontal resize divider: 3px visible seam (h-[3px]), 8px pointer-capture
|
||||
// Horizontal resize divider: 3px visible seam (h-[3px]), 10px pointer-capture
|
||||
// zone via the absolutely-positioned inner hit area so the layout gap stays
|
||||
// at 3px while draggability is preserved over the full 8px band.
|
||||
// at 3px while draggability is preserved over the full band.
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
@@ -94,8 +94,13 @@ export function TimelineResizeDivider({
|
||||
onPointerCancel={handlePointerUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Expanded hit zone: 8px tall, centered on the 3px seam */}
|
||||
<div className="absolute inset-x-0 -top-[2.5px] h-2" />
|
||||
{/* Expanded hit zone, deliberately asymmetric: 4px up into the transport
|
||||
row's vertical centring slack, the 3px seam, then 3px down into the
|
||||
timeline card's border + toolbar padding. It stops at 10px rather than
|
||||
the 24px WCAG 2.2 (2.5.8) target because that is all the dead space
|
||||
there is: 28px transport buttons sit above and 28px toolbar buttons
|
||||
below, and silently stealing their clicks is the worse bug. */}
|
||||
<div className="absolute inset-x-0 -top-[4px] h-[10px]" />
|
||||
{/* Visible hairline — invisible at rest, subtle wash on hover/drag/focus */}
|
||||
<div className="h-[3px] w-full bg-transparent transition-colors group-hover:bg-white/12 group-active:bg-white/18 group-focus-visible:bg-studio-accent/60" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { CompositionsTab } from "./sidebar/CompositionsTab";
|
||||
import { TimelineToolbar } from "./TimelineToolbar";
|
||||
|
||||
// WCAG 2.2 (2.5.8) requires a 24x24 CSS pixel pointer target. happy-dom has no
|
||||
// CSS engine, so getBoundingClientRect() is 0x0 for everything here and the size
|
||||
// itself cannot be measured — these assert the utility classes that PRODUCE the
|
||||
// size instead, which is enough to fail if someone drops them. Real geometry
|
||||
// still needs a browser check.
|
||||
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
||||
(
|
||||
window as unknown as { happyDOM: { settings: { disableIframePageLoading: boolean } } }
|
||||
).happyDOM.settings.disableIframePageLoading = true;
|
||||
|
||||
let root: Root | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
if (root) act(() => root?.unmount());
|
||||
root = null;
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function mount(element: React.ReactNode) {
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
root = createRoot(host);
|
||||
act(() => root?.render(element));
|
||||
return host;
|
||||
}
|
||||
|
||||
describe("pointer target size (WCAG 2.5.8)", () => {
|
||||
it("gives the timeline zoom slider a 24px box without changing its 2px track or 10px thumb", () => {
|
||||
const host = mount(<TimelineToolbar />);
|
||||
const slider = host.querySelector<HTMLInputElement>('input[aria-label="Timeline zoom"]');
|
||||
if (!slider) throw new Error("zoom slider did not render");
|
||||
|
||||
expect(slider.className).toContain("h-6");
|
||||
expect(slider.className).toContain("[&::-webkit-slider-runnable-track]:h-[2px]");
|
||||
expect(slider.className).toContain("[&::-webkit-slider-thumb]:h-[10px]");
|
||||
expect(slider.className).toContain("[&::-webkit-slider-thumb]:w-[10px]");
|
||||
});
|
||||
|
||||
it("gives the composition card's render button a 24x24 box around its 14px glyph", () => {
|
||||
const host = mount(
|
||||
<CompositionsTab
|
||||
projectId="demo"
|
||||
compositions={["compositions/headline.html"]}
|
||||
activeComposition={null}
|
||||
onSelect={vi.fn()}
|
||||
onRenderComposition={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const button = host.querySelector<HTMLButtonElement>('button[aria-label="Render headline"]');
|
||||
if (!button) throw new Error("render button did not render");
|
||||
|
||||
expect(button.className).toContain("h-6");
|
||||
expect(button.className).toContain("w-6");
|
||||
expect(button.querySelector("svg")?.getAttribute("width")).toBe("14");
|
||||
});
|
||||
});
|
||||
@@ -317,7 +317,10 @@ function CompCard({
|
||||
e.stopPropagation();
|
||||
onRender();
|
||||
}}
|
||||
className={`flex-shrink-0 p-1 rounded transition-colors ${
|
||||
// h-6 w-6 = the 24x24 WCAG 2.2 (2.5.8) minimum target; the 14px glyph
|
||||
// is unchanged, only the box grows. The sibling "+" button is h-8 w-8,
|
||||
// so the card row already has the room.
|
||||
className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded transition-colors ${
|
||||
isRendering
|
||||
? "text-neutral-600 cursor-not-allowed"
|
||||
: "text-neutral-600 hover:text-studio-accent hover:bg-neutral-800"
|
||||
|
||||
Reference in New Issue
Block a user