Skip to content

fix(sidebar): honor category drop indicator contract#5

Merged
O0000-code merged 1 commit into
mainfrom
agent/fix/NOISSUE-category-drop-indicator-contract
May 6, 2026
Merged

fix(sidebar): honor category drop indicator contract#5
O0000-code merged 1 commit into
mainfrom
agent/fix/NOISSUE-category-drop-indicator-contract

Conversation

@O0000-code

@O0000-code O0000-code commented May 6, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the category hierarchy drag/drop contract so a visible blue drop-into indicator is the single source of truth for committing a category as a child.

Changes

  • Gate mouse drop-into projections on a resolved above/below insertion side so top-edge hovers cannot become child drops.
  • Store the last visible drop-into projection and use it at drag end instead of recomputing a hidden final projection.
  • Add regression coverage for the visible-indicator commit contract.

Test Plan

  • npx tsc --noEmit
  • npm run test
  • cd src-tauri && cargo test

Notes

  • cd src-tauri && cargo clippy -- -D warnings currently fails on existing unrelated Rust lint warnings/errors outside this change, mainly uninlined format args and derivable impls.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added keyboard support for drag-and-drop operations on the sidebar categories list, enabling users to reorder categories and modify their hierarchy using keyboard controls with feature parity to pointer-driven actions.

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces keyboard drag support for the sortable categories list by adding state to distinguish keyboard-initiated drags, a new utility function to determine visible drop projections, and lifecycle management to sync projection visibility with the UI during drag operations.

Changes

Keyboard Drag Support

Layer / File(s) Summary
Utility Function
src/components/sidebar/dnd/treeUtilities.ts
New getVisibleDropIntoProjection function decides whether a computed projection should be visible as a drop target indicator, guarding against null inputs, mismatched parents, root scenarios, and invalid projections.
State & Lifecycle
src/components/sidebar/SortableCategoriesList.tsx
Added isKeyboardDrag state to track keyboard-initiated drags, visibleDropIntoProjectionRef to hold the painted drop projection, and a useLayoutEffect to sync the visible projection using the new utility.
Event Handlers & Projection Logic
src/components/sidebar/SortableCategoriesList.tsx
Updated handleDragStart to initialize keyboard drag state, modified projection gating to suppress projections in non-child drags without pointer information, updated handleDragEnd to compute final projection from the visible ref, and extended cleanup in handleDragCancel.
Tests & Validation
src/components/sidebar/dnd/__tests__/treeUtilities.test.ts
New test suite for getVisibleDropIntoProjection validating acceptance of valid projections and rejection of hidden, invalid, root-level, and mismatched-parent cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A keyboard's tap now moves the tree,
With projections clear for all to see,
State and refs in sync they stay,
Drag and drop the rabbit's way! 🌿✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(sidebar): honor category drop indicator contract' directly and clearly summarizes the main change: fixing the category drop indicator behavior to honor its contract as the single source of truth for drop-into operations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/fix/NOISSUE-category-drop-indicator-contract

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/sidebar/SortableCategoriesList.tsx (2)

791-807: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t record a parent change when the drop lands outside every droppable.

finalProjection can still hold the last visible child projection when event.over is null. This block writes that parent into dropProjectionRef, but the later if (over) guard skips the IPC, so the screen-reader announcement path can report a successful reparent that never happened.

Suggested fix
       const activeStr = String(active.id);
       const activeRow = baseFlatRef.current.find((it) => String(it.id) === activeStr);
       const oldParentId: string | null = activeRow?.parentId ?? null;
       let newParentId: string | null;
