Skip to content

fix: DayTimeline DnD drop always lands at slot 0 (z-order race)#299

Merged
jlunder00 merged 2 commits intomainfrom
fix/daytimeline-slot-position
May 6, 2026
Merged

fix: DayTimeline DnD drop always lands at slot 0 (z-order race)#299
jlunder00 merged 2 commits intomainfrom
fix/daytimeline-slot-position

Conversation

@jlunder00
Copy link
Copy Markdown
Owner

Problem

Dragging a task from the plan's anchor list and dropping it onto the DayTimeline always scheduled it at 6:00 AM (slot 0) instead of the visually targeted time slot.

Root cause — z-order race:

  1. Slot divs are rendered before TaskCard blocks in DOM order; TaskCards have higher z-order by default.
  2. When overSlotIndex was computed from clientY (via slotIndexFromClientY), any inaccuracy in getBoundingClientRect() at drag-start (e.g., during the first dragover into the component) could produce slot 0.
  3. The highlighted slot 0 got z-30 (above TaskCards). Subsequent drop events hit slot 0 regardless of the cursor position, calling onSlotDrop(e, 0) → always 6:00 AM.

Fix

Set dragActive = ref(false) in DayTimeline. On @dragenter it flips true; on @dragleave (container exit), @drop (container fallback), and @drop.stop (slot-direct) it flips false.

While dragActive is true, TaskCard blocks receive pointer-events-none. Drops over event-block areas pass through directly to the underlying slot divs, calling onSlotDrop(e, i) with the exact slot index — no slotIndexFromClientY math involved, no z-order race.

slotIndexFromClientY and onTimedAreaDrop remain as a safety net for drops landing in the hour-labels gutter column (left strip with no slot divs).

Tests

  • Regression: onTimedAreaDrop maps clientY to the correct 15-min slot using rect.top + scrollTop — mocks getBoundingClientRect and verifies clientY=190 / top=100 maps to slot 4 (07:00). Passes before and after (math was always correct).
  • Structural (TDD red→green): TaskCard event blocks receive pointer-events-none class while a drag is active — fails before fix, passes after.

All 539 tests pass. Build is clean (zero vue-tsc errors).

Note on approach

Team-lead's post-compact reminder suggested await nextTick() in onTimedAreaDrop. This is incorrect — e.clientY is immutable at event-fire time and getBoundingClientRect() is synchronous; there is nothing to settle with a tick. The structural pointer-events-none fix eliminates the z-order race entirely rather than patching the coordinate math.

@jlunder00 jlunder00 merged commit 8be2f60 into main May 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant