mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(studio): announce real track numbers and point aria-controls at the lanes
The timeline's track key is a fractional z-order sort value, and the header built its visibility label straight from it, so screen readers announced "Hide track 0.16666666666666666". A track's 1-based display row is now passed alongside the key: the row number goes in every label, the key keeps routing every callback (visibility toggle, lane context menu). The same fix covers the `Track N` fallback used when a track holds no labelled element. The layer disclosure caret's aria-controls named a div in the sticky label column. That subtree is not empty, it holds the per-lane keyframe controls, but its children are all absolutely positioned so the div computes to 0x0, and the diamonds the caret visibly reveals live on the canvas instead. The caret expands two disjoint subtrees and was naming the less useful one. TimelinePropertyLanes now renders one static wrapper (static, not relative, so it establishes no containing block and the absolutely-positioned lanes keep resolving against the track-content div with identical geometry) and takes the id. TimelineLanes mints that id, since it is the only place that sees both ends of the disclosure, and mounts the wrapper for the track's keyframe clip in both disclosure states so the reference still resolves while collapsed. TimelineLaneBaseProps moves to its own module: it is the contract shared by TimelineCanvas and TimelineLanes, and lifting it out keeps TimelineLanes.tsx well under the 600-line cap instead of pushing past it.
This commit is contained in:
@@ -17,8 +17,17 @@ import {
|
||||
import { valueReadout } from "./trackHeaderLaneValues";
|
||||
|
||||
interface TimelineTrackHeaderProps {
|
||||
/** The track's real key: a FRACTIONAL z-order sort value. Routes callbacks;
|
||||
* never shown or announced. */
|
||||
trackNumber: number;
|
||||
/** The track's 1-based position in the rendered order: the only number safe
|
||||
* to put in a label. Announcing `trackNumber` read out "track
|
||||
* 0.16666666666666666". */
|
||||
trackDisplayNumber: number;
|
||||
trackLabel: string;
|
||||
/** Id of the canvas-side lanes element the disclosure caret expands. Minted by
|
||||
* TimelineLanes, which is the one place that sees both subtrees. */
|
||||
lanesId: string;
|
||||
contentOrigin: number;
|
||||
/** The track's active keyframe clip (selected, else primary) — the one whose
|
||||
* disclosure + property rows this header shows, whether expanded or not. */
|
||||
@@ -40,16 +49,20 @@ interface TimelineTrackHeaderProps {
|
||||
function VisibilityButton({
|
||||
hidden,
|
||||
trackNumber,
|
||||
trackDisplayNumber,
|
||||
visible,
|
||||
onToggle,
|
||||
}: {
|
||||
hidden: boolean;
|
||||
trackNumber: number;
|
||||
trackDisplayNumber: number;
|
||||
visible: boolean;
|
||||
onToggle: TimelineEditCallbacks["onToggleTrackHidden"];
|
||||
}) {
|
||||
if (!visible) return <span aria-hidden="true" className="h-6 w-6 shrink-0" />;
|
||||
const label = hidden ? `Show track ${trackNumber}` : `Hide track ${trackNumber}`;
|
||||
// Display number in the text, real key in the callback. The two must not be
|
||||
// conflated in either direction.
|
||||
const label = hidden ? `Show track ${trackDisplayNumber}` : `Hide track ${trackDisplayNumber}`;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -77,6 +90,7 @@ function VisibilityButton({
|
||||
// count, eye. Not deprecated — it is the live path for every track without lanes.
|
||||
function PlainTrackHeader({
|
||||
trackNumber,
|
||||
trackDisplayNumber,
|
||||
trackLabel,
|
||||
clipCount,
|
||||
showTrackLabel,
|
||||
@@ -86,6 +100,7 @@ function PlainTrackHeader({
|
||||
}: Pick<
|
||||
TimelineTrackHeaderProps,
|
||||
| "trackNumber"
|
||||
| "trackDisplayNumber"
|
||||
| "trackLabel"
|
||||
| "clipCount"
|
||||
| "isTrackHidden"
|
||||
@@ -106,6 +121,7 @@ function PlainTrackHeader({
|
||||
<VisibilityButton
|
||||
hidden={isTrackHidden}
|
||||
trackNumber={trackNumber}
|
||||
trackDisplayNumber={trackDisplayNumber}
|
||||
visible
|
||||
onToggle={onToggleTrackHidden}
|
||||
/>
|
||||
@@ -263,7 +279,9 @@ function PropertyGroupHeaderRow({
|
||||
|
||||
export function TimelineTrackHeader({
|
||||
trackNumber,
|
||||
trackDisplayNumber,
|
||||
trackLabel,
|
||||
lanesId,
|
||||
contentOrigin,
|
||||
keyframeClip,
|
||||
clipCount,
|
||||
@@ -290,7 +308,6 @@ export function TimelineTrackHeader({
|
||||
// owns the gutter past it, so a 0% diamond isn't clipped by this panel).
|
||||
const showTrackLabel = contentOrigin >= LABEL_COL_W;
|
||||
const isKeyframeLayer = !!keyframeClip && lanes.length > 0;
|
||||
const lanesId = `timeline-lanes-track-${trackNumber}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -310,6 +327,7 @@ export function TimelineTrackHeader({
|
||||
{!keyframeClip || lanes.length === 0 ? (
|
||||
<PlainTrackHeader
|
||||
trackNumber={trackNumber}
|
||||
trackDisplayNumber={trackDisplayNumber}
|
||||
trackLabel={trackLabel}
|
||||
clipCount={clipCount}
|
||||
showTrackLabel={showTrackLabel}
|
||||
@@ -336,29 +354,35 @@ export function TimelineTrackHeader({
|
||||
<VisibilityButton
|
||||
hidden={isTrackHidden}
|
||||
trackNumber={trackNumber}
|
||||
trackDisplayNumber={trackDisplayNumber}
|
||||
visible
|
||||
onToggle={onToggleTrackHidden}
|
||||
/>
|
||||
</LayerDisclosureRow>
|
||||
{/* Always mounted so the caret's aria-controls resolves in both states. */}
|
||||
<div id={lanesId}>
|
||||
{isExpanded &&
|
||||
lanes.map((lane, laneIndex) => (
|
||||
<PropertyGroupHeaderRow
|
||||
key={lane.group}
|
||||
lane={lane}
|
||||
laneIndex={laneIndex}
|
||||
isLastLane={laneIndex === lanes.length - 1}
|
||||
expandedElement={keyframeClip}
|
||||
currentTime={currentTime}
|
||||
clipPercentage={clipPercentage}
|
||||
gutterBackground={theme.gutterBackground}
|
||||
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
||||
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
|
||||
onSeek={onSeek}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* The caret expands TWO disjoint subtrees: these label-column rows,
|
||||
which carry the per-lane keyframe controls, and the diamond lanes
|
||||
on the canvas. `lanesId` names the canvas lanes (rendered by
|
||||
TimelineLanes), because that is what a sighted user watches appear
|
||||
and what following the reference has to land on. These rows are not
|
||||
empty and are not the target; they are absolutely positioned inside
|
||||
the sticky column, which is what made a wrapper HERE compute to
|
||||
0x0 and hold no diamonds. */}
|
||||
{isExpanded &&
|
||||
lanes.map((lane, laneIndex) => (
|
||||
<PropertyGroupHeaderRow
|
||||
key={lane.group}
|
||||
lane={lane}
|
||||
laneIndex={laneIndex}
|
||||
isLastLane={laneIndex === lanes.length - 1}
|
||||
expandedElement={keyframeClip}
|
||||
currentTime={currentTime}
|
||||
clipPercentage={clipPercentage}
|
||||
gutterBackground={theme.gutterBackground}
|
||||
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
||||
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
|
||||
onSeek={onSeek}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user