Skip to content

Commit 98bf85d

Browse files
authored
feat(block-group, list): Add event for when a move is halted due to canPut or canPull returning false (#11567)
**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
1 parent 2bec728 commit 98bf85d

File tree

4 files changed

+252
-5
lines changed

4 files changed

+252
-5
lines changed

packages/calcite-components/src/components/block-group/block-group.e2e.ts

+84
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ describe("calcite-block-group", () => {
145145
endOldIndex: number;
146146
startNewIndex: number;
147147
startOldIndex: number;
148+
moveHaltNewIndex: number;
149+
moveHaltOldIndex: number;
150+
moveHaltCalledTimes: number;
148151
}>;
149152

150153
it("works using a mouse", async () => {
@@ -346,6 +349,87 @@ describe("calcite-block-group", () => {
346349
expect(await page.evaluate(() => (window as TestWindow).calledTimes)).toBe(2);
347350
});
348351

352+
it("calls canPull and canPut for move items", async () => {
353+
const page = await newE2EPage();
354+
await page.setContent(html`
355+
<calcite-block-group id="first-letters" drag-enabled group="letters">
356+
<calcite-block id="a" heading="a" label="A"></calcite-block>
357+
<calcite-block id="b" heading="b" label="B"></calcite-block>
358+
</calcite-block-group>
359+
<calcite-block-group id="second-letters" drag-enabled group="letters">
360+
<calcite-block id="c" heading="c" label="C"></calcite-block>
361+
<calcite-block id="d" heading="d" label="D"></calcite-block>
362+
</calcite-block-group>
363+
`);
364+
365+
// 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
366+
await page.evaluate(() => {
367+
const testWindow = window as TestWindow;
368+
testWindow.moveHaltCalledTimes = 0;
369+
const firstLetters = document.getElementById("first-letters") as BlockGroup["el"];
370+
371+
firstLetters.addEventListener("calciteBlockGroupMoveHalt", (event: CustomEvent<BlockDragDetail>) => {
372+
testWindow.moveHaltCalledTimes++;
373+
testWindow.moveHaltNewIndex = event.detail.newIndex;
374+
testWindow.moveHaltOldIndex = event.detail.oldIndex;
375+
});
376+
377+
firstLetters.canPull = ({ dragEl }) => dragEl.id === "b";
378+
firstLetters.canPut = ({ dragEl }) => dragEl.id === "c";
379+
});
380+
await page.waitForChanges();
381+
382+
async function clickMoveDropdownItem(id: string) {
383+
const component = await page.find(`#${id}`);
384+
component.setProperty("sortHandleOpen", true);
385+
await page.waitForChanges();
386+
387+
const dropdownItem = await page.find(`#${id} >>> calcite-dropdown-group:last-child calcite-dropdown-item`);
388+
expect(dropdownItem).not.toBeNull();
389+
await dropdownItem.click();
390+
391+
await page.waitForChanges();
392+
}
393+
394+
async function getResults() {
395+
return await page.evaluate(() => {
396+
const testWindow = window as TestWindow;
397+
398+
return {
399+
moveHaltCalledTimes: testWindow.moveHaltCalledTimes,
400+
moveHaltOldIndex: testWindow.moveHaltOldIndex,
401+
moveHaltNewIndex: testWindow.moveHaltNewIndex,
402+
};
403+
});
404+
}
405+
406+
await clickMoveDropdownItem("a");
407+
let results = await getResults();
408+
409+
expect(results.moveHaltCalledTimes).toBe(1);
410+
expect(results.moveHaltNewIndex).toBe(0);
411+
expect(results.moveHaltOldIndex).toBe(0);
412+
413+
await clickMoveDropdownItem("b");
414+
results = await getResults();
415+
416+
expect(results.moveHaltCalledTimes).toBe(1);
417+
expect(results.moveHaltNewIndex).toBe(0);
418+
expect(results.moveHaltNewIndex).toBe(0);
419+
420+
await clickMoveDropdownItem("c");
421+
results = await getResults();
422+
423+
expect(results.moveHaltCalledTimes).toBe(1);
424+
425+
await clickMoveDropdownItem("d");
426+
results = await getResults();
427+
428+
expect(results.moveHaltCalledTimes).toBe(2);
429+
expect(results.moveHaltNewIndex).toBe(0);
430+
expect(results.moveHaltOldIndex).toBe(1);
431+
});
432+
349433
it("reorders using a keyboard", async () => {
350434
const page = await createSimpleBlockGroup();
351435

packages/calcite-components/src/components/block-group/block-group.tsx

+41-2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,17 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
133133
focusFirstTabbable(this.el);
134134
}
135135

136+
/**
137+
* Emits a `calciteBlockGroupMoveHalt` event.
138+
*
139+
* @private
140+
* @param dragDetail
141+
*/
142+
@method()
143+
putFailed(dragDetail: BlockDragDetail): void {
144+
this.calciteBlockGroupMoveHalt.emit(dragDetail);
145+
}
146+
136147
// #endregion
137148

138149
// #region Events
@@ -146,6 +157,9 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
146157
/** Fires when the component's item order changes. */
147158
calciteBlockGroupOrderChange = createEvent<BlockDragDetail>({ cancelable: false });
148159

160+
/** Fires when a user attempts to move an element using the sort menu and 'canPut' or 'canPull' returns falsy. */
161+
calciteBlockGroupMoveHalt = createEvent<BlockDragDetail>({ cancelable: false });
162+
149163
// #endregion
150164

151165
// #region Lifecycle
@@ -287,18 +301,43 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
287301
const toEl = moveTo.element as BlockGroup["el"];
288302
const fromElItems = Array.from(fromEl.children).filter(isBlock);
289303
const oldIndex = fromElItems.indexOf(dragEl);
304+
const newIndex = 0;
290305

291306
if (!fromEl) {
292307
return;
293308
}
294309

310+
if (
311+
fromEl.canPull?.({
312+
toEl,
313+
fromEl,
314+
dragEl,
315+
newIndex,
316+
oldIndex,
317+
}) === false
318+
) {
319+
this.calciteBlockGroupMoveHalt.emit({ toEl, fromEl, dragEl, oldIndex, newIndex });
320+
return;
321+
}
322+
323+
if (
324+
toEl.canPut?.({
325+
toEl,
326+
fromEl,
327+
dragEl,
328+
newIndex,
329+
oldIndex,
330+
}) === false
331+
) {
332+
toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex });
333+
return;
334+
}
335+
295336
dragEl.sortHandleOpen = false;
296337

297338
this.disconnectObserver();
298339

299340
toEl.prepend(dragEl);
300-
const toElItems = Array.from(toEl.children).filter(isBlock);
301-
const newIndex = toElItems.indexOf(dragEl);
302341

303342
this.updateBlockItems();
304343
this.connectObserver();

packages/calcite-components/src/components/list/list.e2e.ts

+86
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,9 @@ describe("calcite-list", () => {
15141514
endOldIndex: number;
15151515
startNewIndex: number;
15161516
startOldIndex: number;
1517+
moveHaltNewIndex: number;
1518+
moveHaltOldIndex: number;
1519+
moveHaltCalledTimes: number;
15171520
}>;
15181521

15191522
it("works using a mouse", async () => {
@@ -1754,6 +1757,89 @@ describe("calcite-list", () => {
17541757
expect(await page.evaluate(() => (window as TestWindow).calledTimes)).toBe(2);
17551758
});
17561759

1760+
it("calls canPull and canPut for move items", async () => {
1761+
const page = await newE2EPage();
1762+
await page.setContent(html`
1763+
<calcite-list id="first-letters" drag-enabled group="letters">
1764+
<calcite-list-item id="a" heading="a" label="A"></calcite-list-item>
1765+
<calcite-list-item id="b" heading="b" label="B"></calcite-list-item>
1766+
</calcite-list>
1767+
<calcite-list id="second-letters" drag-enabled group="letters">
1768+
<calcite-list-item id="c" heading="c" label="C"></calcite-list-item>
1769+
<calcite-list-item id="d" heading="d" label="D"></calcite-list-item>
1770+
</calcite-list>
1771+
`);
1772+
1773+
// 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
1774+
await page.evaluate(() => {
1775+
const testWindow = window as TestWindow;
1776+
testWindow.moveHaltCalledTimes = 0;
1777+
const firstLetters = document.getElementById("first-letters") as List["el"];
1778+
1779+
firstLetters.addEventListener("calciteListMoveHalt", (event: CustomEvent<ListDragDetail>) => {
1780+
testWindow.moveHaltCalledTimes++;
1781+
testWindow.moveHaltNewIndex = event.detail.newIndex;
1782+
testWindow.moveHaltOldIndex = event.detail.oldIndex;
1783+
});
1784+
1785+
firstLetters.canPull = ({ dragEl }) => dragEl.id === "b";
1786+
firstLetters.canPut = ({ dragEl }) => dragEl.id === "c";
1787+
});
1788+
await page.waitForChanges();
1789+
1790+
async function clickMoveDropdownItem(id: string) {
1791+
const component = await page.find(`#${id}`);
1792+
component.setProperty("sortHandleOpen", true);
1793+
await page.waitForChanges();
1794+
1795+
const dropdownItem = await page.find(`#${id} >>> calcite-dropdown-group:last-child calcite-dropdown-item`);
1796+
expect(dropdownItem).not.toBeNull();
1797+
await dropdownItem.click();
1798+
1799+
await page.waitForChanges();
1800+
}
1801+
1802+
async function getResults() {
1803+
return await page.evaluate(() => {
1804+
const testWindow = window as TestWindow;
1805+
1806+
return {
1807+
moveHaltCalledTimes: testWindow.moveHaltCalledTimes,
1808+
moveHaltOldIndex: testWindow.moveHaltOldIndex,
1809+
moveHaltNewIndex: testWindow.moveHaltNewIndex,
1810+
};
1811+
});
1812+
}
1813+
1814+
await clickMoveDropdownItem("a");
1815+
let results = await getResults();
1816+
1817+
expect(results.moveHaltCalledTimes).toBe(1);
1818+
expect(results.moveHaltNewIndex).toBe(0);
1819+
expect(results.moveHaltOldIndex).toBe(0);
1820+
1821+
await clickMoveDropdownItem("b");
1822+
results = await getResults();
1823+
1824+
expect(results.moveHaltCalledTimes).toBe(1);
1825+
expect(results.moveHaltNewIndex).toBe(0);
1826+
expect(results.moveHaltOldIndex).toBe(0);
1827+
1828+
await clickMoveDropdownItem("c");
1829+
results = await getResults();
1830+
1831+
expect(results.moveHaltCalledTimes).toBe(1);
1832+
expect(results.moveHaltNewIndex).toBe(0);
1833+
expect(results.moveHaltOldIndex).toBe(0);
1834+
1835+
await clickMoveDropdownItem("d");
1836+
results = await getResults();
1837+
1838+
expect(results.moveHaltCalledTimes).toBe(2);
1839+
expect(results.moveHaltNewIndex).toBe(0);
1840+
expect(results.moveHaltOldIndex).toBe(1);
1841+
});
1842+
17571843
it("reorders using a keyboard", async () => {
17581844
const page = await createSimpleList();
17591845

packages/calcite-components/src/components/list/list.tsx

+41-3
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,17 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
330330

331331
// #region Public Methods
332332

333+
/**
334+
* Emits a `calciteListMoveHalt` event.
335+
*
336+
* @private
337+
* @param dragDetail
338+
*/
339+
@method()
340+
putFailed(dragDetail: ListDragDetail): void {
341+
this.calciteListMoveHalt.emit(dragDetail);
342+
}
343+
333344
/**
334345
* Sets focus on the component's first focusable element.
335346
*
@@ -372,6 +383,9 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
372383
/** Fires when the component's item order changes. */
373384
calciteListOrderChange = createEvent<ListDragDetail>({ cancelable: false });
374385

386+
/** Fires when a user attempts to move an element using the sort menu and 'canPut' or 'canPull' returns falsy. */
387+
calciteListMoveHalt = createEvent<ListDragDetail>({ cancelable: false });
388+
375389
// #endregion
376390

377391
// #region Lifecycle
@@ -937,20 +951,44 @@ export class List extends LitElement implements InteractiveComponent, SortableCo
937951
const toEl = moveTo.element as List["el"];
938952
const fromElItems = Array.from(fromEl.children).filter(isListItem);
939953
const oldIndex = fromElItems.indexOf(dragEl);
954+
const newIndex = 0;
940955

941956
if (!fromEl) {
942957
return;
943958
}
944959

960+
if (
961+
fromEl.canPull?.({
962+
toEl,
963+
fromEl,
964+
dragEl,
965+
newIndex,
966+
oldIndex,
967+
}) === false
968+
) {
969+
this.calciteListMoveHalt.emit({ toEl, fromEl, dragEl, oldIndex, newIndex });
970+
return;
971+
}
972+
973+
if (
974+
toEl.canPut?.({
975+
toEl,
976+
fromEl,
977+
dragEl,
978+
newIndex,
979+
oldIndex,
980+
}) === false
981+
) {
982+
toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex });
983+
return;
984+
}
985+
945986
dragEl.sortHandleOpen = false;
946987

947988
this.disconnectObserver();
948989

949990
toEl.prepend(dragEl);
950991
expandedAncestors(dragEl);
951-
const toElItems = Array.from(toEl.children).filter(isListItem);
952-
const newIndex = toElItems.indexOf(dragEl);
953-
954992
this.updateListItems();
955993
this.connectObserver();
956994

0 commit comments

Comments
 (0)