Skip to content

fix: only initialize useSortable if dragEnabled is true#14261

Open
isaacbraun wants to merge 3 commits intodevfrom
isaacbraun/14259-fix-sortable-null-parent-error
Open

fix: only initialize useSortable if dragEnabled is true#14261
isaacbraun wants to merge 3 commits intodevfrom
isaacbraun/14259-fix-sortable-null-parent-error

Conversation

@isaacbraun
Copy link
Copy Markdown
Contributor

Related Issue: #14259

Summary

This PR refactors the useSortable setUpSortable function to always tear down before setting up, and crucially only set it up if the component's dragEnabled property is true.

Without this change, nested List Item instances and Lists inside Blocks are being initialized as Sortable containers regardless of their dragEnabled value, letting Sortable treat them as active drop targets and creating the parentNode null crash during drag-over.

Additionally, adds a Browser test for the useSortable controller. NOTE: this test is AI generated, I would appreciate a closer review.

@github-actions github-actions bot added the bug Bug reports for broken functionality. Issues should include a reproduction of the bug. label Apr 14, 2026
this.mutationObserver?.disconnect();
}

private setUpSorting(): void {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This whole function is no longer needed, as the !dragEnabled check is moved to the controller and it would only be calling this.sortable.reset().

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a drag-and-drop regression in Calcite’s sortable controller by ensuring SortableJS is only initialized for components that actually have dragEnabled set to true, preventing nested Lists/List Items and Blocks from being treated as active drop targets (and avoiding the reported parentNode null crash during drag-over).

Changes:

  • Updated useSortable to always tear down before re-initializing, and to no-op setup when dragEnabled is false.
  • Added a browser-level controller test to verify SortableJS is created/destroyed based on dragEnabled.
  • Simplified List/BlockGroup sorting setup to rely on sortable.reset() now that the controller correctly guards initialization.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
packages/components/src/controllers/useSortable.ts Ensures Sortable is torn down before setup and only created when dragEnabled is true.
packages/components/src/controllers/useSortable.browser.spec.tsx Adds coverage verifying SortableJS create/destroy behavior driven by dragEnabled.
packages/components/src/components/list/list.tsx Adjusts sorting setup to always reset (controller now safely handles dragEnabled=false).
packages/components/src/components/block-group/block-group.tsx Removes redundant setUpSorting wrapper and directly resets sortable in relevant lifecycle/update paths.

@isaacbraun isaacbraun added the pr ready for visual snapshots Adding this label will run visual snapshot testing. label Apr 14, 2026
@isaacbraun isaacbraun requested review from driskull and jcfranco April 14, 2026 22:49
@isaacbraun isaacbraun marked this pull request as ready for review April 14, 2026 22:49
Copy link
Copy Markdown
Member

@driskull driskull left a comment

Choose a reason for hiding this comment

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

This looks good to me @isaacbraun. Lets let @jcfranco give the thumbs up before merging if possible 👍

return component.dragEnabled && globalDragState.active;
}

async function setUpSortable(component: SortableComponent): Promise<void> {
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 wonder why this was async before. Hmm 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wondered the same 👀

Copy link
Copy Markdown
Member

@jcfranco jcfranco left a comment

Choose a reason for hiding this comment

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

Awesome stuff, @isaacbraun!

Thanks for setting up a spec test for useSortable. Let’s keep beefing it up with more coverage. ✨💪✨

Comment thread packages/components/src/controllers/useSortable.browser.spec.tsx Outdated

describe("useSortable", () => {
class Test extends LitElement {
dragEnabled = false;
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.

Suggestion: You can override this property in mount's afterConnect to avoid the additional DragEnabledTest class.

Copy link
Copy Markdown
Contributor Author

@isaacbraun isaacbraun Apr 15, 2026

Choose a reason for hiding this comment

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

Oh nice! I may be missing something, as I had to call reset() after overriding the property in afterConnect. It's probably okay but wanted to call it out in case you knew a better way. See LN43

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.

Apologies, it looks like my suggestion won't work as I expected due to the connected hook firing prior to the prop override (in retrospect, it makes sense due to the name 😅). An alternative would be to decorate the property (similar to how the controller expects the component to implement) and use a dynamic component + lit HTML templating:

Apologies, it looks like my suggestion won't work as expected since the connected hook fires before the prop override, which in retrospect makes sense given the name. 😅

An alternative would be to decorate the property, similar to how the controller expects the component to implement it, and use mount's dynamicComponents + Lit HTML templating:

import { html } from "lit";

// ...

class Test extends LitElement {
  static tagName = "sortable-test";

  @property({ type: Boolean }) dragEnabled = false;
  // ...
}

// ...

  await mount(
    html`<sortable-test drag-enabled></sortable-test>`, {
      dynamicComponents: [Test]
    }
  );

I’ll defer to you on whether this version or the original fits best.

@isaacbraun isaacbraun added pr ready for visual snapshots Adding this label will run visual snapshot testing. and removed pr ready for visual snapshots Adding this label will run visual snapshot testing. labels Apr 15, 2026
@isaacbraun isaacbraun force-pushed the isaacbraun/14259-fix-sortable-null-parent-error branch from 5758b13 to 6e56e2e Compare April 15, 2026 17:19
@isaacbraun isaacbraun added pr ready for visual snapshots Adding this label will run visual snapshot testing. and removed pr ready for visual snapshots Adding this label will run visual snapshot testing. labels Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug reports for broken functionality. Issues should include a reproduction of the bug. pr ready for visual snapshots Adding this label will run visual snapshot testing.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants