Repository: Esri/calcite-design-system
Pull request #11567: feat(block-group, list): Add event for when a move is halted due to `canPut` or `canPull` returning false
Author: login: driskull
Labels: name: bug
description: Bug reports for broken functionality. Issues should include a reproduction of the bug.
name: enhancement
description: Issues tied to a new feature or request.
name: skip visual snapshots
description: Pull requests that do not need visual regression testing.
Created: 2025-02-15T00:09:33Z | Merged: 2025-04-02T18:59:02Z

**Related Issue:** #11447

## Summary

- Add event for when a user uses the sort dropdown and `canPull` or `canPut` returns false
  - Call `canPull` and `canPut` when moving items via the "Move to" menu provided under the drag handle
  - If an item cannot be pulled or put, an event is emitted.
  - Users should handle these events to show a notification of their choice
- Adds tests


Diff:
diff --git a/packages/calcite-components/src/components/block-group/block-group.e2e.ts b/packages/calcite-components/src/components/block-group/block-group.e2e.ts
index 810c9b8cbe2..32f8a0acf97 100755
--- a/packages/calcite-components/src/components/block-group/block-group.e2e.ts
+++ b/packages/calcite-components/src/components/block-group/block-group.e2e.ts
@@ -145,6 +145,9 @@ describe("calcite-block-group", () => {
       endOldIndex: number;
       startNewIndex: number;
       startOldIndex: number;
+      moveHaltNewIndex: number;
+      moveHaltOldIndex: number;
+      moveHaltCalledTimes: number;
     }>;
 
     it("works using a mouse", async () => {
@@ -346,6 +349,87 @@ describe("calcite-block-group", () => {
       expect(await page.evaluate(() => (window as TestWindow).calledTimes)).toBe(2);
     });
 
+    it("calls canPull and canPut for move items", async () => {
+      const page = await newE2EPage();
+      await page.setContent(html`
+        <calcite-block-group id="first-letters" drag-enabled group="letters">
+          <calcite-block id="a" heading="a" label="A"></calcite-block>
+          <calcite-block id="b" heading="b" label="B"></calcite-block>
+        </calcite-block-group>
+        <calcite-block-group id="second-letters" drag-enabled group="letters">
+          <calcite-block id="c" heading="c" label="C"></calcite-block>
+          <calcite-block id="d" heading="d" label="D"></calcite-block>
+        </calcite-block-group>
+      `);
+
+      // Workaround for page.spyOnEvent() failing due to drag event payload being serialized and there being circular JSON structures from the payload elements. See: https://github.com/Esri/calcite-design-system/issues/7643
+      await page.evaluate(() => {
+        const testWindow = window as TestWindow;
+        testWindow.moveHaltCalledTimes = 0;
+        const firstLetters = document.getElementById("first-letters") as BlockGroup["el"];
+
+        firstLetters.addEventListener("calciteBlockGroupMoveHalt", (event: CustomEvent<BlockDragDetail>) => {
+          testWindow.moveHaltCalledTimes++;
+          testWindow.moveHaltNewIndex = event.detail.newIndex;
+          testWindow.moveHaltOldIndex = event.detail.oldIndex;
+        });
+
+        firstLetters.canPull = ({ dragEl }) => dragEl.id === "b";
+        firstLetters.canPut = ({ dragEl }) => dragEl.id === "c";
+      });
+      await page.waitForChanges();
+
+      async function clickMoveDropdownItem(id: string) {
+        const component = await page.find(`#${id}`);
+        component.setProperty("sortHandleOpen", true);
+        await page.waitForChanges();
+
+        const dropdownItem = await page.find(`#${id} >>> calcite-dropdown-group:last-child calcite-dropdown-item`);
+        expect(dropdownItem).not.toBeNull();
+        await dropdownItem.click();
+
+        await page.waitForChanges();
+      }
+
+      async function getResults() {
+        return await page.evaluate(() => {
+          const testWindow = window as TestWindow;
+
+          return {
+            moveHaltCalledTimes: testWindow.moveHaltCalledTimes,
+            moveHaltOldIndex: testWindow.moveHaltOldIndex,
+            moveHaltNewIndex: testWindow.moveHaltNewIndex,
+          };
+        });
+      }
+
+      await clickMoveDropdownItem("a");
+      let results = await getResults();
+
+      expect(results.moveHaltCalledTimes).toBe(1);
+      expect(results.moveHaltNewIndex).toBe(0);
+      expect(results.moveHaltOldIndex).toBe(0);
+
+      await clickMoveDropdownItem("b");
+      results = await getResults();
+
+      expect(results.moveHaltCalledTimes).toBe(1);
+      expect(results.moveHaltNewIndex).toBe(0);
+      expect(results.moveHaltNewIndex).toBe(0);
+
+      await clickMoveDropdownItem("c");
+      results = await getResults();
+
+      expect(results.moveHaltCalledTimes).toBe(1);
+
+      await clickMoveDropdownItem("d");
+      results = await getResults();
+
+      expect(results.moveHaltCalledTimes).toBe(2);
+      expect(results.moveHaltNewIndex).toBe(0);
+      expect(results.moveHaltOldIndex).toBe(1);
+    });
+
     it("reorders using a keyboard", async () => {
       const page = await createSimpleBlockGroup();
 
diff --git a/packages/calcite-components/src/components/block-group/block-group.tsx b/packages/calcite-components/src/components/block-group/block-group.tsx
index 2cb20359b63..dbd97eba106 100755
--- a/packages/calcite-components/src/components/block-group/block-group.tsx
+++ b/packages/calcite-components/src/components/block-group/block-group.tsx
@@ -133,6 +133,17 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
     focusFirstTabbable(this.el);
   }
 
+  /**
+   * Emits a `calciteBlockGroupMoveHalt` event.
+   *
+   * @private
+   * @param dragDetail
+   */
+  @method()
+  putFailed(dragDetail: BlockDragDetail): void {
+    this.calciteBlockGroupMoveHalt.emit(dragDetail);
+  }
+
   // #endregion
 
   // #region Events
@@ -146,6 +157,9 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
   /** Fires when the component's item order changes. */
   calciteBlockGroupOrderChange = createEvent<BlockDragDetail>({ cancelable: false });
 
+  /** Fires when a user attempts to move an element using the sort menu and 'canPut' or 'canPull' returns falsy. */
+  calciteBlockGroupMoveHalt = createEvent<BlockDragDetail>({ cancelable: false });
+
   // #endregion
 
   // #region Lifecycle
@@ -287,18 +301,43 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
     const toEl = moveTo.element as BlockGroup["el"];
     const fromElItems = Array.from(fromEl.children).filter(isBlock);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
+      fromEl.canPull?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      this.calciteBlockGroupMoveHalt.emit({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
+    if (
+      toEl.canPut?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
     dragEl.sortHandleOpen = false;
 
     this.disconnectObserver();
 
     toEl.prepend(dragEl);
-    const toElItems = Array.from(toEl.children).filter(isBlock);
-    const newIndex = toElItems.indexOf(dragEl);
 
     this.updateBlockItems();
     this.connectObserver();
diff --git a/packages/calcite-components/src/components/list/list.e2e.ts b/packages/calcite-components/src/components/list/list.e2e.ts
index b268abf424a..8c8bbdbbb86 100755
--- a/packages/calcite-components/src/components/list/list.e2e.ts
+++ b/packages/calcite-components/src/components/list/list.e2e.ts
@@ -1514,6 +1514,9 @@ describe("calcite-list", () => {
       endOldIndex: number;
       startNewIndex: number;
       startOldIndex: number;
+      moveHaltNewIndex: number;
+      moveHaltOldIndex: number;
+      moveHaltCalledTimes: number;
     }>;
 
     it("works using a mouse", async () => {
@@ -1754,6 +1757,89 @@ describe("calcite-list", () => {
       expect(await page.evaluate(() => (window as TestWindow).calledTimes)).toBe(2);
     });
 
+    it("calls canPull and canPut for move items", async () => {
+      const page = await newE2EPage();
+      await page.setContent(html`
+        <calcite-list id="first-letters" drag-enabled group="letters">
+          <calcite-list-item id="a" heading="a" label="A"></calcite-list-item>
+          <calcite-list-item id="b" heading="b" label="B"></calcite-list-item>
+        </calcite-list>
+        <calcite-list id="second-letters" drag-enabled group="letters">
+          <calcite-list-item id="c" heading="c" label="C"></calcite-list-item>
+          <calcite-list-item id="d" heading="d" label="D"></calcite-list-item>
+        </calcite-list>
+      `);
+
+      // Workaround for page.spyOnEvent() failing due to drag event payload being serialized and there being circular JSON structures from the payload elements. See: https://github.com/Esri/calcite-design-system/issues/7643
+      await page.evaluate(() => {
+        const testWindow = window as TestWindow;
+        testWindow.moveHaltCalledTimes = 0;
+        const firstLetters = document.getElementById("first-letters") as List["el"];
+
+        firstLetters.addEventListener("calciteListMoveHalt", (event: CustomEvent<ListDragDetail>) => {
+          testWindow.moveHaltCalledTimes++;
+          testWindow.moveHaltNewIndex = event.detail.newIndex;
+          testWindow.moveHaltOldIndex = event.detail.oldIndex;
+        });
+
+        firstLetters.canPull = ({ dragEl }) => dragEl.id === "b";
+        firstLetters.canPut = ({ dragEl }) => dragEl.id === "c";
+      });
+      await page.waitForChanges();
+
+      async function clickMoveDropdownItem(id: string) {
+        const component = await page.find(`#${id}`);
+        component.setProperty("sortHandleOpen", true);
+        await page.waitForChanges();
+
+        const dropdownItem = await page.find(`#${id} >>> calcite-dropdown-group:last-child calcite-dropdown-item`);
+        expect(dropdownItem).not.toBeNull();
+        await dropdownItem.click();
+
+        await page.waitForChanges();
+      }
+
+      async function getResults() {
+        return await page.evaluate(() => {
+          const testWindow = window as TestWindow;
+
+          return {
+            moveHaltCalledTimes: testWindow.moveHaltCalledTimes,
+            moveHaltOldIndex: testWindow.moveHaltOldIndex,
+            moveHaltNewIndex: testWindow.moveHaltNewIndex,
+          };
+        });
+      }
+
+      await clickMoveDropdownItem("a");
+      let results = await getResults();
+
+      expect(results.moveHaltCalledTimes).toBe(1);
+      expect(results.moveHaltNewIndex).toBe(0);
+      expect(results.moveHaltOldIndex).toBe(0);
+
+      await clickMoveDropdownItem("b");
+      results = await getResults();
+
+      expect(results.moveHaltCalledTimes).toBe(1);
+      expect(results.moveHaltNewIndex).toBe(0);
+      expect(results.moveHaltOldIndex).toBe(0);
+
+      await clickMoveDropdownItem("c");
+      results = await getResults();
+
+      expect(results.moveHaltCalledTimes).toBe(1);
+      expect(results.moveHaltNewIndex).toBe(0);
+      expect(results.moveHaltOldIndex).toBe(0);
+
+      await clickMoveDropdownItem("d");
+      results = await getResults();
+
+      expect(results.moveHaltCalledTimes).toBe(2);
+      expect(results.moveHaltNewIndex).toBe(0);
+      expect(results.moveHaltOldIndex).toBe(1);
+    });
+
     it("reorders using a keyboard", async () => {
       const page = await createSimpleList();
 
diff --git a/packages/calcite-components/src/components/list/list.tsx b/packages/calcite-components/src/components/list/list.tsx
index 42457e1ed82..52deb41325f 100755
--- a/packages/calcite-components/src/components/list/list.tsx
+++ b/packages/calcite-components/src/components/list/list.tsx
@@ -330,6 +330,17 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
 
   // #region Public Methods
 
+  /**
+   * Emits a `calciteListMoveHalt` event.
+   *
+   * @private
+   * @param dragDetail
+   */
+  @method()
+  putFailed(dragDetail: ListDragDetail): void {
+    this.calciteListMoveHalt.emit(dragDetail);
+  }
+
   /**
    * Sets focus on the component's first focusable element.
    *
@@ -372,6 +383,9 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
   /** Fires when the component's item order changes. */
   calciteListOrderChange = createEvent<ListDragDetail>({ cancelable: false });
 
+  /** Fires when a user attempts to move an element using the sort menu and 'canPut' or 'canPull' returns falsy. */
+  calciteListMoveHalt = createEvent<ListDragDetail>({ cancelable: false });
+
   // #endregion
 
   // #region Lifecycle
@@ -937,20 +951,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
+      fromEl.canPull?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      this.calciteListMoveHalt.emit({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
+    if (
+      toEl.canPut?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
     dragEl.sortHandleOpen = false;
 
     this.disconnectObserver();
 
     toEl.prepend(dragEl);
     expandedAncestors(dragEl);
-    const toElItems = Array.from(toEl.children).filter(isListItem);
-    const newIndex = toElItems.indexOf(dragEl);
-
     this.updateListItems();
     this.connectObserver();
 


Reviews:
user: login: jcfranco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T22:57:02Z
user: login: driskull
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:16:49Z
user: login: driskull
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:17:42Z
user: login: driskull
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:18:00Z
user: login: jcfranco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:19:28Z
user: login: jcfranco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:21:18Z
user: login: jcfranco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:26:54Z
user: login: jcfranco
body: ✨↕️🤏✨
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-01T23:27:49Z
user: login: driskull
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:28:31Z
user: login: driskull
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:30:13Z
user: login: jcfranco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:32:44Z
user: login: driskull
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:35:13Z
user: login: jcfranco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T23:36:33Z
user: login: jcfranco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T15:57:55Z
user: login: driskull
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T18:57:38Z

Review comments:
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
path: packages/calcite-components/src/components/list/list.tsx
user: login: jcfranco
body: Since this is shared, let’s create a utility for both `list` and `block-list` to use, and do the same for their tests.
created_at: 2025-04-01T22:42:41Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
path: packages/calcite-components/src/components/list/list.tsx
user: login: jcfranco
body: If a put/pull fails, does the index get modified to 0? 
created_at: 2025-04-01T22:52:34Z
author_association: MEMBER
diff_hunk: @@ -287,18 +304,43 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
     const toEl = moveTo.element as BlockGroup["el"];
     const fromElItems = Array.from(fromEl.children).filter(isBlock);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
+      fromEl.canPull?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      this.calciteBlockGroupPullFail.emit({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
+    if (
+      toEl.canPut?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex });
path: packages/calcite-components/src/components/block-group/block-group.tsx
user: login: jcfranco
body: It looks like `putFailed` can be inlined since it seems to only be used here. Applies to `list`.
created_at: 2025-04-01T22:53:57Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
+      fromEl.canPull?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      this.calciteListPullFail.emit({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
+    if (
+      toEl.canPut?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
     dragEl.sortHandleOpen = false;
 
     this.disconnectObserver();
 
     toEl.prepend(dragEl);
     expandedAncestors(dragEl);
-    const toElItems = Array.from(toEl.children).filter(isListItem);
-    const newIndex = toElItems.indexOf(dragEl);
path: packages/calcite-components/src/components/list/list.tsx
user: login: jcfranco
body: Where is `newIndex` updated now? It looks like it's always 0 in the latest changes, where previously, it depended on the placement.
created_at: 2025-04-01T22:56:07Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
+      fromEl.canPull?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      this.calciteListPullFail.emit({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
+    if (
+      toEl.canPut?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
     dragEl.sortHandleOpen = false;
 
     this.disconnectObserver();
 
     toEl.prepend(dragEl);
     expandedAncestors(dragEl);
-    const toElItems = Array.from(toEl.children).filter(isListItem);
-    const newIndex = toElItems.indexOf(dragEl);
path: packages/calcite-components/src/components/list/list.tsx
user: login: driskull
body: It was always 0, we always put at the top of the list when moving lists via keyboard which is why this code wasn't necessary.
created_at: 2025-04-01T23:16:49Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
path: packages/calcite-components/src/components/list/list.tsx
user: login: driskull
body: When moving via keyboard, we always move to index 0. So that is what the fail event would show.
created_at: 2025-04-01T23:17:42Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
path: packages/calcite-components/src/components/list/list.tsx
user: login: driskull
body: Can we do this as part of a new refactor issue?
created_at: 2025-04-01T23:18:00Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
path: packages/calcite-components/src/components/list/list.tsx
user: login: jcfranco
body: Ah, gotcha. So this is keyboard-specific moving logic.
created_at: 2025-04-01T23:19:28Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
path: packages/calcite-components/src/components/list/list.tsx
user: login: jcfranco
body: Sounds good, but let’s make it a priority to unify these paths as soon as possible.
created_at: 2025-04-01T23:21:18Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
path: packages/calcite-components/src/components/list/list.tsx
user: login: jcfranco
body: > So this is keyboard-specific moving logic.

Scratch that. This is move-event-specific logic.
created_at: 2025-04-01T23:26:54Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
path: packages/calcite-components/src/components/list/list.tsx
user: login: driskull
body: Both, its logic specific to moving via keyboard. When moving via keyboard we always place at the top.
created_at: 2025-04-01T23:28:31Z
author_association: MEMBER
diff_hunk: @@ -287,18 +304,43 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
     const toEl = moveTo.element as BlockGroup["el"];
     const fromElItems = Array.from(fromEl.children).filter(isBlock);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
+      fromEl.canPull?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      this.calciteBlockGroupPullFail.emit({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
+    if (
+      toEl.canPut?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex });
path: packages/calcite-components/src/components/block-group/block-group.tsx
user: login: driskull
body: @jcfranco what do you mean by inlined? Can you elaborate pretty plz :)
created_at: 2025-04-01T23:30:13Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
path: packages/calcite-components/src/components/list/list.tsx
user: login: jcfranco
body: Is a move is handled the same way whether it's keyboard or mouse-initiated? I guess I'm trying to confirm whether `newIndex` is updated differently when it's not moved via keyboard.
created_at: 2025-04-01T23:32:44Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
path: packages/calcite-components/src/components/list/list.tsx
user: login: driskull
body: > Is a move is handled the same way whether it's keyboard or mouse-initiated?

Not exactly, when using a mouse the sortable library lets you move to any index within another list whereas with the keyboard moving via the handle component, we only allow moving to the top of another list. Then a user would need to sort within that list to sort within the list. 

This logic is only for moving via the sort handle which places the element at the top of the list being moved to.
created_at: 2025-04-01T23:35:13Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
path: packages/calcite-components/src/components/list/list.tsx
user: login: jcfranco
body: Sweet. Thanks for clarifying.
created_at: 2025-04-01T23:36:33Z
author_association: MEMBER
diff_hunk: @@ -287,18 +304,43 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
     const toEl = moveTo.element as BlockGroup["el"];
     const fromElItems = Array.from(fromEl.children).filter(isBlock);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
+      fromEl.canPull?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      this.calciteBlockGroupPullFail.emit({ toEl, fromEl, dragEl, oldIndex, newIndex });
+      return;
+    }
+
+    if (
+      toEl.canPut?.({
+        toEl,
+        fromEl,
+        dragEl,
+        newIndex,
+        oldIndex,
+      }) === false
+    ) {
+      toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex });
path: packages/calcite-components/src/components/block-group/block-group.tsx
user: login: jcfranco
body: You can ignore this one. I thought `putFailed` wasn't accessed externally.
created_at: 2025-04-02T15:57:55Z
author_association: MEMBER
diff_hunk: @@ -937,20 +954,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
     const toEl = moveTo.element as List["el"];
     const fromElItems = Array.from(fromEl.children).filter(isListItem);
     const oldIndex = fromElItems.indexOf(dragEl);
+    const newIndex = 0;
 
     if (!fromEl) {
       return;
     }
 
+    if (
path: packages/calcite-components/src/components/list/list.tsx
user: login: driskull
body: created https://github.com/Esri/calcite-design-system/issues/11849
created_at: 2025-04-02T18:57:38Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-02-28T01:59:39Z
body: This PR has been automatically marked as stale because it has not had recent activity. Please close your PR if it is no longer relevant. Thank you for your contributions.
author_association: CONTRIBUTOR
user: login: driskull
created_at: 2025-03-28T23:49:04Z
body: @jcfranco @macandcheese this is still awaiting review.
author_association: MEMBER
user: login: jcfranco
created_at: 2025-04-01T23:44:00Z
body: PR-title/changelog-wise, would developers know when `canPull`/`canPut` returns false? Maybe rephrasing to emphasize  what these events will enable would be helpful.
author_association: MEMBER
user: login: driskull
created_at: 2025-04-01T23:51:47Z
body: @jcfranco how about `feat(block-group, list): Add events for when a move is prevented with canPut or canPull #11567`

Also, are the event names acceptable? I wasn't sure if `Fail` was the right term or if we wanted something different. Other potential terms: rejected, prevented, stopped, disabled

Spawns from this option: https://github.com/SortableJS/Sortable?tab=readme-ov-file#group-option
author_association: MEMBER
user: login: jcfranco
created_at: 2025-04-02T16:04:21Z
body: > how about feat(block-group, list): Add events for when a move is prevented with canPut or canPull #11567

🚢 it!

> Also, are the event names acceptable?

Good question. Fail makes it sound like `canPut` or `canPull` threw an error.

This got me thinking, if a developer is handling failed moves, will they typically need to listen for both events? If so, maybe they can share the same event regardless of `canPut` or `canPull` (with this info as part of the event payload, if needed). Something like `calcite<Component>MoveHalt`?
author_association: MEMBER
user: login: driskull
created_at: 2025-04-02T16:06:42Z
body: > Something like calcite<Component>MoveHalt?

Yeah we could do that. Should I proceed?
author_association: MEMBER
user: login: jcfranco
created_at: 2025-04-02T16:19:19Z
body: SGTM!
author_association: MEMBER