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
@@ -1,14 +1,19 @@
import type { ReactNode } from "react";
import { timelineLogicalRowCellId } from "./timelineNavigationIdentity";
import type { TimelineLogicalRow } from "./timelineKeyboardNavigation";
interface TimelineTrackRowProps {
index: number;
rowKey: number;
rowCount: number;
logicalRow: TimelineLogicalRow;
propertyRows: readonly TimelineLogicalRow[];
lanesId: string;
top: number;
height: number;
virtualized: boolean;
background: string;
borderColor: string;
rovingTargetId?: string | null;
children: ReactNode;
}
@@ -16,23 +21,24 @@ interface TimelineTrackRowProps {
export function TimelineTrackRow({
index,
rowKey,
rowCount,
logicalRow,
propertyRows,
lanesId,
top,
height,
virtualized,
background,
borderColor,
rovingTargetId = null,
children,
}: TimelineTrackRowProps) {
return (
<div
role="listitem"
aria-posinset={index + 1}
aria-setsize={rowCount}
role="rowgroup"
data-index={index}
data-timeline-row={index}
data-timeline-row-key={rowKey}
className={`${virtualized ? "absolute left-0 right-0" : "relative"} flex`}
className={virtualized ? "absolute left-0 right-0" : "relative"}
style={{
top: virtualized ? top : undefined,
height,
@@ -40,7 +46,52 @@ export function TimelineTrackRow({
borderBottom: `1px solid ${borderColor}`,
}}
>
{children}
<div
role="row"
aria-rowindex={logicalRow.logicalIndex + 1}
aria-level={logicalRow.level}
aria-expanded={logicalRow.expandable ? logicalRow.expanded : undefined}
data-timeline-logical-row-id={logicalRow.id}
data-timeline-focus-id={logicalRow.id}
tabIndex={rovingTargetId === logicalRow.id ? 0 : -1}
className="flex"
style={{ height }}
>
{children}
</div>
{propertyRows.map((row) => {
const group = row.propertyGroup;
const keyframeCount = row.items.filter((item) => item.kind === "keyframe").length;
const easeCount = row.items.filter((item) => item.kind === "ease").length;
return (
// ponytail: aria-owns maps this hidden logical row onto the two visible
// property-lane cells without duplicating interactive controls.
<div
key={row.id}
role="row"
aria-rowindex={row.logicalIndex + 1}
aria-level={row.level}
data-property-group={group}
data-timeline-logical-row-id={row.id}
className="sr-only"
>
<div
role="rowheader"
aria-colindex={1}
aria-owns={timelineLogicalRowCellId(lanesId, row.id, "header")}
>
{group}
</div>
<div
role="gridcell"
aria-colindex={2}
aria-owns={timelineLogicalRowCellId(lanesId, row.id, "content")}
>
{keyframeCount} keyframes, {easeCount} ease controls
</div>
</div>
);
})}
</div>
);
}