Skip to content

[scheduler] Timeline event pills drift off the model grid when the vertical scrollbar takes space #23167

Closed

Description

@rita-codes

Found while building the dependency arrows (#22855): the arrows anchor on the data model, and that exposed that the event pills can render away from their model position.

Image Image

What happens

When the vertical scrollbar takes real layout space, the timeline rows become wider than title column + ticks, and the event pills render shifted to the right of where the model says they are. The shift grows with the distance from the start of the collection: invisible near the left edge, several pixels a few days in. Without dependencies the symptom is subtle — a pill's edges no longer line up with the header's time gridlines. With arrows it is obvious: they anchor on the model, so they visibly start "inside" the predecessor pill.

Why most machines don't show it

Classic scrollbars (with real width) are the default on Windows and Linux; on macOS it is enough to have a mouse connected with the default "Show scroll bars: Automatically" setting. Overlay scrollbars (trackpad-only setups, and CI) report a width of 0 — the rows never get wider, so the bug is invisible there. That's why it went unnoticed.

Reproduction

  1. Use an environment where scrollbars take layout space (see above).
  2. Render an EventTimelinePremium with enough resources to get vertical scroll (hasScrollY) and an event on day 3+ of a dayAndHour window.
  3. Compare the pill's right edge with the header gridline of its end hour: it renders past it.

Measured on an affected machine (dayAndHour, 96 ticks × 64px = 6144px, title column 107px, scrollbar 11px):

value expected actual
--row-width 6251 (107 + 6144) 6262
events cell width 6144 6155
pill end vs model x (day 3, fraction 0.61) 0 +6.76px

Root cause

Three things combine:

  1. x-virtualizer makes rows wider than the columns on purpose — inherited from the Data Grid, where rows must extend under the native vertical scrollbar:

    // packages/x-virtualizer/src/features/dimensions.ts
    const rowWidth = Math.max(
      viewportOuterSize.width,
      columnsTotalWidth + (hasScrollY ? scrollbarSize : 0),
    );
  2. The timeline's EventTimelinePremiumEventsCell has flex: 1, so it stretches to fill that wider row instead of staying at the ticks width, calc(var(--unit-count) * var(--unit-width)) (with flex-basis: 0% its width declaration is ignored).

  3. The pills are positioned with percentages of the cell (--x-position, --width), so they scale with the stretched cell — while the header, the current-time indicator, and the dependency arrows all use the exact ticks width. The mismatch between those two widths is the drift.

The invariant to restore: the events cell's rendered width must equal tickCount × tickWidth, because every percentage inside it assumes that.

Fix

Cap the events cell at the model width: maxWidth: calc(var(--unit-count) * var(--unit-width)). The row can stay wider; the cell — and every percentage inside it — stops following it.

Preferred over the alternative (stopping the virtualizer from adding the scrollbar gutter to rowWidth for the timeline layout) because:

  • It restores the invariant against both sources of extra width: the scrollbar gutter and the Math.max with viewportOuterSize.width — a viewport wider than the content produces the same drift, and removing only the gutter term would leave that case broken.
  • It is local to the timeline (one CSS declaration on the cell) instead of a behavior option in x-virtualizer, which is shared with the Data Grid.

One detail to take care of: the row's bottom border lives on the cell, so once capped it would stop short of --row-width when the row is wider — the border needs to keep covering the full row (move it to the row, or extend it some other way).

Activity

  1. self-assigned this
    on Aug 21, 2026
  2. rita-codes commented on Aug 25, 2026

    @rita-codes
    MemberAuthor

    Fixed by #23212, which landed the first candidate fix from the description while reworking the events cell for the min/max hour support: the cell is now pinned to the ticks width (flex: 0 0 auto + width: calc(var(--unit-count) * var(--unit-width))) instead of stretching to --row-width, and the new BodyRowFiller sibling carries the row divider across the leftover space — which also covers the bottom-border caveat from the description.

    Verified against master with the issue's scenario (dayAndHour, 20 scrollable resources, 11px scrollbars): the row width includes the gutter (6259px = 104 title + 6144 ticks + 11 scrollbar) and the events cell stays at exactly 6144px = 96 × 64, with the dependency arrows anchored on the pill edges. Restoring the old flex: 1 cell styles reproduces the drift (cell at 6155px, +11px — the numbers from the table above), confirming it was this bug. The pinned cell also covers the second drift source (the Math.max with viewportOuterSize.width when the viewport is wider than the content).

  3. github-actions commented on Aug 25, 2026

    @github-actions
    Contributor

    This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue.
    Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

scope: schedulerChanges related to the scheduler.type: bugIt doesn't behave as expected.

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions