Repository navigation
[scheduler] Dependencies: render the FS arrow #22855
Description
Part of #22853 (event-scheduling umbrella) — FS behavior.
Pure rendering: draw the dependency as an arrow connecting predecessor and successor. No logic, no validation, no rescheduling — it only reads dependencies + event positions and draws.
Coordinate system
Timeline events aren't positioned in raw pixels, and not by time-column indices either — the two axes work differently:
- Horizontal — percentage-based.
useElementPositionInCollectiongives each occurrence a fractionalposition/durationrelative to the whole collection range, applied as--x-position: ${position * 100}%/--width: ${duration * 100}%(TimelineGridEvent). - Vertical — CSS grid rows.
useEventOccurrencesWithTimelinePositionassigns each occurrence a lane for stacking when events overlap (position.firstIndex, consumed asgrid-row; the timeline calls it withmaxSpan: 1, solastIndex === firstIndex). One row per resource; a row's height is its lane count.
So both arrow anchors must be computed from the model, not measured from the DOM:
- x = time fraction of the predecessor's end / successor's start × the scrollable width.
- y = resource row offset + lane index × lane height.
There's an exact precedent: TimelineGridCurrentTimeIndicator is positioned with calc(var(--title-column-width) + var(--unit-count) * var(--unit-width) * var(--x-position)) inside the row container. The SVG overlay should live in the same coordinate space (sibling of the rows inside RowContainer) and derive its anchor coordinates the same way.
DOM measurement (refs + getBoundingClientRect) is not viable: the timeline is virtualized on both axes and off-screen events are unmounted (see below), so an arrow endpoint may simply have no DOM node.
Reactivity
Anchors derive from reactive state (occurrence positions, lanes, dependencies), so re-drawing on move/resize/scroll follows from selector subscriptions — no ResizeObserver or layout reads needed. If a DOM read ever becomes necessary, it must go in useLayoutEffect (not useEffect), but the model-computed approach avoids the problem entirely.
Virtualization
The timeline is virtualized on both axes (@mui/x-virtualizer): resource rows outside the vertical window are unmounted, and events are additionally culled by the visible column range (EventList in EventTimelinePremiumContent). Consequences:
- Anchors must come from the model (see above) — an endpoint's DOM may not exist.
- Only draw arrows that intersect the viewport. The "predecessor visible, successor off-screen" case draws the arrow toward the model-computed (off-screen) anchor, clipped by the overlay.
Edge cases
- Events with no resource aren't rendered in the timeline at all (
occurrencesGroupedByResourceListSelectordrops them — there's no "unassigned" row). A dependency whose endpoint is such an event has no anchor → don't draw that arrow (the dependency itself still exists in data). - Milestones — not supported yet. The scheduler has no milestone / zero-duration concept today (the event model requires
start+end, and drag/resize clamps prevent ~0 durations). Nothing to implement in v1 — just avoid baking in assumptions that would break if milestones land later: don't assume the start/end anchors are two distinct points, don't divide by the event's width.
Scope
- Render an FS arrow from the predecessor's end edge to the successor's start edge.
- Anchors computed from the model (time fraction + row + lane); the arrow stays attached on move/resize/scroll.
- Correct behaviour under virtualization (off-screen endpoints).
Out of scope
- SS/FF/SF arrow geometry (changes which edges connect).
- Creating/editing the dependency by dragging ([scheduler] Dependencies: create, edit and delete via terminals #22856).
- Visual styling beyond a default.
Prior art (how competitors render the arrow)
Surveyed Bryntum, DHTMLX, Syncfusion, Frappe, MS Project / GanttPRO:
- Orthogonal routing (right-angle elbows: straight horizontal/vertical segments) is the de-facto standard — Bryntum, DHTMLX, Syncfusion, MS Project, GanttPRO. Corners are usually softened with a small radius (Bryntum). Frappe is the outlier: a single Bézier curve (
arrow_curve), which tangles visually once many links cross. - All render into an SVG overlay above the grid, with the arrow head (
marker-end) at the successor's start edge. - Same-row links are a straight horizontal segment; cross-row links add the vertical segment — confirms anchoring by row + lane matters.
→ Default to orthogonal routing with softened corners (Bryntum/DHTMLX style), not a curve.
Accessibility
The SVG arrow is decorative — a screen reader can't infer "A → B" from a path. Most competitors don't expose the relationship programmatically (DHTMLX docs admit "advanced dependency visualizations may not provide complete programmatic relationships for assistive technologies").
- Mark the arrow
aria-hidden. - Expose the dependency relationship in the DOM instead — e.g. accessible text /
aria-describedbyon the successor ("depends on "). Few libraries do this well → differentiation opportunity.
(Creating/editing a dependency by keyboard is #22856's concern, not this issue.)
Activity
github-actions commentedon Jul 22, 2026
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.
Apply the remaining review cleanups