Skip to content

[scheduler-premium] Dependencies - Render the FS arrow#23162

Merged
rita-codes merged 18 commits into
mui:masterfrom
rita-codes:22855-scheduler-dependencies-render-the-fs-arrow
Jul 22, 2026
Merged

[scheduler-premium] Dependencies - Render the FS arrow#23162
rita-codes merged 18 commits into
mui:masterfrom
rita-codes:22855-scheduler-dependencies-render-the-fs-arrow

Conversation

@rita-codes

@rita-codes rita-codes commented Jul 17, 2026

Copy link
Copy Markdown
Member

Part of #22853. Closes #22855.

Renders the Finish-to-Start dependency arrows in the timeline. Pure rendering: no validation, no rescheduling, no interaction (#22856#22858).

  • Anchors computed from the data model (time fraction × width, row offsets from the virtualizer, lane), never measured on the DOM, so arrows work under virtualization on both axes and follow moves/resizes through store subscriptions alone.
  • Orthogonal routing with softened corners: straight segment on the same lane, elbow across rows with two turn candidates keeping the one crossing the fewest events (best-effort avoidance, not full pathfinding), Bryntum-style short overlapping arrow between adjacent events, and S routes hugging the source event for backward links (so the detour never lands on a row border).
  • Viewport culling from the render context using precomputed bounding boxes; off-screen anchors are clipped by the overlay's viewBox.
  • A11y: the overlay is aria-hidden; successor events expose a visually hidden "Depends on X" description through aria-describedby.
  • Zero public API: the overlay reads the internal dependencies slice ([scheduler-premium] Add the dependencies data model #23117), which cannot be populated through the public component yet. pnpm proptypes + pnpm docs:api produce no changes.

How to review

  • Open the deploy preview — the "Timeline dependency arrows" section showcases every route shape with draggable events. The demo feeds the internal store parameters through a seam exported from the premium internals.
  • Suggested reading order: the pure extractions (computeElementPositionInCollection, computeOccurrencesFirstIndexLookup) → rowGeometry.tsdependencyArrowGeometry.ts with its tests → the overlay component → the a11y increment.
  • Heads-up: on machines where scrollbars take layout space (Windows/Linux, or macOS with a mouse connected) the event pills themselves drift off the model grid — that is the pre-existing [scheduler] Timeline event pills drift off the model grid when the vertical scrollbar takes space #23167, which the model-anchored arrows expose; it is not introduced here.

Flagged for review

  • Multi-resource endpoint events anchor on their first row in render order (the issue is silent on this case).
  • Visual constants (grey[400]/grey[600], 1px stroke, 7px filled arrowhead, radius 4, stub 8) are isolated as module constants pending design review.
  • Hardcoded English a11y string ("Depends on X") with a TODO to move into localeText at the public flip. At that point the wording should become one parametrized key per dependency type ("Cannot start until X finishes" for FS, "Cannot start until X starts" for SS, etc.) — the short "Depends on" form is deliberate in v1 to keep screen reader verbosity low with multiple predecessors.
  • The S route does not participate in obstacle avoidance yet.

@code-infra-dashboard

code-infra-dashboard Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-scheduler 🔺+191B(+0.05%) 🔺+11B(+0.01%)
@mui/x-scheduler-premium 🔺+6.09KB(+1.25%) 🔺+2.17KB(+1.62%)
@mui/x-chat 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@rita-codes rita-codes added type: new feature Expand the scope of the product to solve a new problem. scope: scheduler Changes related to the scheduler. labels Jul 17, 2026
@rita-codes rita-codes linked an issue Jul 17, 2026 that may be closed by this pull request
rita-codes and others added 4 commits July 17, 2026 13:21
Expose the pure cores of `useElementPositionInCollection`
(`computeElementPositionInCollection`) and of the lane assignment
(`computeOccurrencesFirstIndexLookup`, precedent:
`computeOccurrencesMaxIndex`) so the dependency arrows can compute
anchors for occurrences the virtualizer did not mount.

Co-Authored-By: Claude Fable 5 <[email protected]>
`activeSourceTitlesByTarget` / `activeSourceTitlesForTarget` expose the
predecessor titles of each event so the timeline can describe successor
events for assistive technology.

Co-Authored-By: Claude Fable 5 <[email protected]>
SVG overlay in the row container drawing one path per active
dependency, from the predecessor's end edge to the successor's start
edge. Anchors are computed from the data model (time fraction, row
offset, lane), never measured on the DOM, so arrows stay attached under
virtualization and follow moves/resizes through the store
subscriptions.

Routing is orthogonal with softened corners: straight segment on the
same lane, elbow across rows (two candidates, keeping the one crossing
the fewest events), short overlapping arrow between adjacent events and
an S route hugging the source event for backward links. Only the arrows
intersecting the viewport are rendered.

The overlay is decorative (`aria-hidden`); the relationship is exposed
as a visually hidden "Depends on" description on the successor event.

No public API: the overlay renders from the internal dependencies state
slice, which cannot be populated through the public component yet.

Co-Authored-By: Claude Fable 5 <[email protected]>
Internal sub-component of EventTimelinePremium, like the other entries
in the list.

Co-Authored-By: Claude Fable 5 <[email protected]>
@rita-codes
rita-codes force-pushed the 22855-scheduler-dependencies-render-the-fs-arrow branch 2 times, most recently from 3051bf2 to e2f4cb4 Compare July 17, 2026 16:14
One demo covering every route shape: same-lane straight arrow, elbow
turning before its target to avoid crossing events, short arrow between
adjacent events and backward S routes. Since the feature has no public
API yet, the demo recreates the timeline through the internal store
parameters and the content/styled-context seam exported from the
premium internals.

Co-Authored-By: Claude Fable 5 <[email protected]>
@rita-codes
rita-codes force-pushed the 22855-scheduler-dependencies-render-the-fs-arrow branch from e2f4cb4 to aafea88 Compare July 18, 2026 09:52
@rita-codes rita-codes self-assigned this Jul 20, 2026
rita-codes and others added 2 commits July 20, 2026 11:42
… anchors to the layout

Extract the virtualizer column range to fraction range conversion into
getVisibleFractionRange, shared by the event list and the dependency
arrows overlay, and add a browser test asserting the straight arrow
anchors on the vertical center of the rendered source event, pinning
the CSS-mirrored lane metrics against the real layout.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011iRutoeKtAZeTrCQaXh6k5
@rita-codes
rita-codes marked this pull request as ready for review July 20, 2026 10:53
@rita-codes rita-codes changed the title [DRAFT][scheduler] Dependencies - Render the FS arrow [scheduler] Dependencies - Render the FS arrow Jul 20, 2026
@rita-codes rita-codes changed the title [scheduler] Dependencies - Render the FS arrow [scheduler-premium] Dependencies - Render the FS arrow Jul 20, 2026
@rita-codes rita-codes added the plan: Premium Impact at least one Premium user. label Jul 20, 2026

@flaviendelangle flaviendelangle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR review

Renders Finish-to-Start dependency arrows on the scheduler timeline. The implementation is careful and unusually well-tested; nothing is merge-blocking. The pure-helper extractions (computeElementPositionInCollection, computeOccurrencesFirstIndexLookup, rowGeometry) are arithmetically identical to the code they replace, and the model-anchored/scroll-isolated arrow computation holds up. Highest-severity issues are one cosmetic edge-clipping bug and a couple of untested edge cases. I could not run the test suite in this environment (Node version mismatch), so test findings are from static reading only.

Bugs (4)

1. 🟠 Arrow segments at the timeline's left/right edge are clipped by the SVG's default overflow: hidden

Location: packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-dependency-arrows/EventTimelinePremiumDependencyArrows.tsx:25 (styled SVG sets no overflow), driven by dependencyArrowGeometry.ts:322-323 (exitX = source.x + STUB, entryX = target.x - TARGET_CLEARANCE).

const DependencyArrowsSvg = styled('svg', { name: 'MuiEventTimeline', slot: 'DependencyArrows' })(
  ({ theme }) => ({ position: 'absolute', top: 0, left: 'var(--title-column-width)', /* no overflow */ }),
);

The <svg> root defaults to overflow: hidden, and the viewBox x-range is exactly 0 … eventsWidth. A backward S-route into an event that starts within ~12px of the timeline start makes entryX = target.x - 12 negative, so the entry elbow — and most of the arrowhead (which occupies [target.x-7, target.x]) — is drawn at x < 0 and clipped. The symmetric case: a predecessor ending at the very right edge pushes exitX = source.x + 8 > eventsWidth, clipping the exit stub.

Failure scenario: The first task of a day (starts at hour 0) has a predecessor it depends on; its incoming arrowhead is cut off and the arrow appears to point at nothing.

Fix: Set overflow: 'visible' on DependencyArrowsSvg. This matches the stated design intent ("below the pinned title column, which covers the arrows on horizontal scroll") — the title column's higher z-index still covers any overflow into the x < 0 region.

2. 🟡 ! non-null assertion is a latent crash, safe only by a cross-selector invariant

Location: packages/x-scheduler-internals-premium/src/event-timeline-premium-selectors/eventTimelinePremiumDependencySelectors.ts:52

const title = processedEventLookup.get(dependency.source)!.title;

Verified safe today: activeModelListSelector only keeps dependencies whose source and target both classifyDependencyEvent(...) === 'ok', which requires presence in processedEventLookup. Flagging only because the assertion silently depends on both selectors reading the same lookup; a future refactor feeding this a differently-filtered list would throw on .title.

Fix: processedEventLookup.get(dependency.source)?.title ?? '' (or keep the ! and add a comment pinning the invariant — the comment is already there, so this is optional).

3. 🟡 Transient resourcesrowsMeta.positions desync drops an arrow for one frame

Location: dependencyArrowGeometry.ts:206-208

When a resource is added, groupedByResourceList (driving anchors) can update a commit before the virtualizer's rowsMeta.positions grows; for that render rowPositions[anchor.rowIndex] is undefined and the dependency is skipped. Self-heals on the next commit. The == null guard correctly treats row-0 position 0 as valid, so no off-by-one — purely a one-frame flicker.

4. ℹ️ Self-dependency (source === target) produces undefined geometry

computeDependencyArrows has no guard for dependency.source === dependency.target. Both anchors resolve to the same occurrence → forwardX < 0, same y → it falls into the backward S-route and draws a loop around the event. Not reachable through any current API path, but the data model does not reject it. Consider skipping such dependencies (also see Tests #1).

Tests (4)

1. 🟠 Self-dependency behavior is unpinned

Location: dependencyArrowGeometry.test.ts

No test covers { source: 'a', target: 'a' }. The code produces a loop-shaped S-route with no assertion on whether that is intended or should be skipped — a malformed-but-plausible input yields undefined-by-spec output.

2. 🟡 getVisibleFractionRange has no JSDOM unit test

Location: getVisibleFractionRange.ts

The +1 column offset and Math.max(0, …) clamp are only exercised by browser-only (skipIf(isJSDOM)) integration tests; computeDependencyArrows doesn't use the helper. In a JSDOM run the culling arithmetic is entirely uncovered. A 3-line direct unit test would pin it cheaply.

3. 🟡 Weak "arrow follows the event" assertion

Location: dependencyArrows.EventTimelinePremium.test.tsx (should update the arrow when the predecessor event moves)

The test calls store.updateEvent(...) and asserts only updatedD !== initialD. No real drag/resize gesture is exercised (despite the demo enabling both), and "different" would pass even if the arrow recomputed in the wrong direction. Assert a concrete property of the new path.

4. 🟡 Untested route branches

Location: dependencyArrowGeometry.test.ts

Three reachable branches lack coverage: the forward-but-too-close S-route at a different height (0 < forwardX < STUB+CLEARANCE, all existing S-route tests are backward), the single-candidate forward elbow (lateTurnX <= earlyTurnX, i.e. forwardX at the ~20px boundary), and the eventsWidth <= 0 early return. Each is a one-case addition. (Minor: buildRoundedOrthogonalPath < 2 points → '', formatCoordinate fractional rounding, and empty-input computeOccurrencesFirstIndexLookup are also unpinned — ℹ️.)

Simplifications (2)

1. 🟡 EMPTY_TITLES reinvents the shared EMPTY_ARRAY sentinel

Location: eventTimelinePremiumDependencySelectors.ts:30

const EMPTY_TITLES: readonly string[] = [];

The codebase already exports EMPTY_ARRAY from @base-ui/utils/empty for exactly this "stable empty ref from a selector" purpose (used in schedulerOccurrenceSelectors, schedulerResourceSelectors, and the sibling dependency-utils.ts). EMPTY_ARRAY is readonly [] and satisfies readonly string[]. Import it and delete the bespoke sentinel; the same-instance test passes identically.

2. 🟡 Three exported geometry constants have no external importer

Location: dependencyArrowGeometry.ts:20, 24, 39

DEPENDENCY_ARROW_STUB, DEPENDENCY_ARROW_CORNER_RADIUS, and DEPENDENCY_ARROW_DETOUR_CLEARANCE are exported but grep confirms no importer anywhere (not even tests) — only DEPENDENCY_ARROWHEAD_SIZE is consumed by the component, and DEPENDENCY_ARROW_TARGET_CLEARANCE is deliberately un-exported. Drop export from the three internal-only constants to keep the internal slice's surface consistent. (ℹ️: computeDependencyArrows also recomputes Math.min/max(sourceAnchor.rowIndex, targetAnchor.rowIndex) twice — hoist once; and the two computeOccurrences* helpers share an identical 3-line body that could be one internal function with thin field-pickers.)

Docs (1)

1. 🟡 rowGeometry.ts comment attributes the font-size to the wrong root

Location: packages/x-scheduler-premium/src/event-timeline-premium/content/rowGeometry.ts:15

// `em` in the grid track resolves against the EventsCell's font-size, which
// inherits `theme.typography.body2.fontSize` from the Content root.

The EventTimelinePremiumContentRoot (slot Content) sets no fontSize; it is set on the outer EventTimelinePremium root (EventTimelinePremium.tsx:86). That's precisely why the demo and test host must re-declare fontSize: '0.875rem' — Content does not provide it. Should read "from the EventTimelinePremium root". (Inaccuracy was moved verbatim from the old inline helper, but now lives in new code.)

ℹ️ Also: the maxSpan: 1 qualifier in computeOccurrencesFirstIndexLookup's JSDoc is misleading (firstIndex is independent of maxSpan); the demo's // One event pair per arrow shape over-promises (9 events / 6 deps / 4 shapes); and value={store as any} in the demo and test harness is the file's only unexplained cast.

Verdict

Approve after nits — no critical or high-severity issues; the edge-clipping arrow bug (#1) and the self-dependency gap are the only items worth addressing before merge, and both are low-frequency. Test suite could not be executed here, so the test-gap findings rest on static reading.


🤖 Review generated with Claude Code

rita-codes and others added 3 commits July 21, 2026 12:59
`overflow: visible` was a no-op on the left edge: x < 0 is always covered
by the pinned title column, scrolled or not. Clamp the route instead so
edge anchors keep their elbow inside the visible area.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWfxgH3syTq7t7WJksGAAs
Clamping the elbow to x = 0 left the vertical reading as part of the
column border. Reuse the adjacent-events trade-off instead: the stubs
keep their length and overlap the event when the edge leaves no room.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWfxgH3syTq7t7WJksGAAs
@rita-codes

rita-codes commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Addressed in d6c3c3a, 20ffd61, d22b54b and 77a36f4:

  • Bugs 1: fixed, in two steps — overflow: visible turned out to be a no-op on the left edge (the x < 0 region sits under the pinned title column at any scroll position), and clamping the elbow to x = 0 read as part of the column border. The S route now clamps its verticals to the events area and keeps the stubs at fixed length, riding over the event when the edge leaves no room — the same overlap trade-off as the short adjacent-events arrow. Unit tests pin both edges.
Screenshot 2026-07-21 at 13 29 49
  • Bugs 2: keeping the ! — the invariant is pinned by the comment above it, and a soft fallback would hide a future refactor bug rather than surface it.
  • Bugs 3: agreed — one-frame skip that self-heals; the == null guard exists for exactly that window. Leaving as is.
  • Bugs 4 / Tests 1: self-dependencies are owned by [scheduler] Dependencies: cycle guard #22858 — the cycle guard explicitly rejects the trivial self-loop, including the data-ingestion path. Filtering here would split that logic across two places, so deferring to that issue.
  • Tests 2: added a direct unit test for getVisibleFractionRange.
  • Tests 3: the assertion now checks the arrow's start follows the moved end edge while the entry point and height stay put. Drag/resize gestures stay out of this PR: it renders from the model, gesture coverage belongs to the interaction slice.
  • Tests 4: added the three missing branch cases (forward-but-too-close S route, single-candidate elbow at the boundary, eventsWidth <= 0).
  • Simplifications 1/2: done — shared EMPTY_ARRAY, and un-exported the three internal constants.
  • Docs 1: fixed — it now points at the EventTimelinePremium root.
  • The ℹ️ asides (hoisting the row-index min/max, the maxSpan JSDoc qualifier, the demo dataset comment, the unexplained store as any casts) are addressed in 77a36f4.

An event assigned to several resources renders once per resource row:
draw one arrow per pair of appearances instead of anchoring each
endpoint on its first row.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWfxgH3syTq7t7WJksGAAs

@flaviendelangle flaviendelangle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR review

The pure-geometry extractions (computeElementPositionInCollection, computeOccurrencesFirstIndexLookup, rowGeometry, getVisibleFractionRange) are behavior-preserving and well covered, and the arrow-routing engine is thoroughly unit-tested. One merge-relevant issue survives: the new accessibility increment leaks its "Depends on X" text into the successor event's accessible name (not just its description), because the event names itself with a self-referential aria-labelledby, so screen readers announce the dependency twice. Everything else is clean; nothing else blocks.

Bugs (1)

1. 🔴 The "Depends on X" description also pollutes the event's accessible name and is announced twice

Location: packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-event/EventTimelinePremiumEvent.tsx:216

const sharedProps = {
  id,
  // ...
  'aria-labelledby': `${ariaLabelledBy} ${id}`, // self-reference → name from own subtree
};
// ...
<TimelineGrid.Event
  {...sharedProps}
  aria-describedby={dependsOnTitles.length > 0 ? `${id}-dependencies` : undefined}
>
  <EventTimelinePremiumEventLinesClamp>{occurrence.title}</EventTimelinePremiumEventLinesClamp>
  {dependsOnTitles.length > 0 && (
    <span id={`${id}-dependencies`} style={visuallyHidden}>
      Depends on {dependsOnTitles.join(', ')}
    </span>
  )}

The event computes its accessible name from aria-labelledby={${ariaLabelledBy} ${id}}, where ${id} points at the event element itself — so the name is derived from the event's own text subtree ("name from content"). The new description <span> is hidden with visuallyHidden (a clip style), not aria-hidden, so it stays in the accessibility tree and is folded into that name. The same span is then referenced again by aria-describedby.

Net result for a successor: the dependency text lands in both the name and the description. A screen reader announces roughly "9 AM, Event B Depends on Event A, Event C" (name) followed by "Depends on Event A, Event C" (description) — the predecessor list is spoken twice, and the event's name is no longer just its title.

Failure scenario: With NVDA/VoiceOver on the dependency demo, focusing "Event B" (which depends on A and C) reads the predecessors twice and mixes them into the event's name, instead of announcing the title as the name and the dependency once as a supplementary description.

Fix: Keep the span out of the self-referential name while preserving the description. Add aria-hidden to the span — an element explicitly targeted by aria-describedby is still used for the description even when aria-hidden, but an aria-hidden descendant is excluded from the name-from-content traversal:

<span id={`${id}-dependencies`} style={visuallyHidden} aria-hidden>
  Depends on {dependsOnTitles.join(', ')}
</span>

(Alternatively, render the description node as a sibling outside the aria-labelledby-referenced element.) Worth confirming with a browser test that reads the computed name/description rather than textContent.

Tests (1)

1. 🟠 The a11y test asserts textContent, so it cannot catch the name pollution / double announcement

Location: packages/x-scheduler-premium/src/event-timeline-premium/tests/dependencyArrows.EventTimelinePremium.test.tsx:2635

expect(document.getElementById(descriptionId)!.textContent).to.equal(
  'Depends on Event A, Event C',
);

The test only checks that the hidden span's textContent is correct and that aria-describedby is wired up. It never checks the successor's accessible name, so the leak in finding #1 (dependency text appearing in the name and being announced twice) passes unnoticed. This is exactly the assertion that would have flagged the bug.

Failure scenario: A future refactor that fixes or re-breaks the name/description split produces no test signal, because coverage stops at the span's text rather than the resolved name/description a screen reader would use.

Fix: In a browser-mode test, assert the event's accessible name is just its title (e.g. screen.getByRole(..., { name }) scoped to the title, or compute the name) and that the description is present exactly once — pinning both sides of the name/description split.

Simplifications (0)

No findings.

Docs (0)

No findings.

Verdict

Request changes — the accessibility increment double-announces dependencies and pollutes the event's accessible name; the fix is a one-line aria-hidden plus a stronger test. The geometry/refactor work is solid and the moved exports resolve cleanly. (Severity reflects that this is the a11y behavior the PR ships; note the feature is not yet publicly reachable, so you may calibrate down if you prefer to land it and fix at the public flip — but the fix is trivial enough to do now.)


🤖 Review generated with Claude Code

@rita-codes

rita-codes commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

Fixed in c8722b2 🙌

  • Added aria-hidden to the description span so it's excluded from the name-from-content traversal while aria-describedby still picks it up, as suggested.
  • Replaced the textContent assertion with the toHaveAccessibleName / toHaveAccessibleDescription matchers from @mui/internal-test-utils, which compute the resolved name/description via dom-accessibility-api. They run in both JSDOM and browser mode, so I went with that instead of a browser-only test. The new name assertion reproduced the double announcement before the fix ("Resource 1 Event BDepends on Event A, Event C") and now pins the name to just the title.

@flaviendelangle flaviendelangle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've completed a thorough review of the diff — the geometry core (dependencyArrowGeometry.ts), the overlay component, the pure-helper extractions, the selectors, and the a11y increment — and cross-checked the key invariants against the surrounding code. Here is the report.


PR review

Pure-rendering FS dependency arrows for the premium timeline. The diff is large but disciplined: the three refactors (computeElementPositionInCollection, rowGeometry, getVisibleFractionRange) are behavior-preserving extractions, and the new geometry/overlay/a11y code is backed by unusually complete unit and browser tests. I verified the load-bearing invariants (row-order alignment between groupedByResourceList and the virtualizer rows, the "active dependency always resolves" precondition behind the non-null assertion, the aria-describedby/aria-hidden name-vs-description split, and edge clamping in every route shape). I found nothing merge-blocking and no actionable correctness, test, or docs defects.

Bugs (0)

No findings.

Specifically checked and cleared:

  • Row-order alignment. EventTimelinePremiumDependencyArrows anchors on resources[rowIndex] / rowPositions[rowIndex]; the virtualizer's rows are built from the same groupedByResourceList selector in the same order (EventTimelinePremiumContent.tsx:678), so indices line up. The transient resources/rowsMeta desync is guarded in buildArrow (skips when rowPositions[...] == null).
  • Non-null assertion in activeSourceTitlesByTargetSelector (processedEventLookup.get(dependency.source)!) is safe: activeModelListSelector already filters to dependencies where both endpoints classifyDependencyEvent === 'ok'.
  • aria-describedby after {...sharedProps} does not clobber anything — TimelineGrid.Event sets no aria-describedby, and sharedProps carries none; passing undefined when there are no predecessors is equivalent to omitting it.
  • A11y name/description split — the visually-hidden Depends on … span is aria-hidden (excluded from the self-referential name-from-content) yet still resolved by aria-describedby; both browser assertions confirm it.
  • computeElementPositionInCollection extraction is byte-for-byte equivalent to the old inline EventList math (fractionEnd = position + duration === clamp(endMinutes)/totalMinutes), and default dayStartMinute/dayEndMinute match the old hook destructure.
  • Edge clamping (S-route exit/entry riding over events at x=0 / x=eventsWidth) and division-by-tickCount paths are covered/guarded; tickCount is always positive and the layer early-returns on eventsWidth <= 0.

Tests (0)

No findings. Coverage is strong across both route-shape branches (including S-detour above/below source and the same-height case), obstacle-avoidance tie-breaks, multi-row fan-out keys, culling, virtualization, and a browser anchor-alignment pin against real layout. The author's TODO(multi-resource rendering) correctly notes the one integration gap (per-appearance rendering), which is already covered at the unit level.

Simplifications (0)

No findings. The extractions actively reduce duplication (getVisibleFractionRange now shared between EventList and the overlay; rowGeometry shared with the virtualizer height path).

Docs (0)

No findings. The hardcoded English "Depends on X" string and the visual constants are already flagged with TODOs and in the PR's own "Flagged for review" list; the S-route not participating in obstacle avoidance is likewise author-acknowledged and reasonable for v1.

Verdict

Approve — no correctness, accessibility, test, or docs defects survived verification; the remaining rough edges (S-route obstacle avoidance, multi-resource anchoring, localization) are explicitly tracked as follow-ups, not defects in this diff.


🤖 Review generated with Claude Code

@rita-codes
rita-codes merged commit f14af60 into mui:master Jul 22, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plan: Premium Impact at least one Premium user. scope: scheduler Changes related to the scheduler. type: new feature Expand the scope of the product to solve a new problem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[scheduler] Dependencies: render the FS arrow

2 participants