fix(studio): scope lane ids per timeline and stop inventing a track row

Two latent defects in the announcement path this branch adds.

- lanesId was keyed by render row alone, so a second TimelineLanes on the
  page (a mini-timeline beside the main one) would mint the same
  timeline-lanes-track-0 and every caret's aria-controls would resolve to
  whichever instance mounted first. The prefix now comes from useId, with the
  colons stripped so the id stays a legal CSS selector.
- trackDisplayNumber returned trackOrder.length + 1 for a key it could not
  find, which is indistinguishable from a real row: the label announced a row
  the user can see is wrong and nothing upstream could tell it had guessed. It
  returns null now, and trackDisplaySuffix drops the number from the label
  rather than inventing one.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-28 21:21:27 +02:00
parent 477916cbe2
commit ba0d6406d2
6 changed files with 106 additions and 14 deletions
@@ -15,10 +15,24 @@ export function timelineTrackOrder(elements: readonly { track: number }[]): numb
}
/**
* A track key's 1-based display row. A key not in `trackOrder` (a drag preview
* onto a brand-new track, say) reads as the row it would land on at the end.
* A track key's 1-based display row, or null when the key is not in
* `trackOrder` at all.
*
* Both callers build `trackOrder` from the same elements the key came from, so
* null is unreachable by construction today. It is null rather than a number
* because the only numbers available to return (the end row, the last row) are
* indistinguishable from a real answer: a label would announce a row the user
* can see is wrong, and nothing upstream would ever learn it had guessed.
*/
export function trackDisplayNumber(trackOrder: readonly number[], track: number): number {
export function trackDisplayNumber(trackOrder: readonly number[], track: number): number | null {
const row = trackOrder.indexOf(track);
return row < 0 ? trackOrder.length + 1 : row + 1;
return row < 0 ? null : row + 1;
}
/**
* The `" 3"` in `Hide track 3`, empty when there is no display row to name.
* Announcing "Hide track" is thin; announcing an invented row is wrong.
*/
export function trackDisplaySuffix(displayNumber: number | null): string {
return displayNumber === null ? "" : ` ${displayNumber}`;
}