docs(studio): document timeline keyboard navigation (#3031)

* feat(studio): expose timeline treegrid semantics

* feat(studio): coordinate logical timeline focus

* feat(studio): add timeline keyboard controls

* docs(studio): document timeline keyboard navigation
This commit is contained in:
Miguel Ángel
2026-08-04 17:09:07 -07:00
committed by GitHub
parent 0f1333c80d
commit 7bf425b7a9
41 changed files with 1911 additions and 653 deletions
@@ -16,6 +16,7 @@ import {
} from "./trackHeaderLaneState";
import { valueReadout } from "./trackHeaderLaneValues";
import { trackDisplaySuffix } from "./timelineTrackDisplay";
import { timelineLogicalRowCellId, timelinePropertyRowId } from "./timelineNavigationIdentity";
interface TimelineTrackHeaderProps {
/** The track's real key: a FRACTIONAL z-order sort value. Routes callbacks;
@@ -41,6 +42,7 @@ interface TimelineTrackHeaderProps {
currentTime: number;
isTrackHidden: boolean;
isAudioTrack: boolean;
rovingTargetId?: string | null;
theme: TimelineTheme;
onToggleClipExpanded: () => void;
onToggleTrackHidden: TimelineEditCallbacks["onToggleTrackHidden"];
@@ -191,6 +193,7 @@ function PropertyGroupNavigation({
}
function PropertyGroupHeaderRow({
lanesId,
lane,
laneIndex,
isLastLane,
@@ -201,7 +204,9 @@ function PropertyGroupHeaderRow({
columnWidth,
onTogglePropertyGroupKeyframe,
onSeek,
rovingTargetId = null,
}: {
lanesId: string;
lane: TimelinePropertyLane;
laneIndex: number;
isLastLane: boolean;
@@ -212,7 +217,9 @@ function PropertyGroupHeaderRow({
columnWidth: number;
onTogglePropertyGroupKeyframe?: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"];
onSeek?: (time: number) => void;
rovingTargetId: string | null;
}) {
const elementId = expandedElement.key ?? expandedElement.id;
const { navigation, values, label, toggleTarget } = resolveLaneHeaderState(
lane,
currentTime,
@@ -221,6 +228,10 @@ function PropertyGroupHeaderRow({
return (
<div
id={timelineLogicalRowCellId(lanesId, timelinePropertyRowId(elementId, lane.group), "header")}
data-timeline-focus-id={timelinePropertyRowId(elementId, lane.group)}
data-timeline-element-id={elementId}
tabIndex={rovingTargetId === timelinePropertyRowId(elementId, lane.group) ? 0 : -1}
data-property-group={lane.group}
data-timeline-lane-top={getTimelineLaneTop(laneIndex)}
className="absolute left-0 flex items-center gap-1 overflow-hidden px-1.5 text-[10px] text-white/65"
@@ -298,6 +309,7 @@ export function TimelineTrackHeader({
onToggleTrackHidden,
onTogglePropertyGroupKeyframe,
onSeek,
rovingTargetId = null,
}: TimelineTrackHeaderProps) {
const clipPercentage = keyframeClip
? ((currentTime - keyframeClip.start) / keyframeClip.duration) * 100
@@ -314,6 +326,8 @@ export function TimelineTrackHeader({
return (
<div
role="rowheader"
aria-colindex={1}
className={`sticky left-0 z-[12] shrink-0 ${
!isKeyframeLayer
? showTrackLabel
@@ -374,6 +388,7 @@ export function TimelineTrackHeader({
lanes.map((lane, laneIndex) => (
<PropertyGroupHeaderRow
key={lane.group}
lanesId={lanesId}
lane={lane}
laneIndex={laneIndex}
isLastLane={laneIndex === lanes.length - 1}
@@ -384,6 +399,7 @@ export function TimelineTrackHeader({
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
onSeek={onSeek}
rovingTargetId={rovingTargetId}
/>
))}
</>