-      if (finalProjection) {
+      if (over && finalProjection) {
         newParentId = finalProjection.parentId;
       } else {
         // No projection (dwell never armed) — same-level reorder; parent
         // stays unchanged.
         newParentId = oldParentId;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/sidebar/SortableCategoriesList.tsx` around lines 791 - 807,
The code sets newParentId from finalProjection.parentId even when the drop
target is outside every droppable (event.over is null), causing a false reparent
in dropProjectionRef; change the logic so newParentId is set to
finalProjection.parentId only when the actual over target exists (e.g., over is
truthy), otherwise keep newParentId = oldParentId; update the block that
computes newParentId (around activeStr, baseFlatRef.current, finalProjection,
oldParentId) so dropProjectionRef.current never records a parent change when
over is null.

403-423: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keyboard drags must bypass pointer-geometry resolution in getProjection.

When isKeyboardDrag is true, pointerBelowOver can become non-null during onDragMove (since keyboard sensors emit DragMove events). Passing this pointer-derived value to getProjection activates position-aware mode, causing the insertion point to be determined by pointer geometry instead of the keyboard coordinate getter's X-offset logic. This contradicts the documented intent (line 390) and defeats the keyboard sensor's depth-control mechanism.

The suggested fix correctly enforces the legacy path for keyboard drags by passing undefined instead of pointerBelowOver ?? undefined when isKeyboardDrag is true.

Suggested fix
   const projected = useMemo(() => {
     if (activeId === null || overId === null) return null;
     // Asymmetric gate: only ROOT-active demote requires dwell.
     if (!isChildActive && dwellState === 'OUT') return null;
@@
     // the legacy arrayMove path because there is no
     // pointer side; the coordinate getter supplies depth intent via X.
     if (!isChildActive && !isKeyboardDrag && pointerBelowOver === null) return null;
+    const projectionPointerSide = isKeyboardDrag ? undefined : (pointerBelowOver ?? undefined);
     return getProjection(
       displayFlat,
       activeId,
       overId,
       offsetLeft,
       INDENT_STEP_PX,
-      pointerBelowOver ?? undefined,
+      projectionPointerSide,
       baseFlat,
       activeOriginalParentId,
     );
   }, [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/sidebar/SortableCategoriesList.tsx` around lines 403 - 423,
The projected useMemo passes pointerBelowOver (via pointerBelowOver ??
undefined) into getProjection which lets pointer geometry override keyboard drag
depth intent; update the call in the projected memo so when isKeyboardDrag is
true you pass undefined for the pointer side (i.e. use pointerBelowOver ??
undefined only when !isKeyboardDrag, otherwise pass undefined) so getProjection
uses the keyboard X-based depth logic; locate this change in the projected
useMemo that calls getProjection and adjust the pointer argument accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/components/sidebar/SortableCategoriesList.tsx`:
- Around line 791-807: The code sets newParentId from finalProjection.parentId
even when the drop target is outside every droppable (event.over is null),
causing a false reparent in dropProjectionRef; change the logic so newParentId
is set to finalProjection.parentId only when the actual over target exists
(e.g., over is truthy), otherwise keep newParentId = oldParentId; update the
block that computes newParentId (around activeStr, baseFlatRef.current,
finalProjection, oldParentId) so dropProjectionRef.current never records a
parent change when over is null.
- Around line 403-423: The projected useMemo passes pointerBelowOver (via
pointerBelowOver ?? undefined) into getProjection which lets pointer geometry
override keyboard drag depth intent; update the call in the projected memo so
when isKeyboardDrag is true you pass undefined for the pointer side (i.e. use
pointerBelowOver ?? undefined only when !isKeyboardDrag, otherwise pass
undefined) so getProjection uses the keyboard X-based depth logic; locate this
change in the projected useMemo that calls getProjection and adjust the pointer
argument accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 119dea7a-5039-4ac1-8c67-bbb45a5efedd

📥 Commits

Reviewing files that changed from the base of the PR and between be7bace and d0503cc.

📒 Files selected for processing (3)
  • src/components/sidebar/SortableCategoriesList.tsx
  • src/components/sidebar/dnd/__tests__/treeUtilities.test.ts
  • src/components/sidebar/dnd/treeUtilities.ts

@O0000-code
O0000-code merged commit d0503cc into main May 6, 2026
7 of 11 checks passed
@O0000-code
O0000-code deleted the agent/fix/NOISSUE-category-drop-indicator-contract branch May 12, 2026 09:32
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