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 8044ce8a568..51a06e6c904 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
@@ -310,10 +310,6 @@ describe("calcite-block-group", () => {
expect(moveToItemIds.length).toBe(6);
- const uniqueMoveToItemIds = new Set(moveToItemIds);
-
- expect(uniqueMoveToItemIds.size).toBe(2);
-
const moveToItemElementIds = await page.evaluate((letterBlockSelector) => {
return Array.from(document.querySelectorAll(letterBlockSelector))
.map((item: Block["el"]) => item.moveToItems.map((moveToItem) => moveToItem.element.id))
@@ -425,7 +421,7 @@ describe("calcite-block-group", () => {
await page.waitForChanges();
await page.waitForTimeout(DEBOUNCE.nextTick);
- return await findAll(page, `#${id} >>> calcite-dropdown-group#${IDS.move} calcite-dropdown-item`, {
+ return findAll(page, `#${id} >>> calcite-dropdown-group#${IDS.move} calcite-dropdown-item`, {
allowEmpty: true,
});
}
@@ -445,6 +441,47 @@ describe("calcite-block-group", () => {
expect(dMoveItems.length).toBe(0);
});
+ it("supports cloning with canPull", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html`
+
+
+
+
+
+
+
+
+ `);
+
+ // 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 firstLetters = document.getElementById("first-letters") as BlockGroup["el"];
+ firstLetters.canPull = () => "clone";
+ });
+ await page.waitForChanges();
+
+ async function getAddToItems(id: string) {
+ return findAll(page, `#${id} >>> calcite-dropdown-group#${IDS.add} calcite-dropdown-item`, {
+ allowEmpty: true,
+ });
+ }
+
+ const aAddToItems = await getAddToItems("a");
+ expect(aAddToItems.length).toBe(1);
+ expect(await aAddToItems[0].getProperty("label")).toBe("Second Letters");
+
+ const bAddToItems = await getAddToItems("b");
+ expect(bAddToItems.length).toBe(1);
+ expect(await bAddToItems[0].getProperty("label")).toBe("Second Letters");
+
+ const cAddToItems = await getAddToItems("c");
+ expect(cAddToItems.length).toBe(0);
+
+ const dAddToItems = await getAddToItems("d");
+ expect(dAddToItems.length).toBe(0);
+ });
+
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 379d57bf41a..a3d247b0730 100755
--- a/packages/calcite-components/src/components/block-group/block-group.tsx
+++ b/packages/calcite-components/src/components/block-group/block-group.tsx
@@ -14,7 +14,12 @@ import {
disconnectSortableComponent,
SortableComponent,
} from "../../utils/sortableComponent";
-import { MoveEventDetail, MoveTo, ReorderEventDetail } from "../sort-handle/interfaces";
+import {
+ MoveEventDetail,
+ SortMenuItem,
+ ReorderEventDetail,
+ AddEventDetail,
+} from "../sort-handle/interfaces";
import { DEBOUNCE } from "../../utils/resources";
import { Block } from "../block/block";
import { getRootNode, slotChangeGetAssignedElements } from "../../utils/dom";
@@ -72,14 +77,14 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
@state() assistiveText: string;
- @state() moveToItems: MoveTo[] = [];
+ @state() sortHandleMenuItems: SortMenuItem[] = [];
// #endregion
// #region Public Properties
/** When provided, the method will be called to determine whether the element can move from the component. */
- @property() canPull: (detail: BlockDragDetail) => boolean;
+ @property() canPull: (detail: BlockDragDetail) => boolean | "clone";
/** When provided, the method will be called to determine whether the element can be added from another component. */
@property() canPut: (detail: BlockDragDetail) => boolean;
@@ -167,6 +172,7 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
);
this.listen("calciteSortHandleReorder", this.handleSortReorder);
this.listen("calciteSortHandleMove", this.handleSortMove);
+ this.listen("calciteSortHandleAdd", this.handleSortAdd);
}
override connectedCallback(): void {
@@ -207,7 +213,7 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
private updateBlockItems(): void {
this.updateGroupItems();
- const { dragEnabled, el, moveToItems, sortDisabled } = this;
+ const { dragEnabled, el, sortDisabled, sortHandleMenuItems } = this;
const items = Array.from(this.el.querySelectorAll(blockSelector));
const fromEl = el;
@@ -215,17 +221,26 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
items.forEach((item) => {
if (item.closest(blockGroupSelector) === el) {
- item.moveToItems = moveToItems.filter(
- (moveToItem) =>
- moveToItem.element !== el &&
- !item.contains(moveToItem.element) &&
- this.validateMove({
- fromEl,
- toEl: moveToItem.element as BlockGroup["el"],
- dragEl: item,
- newIndex: 0,
- oldIndex: fromElItems.indexOf(item),
- }),
+ item.moveToItems = sortHandleMenuItems.filter((moveToItem) =>
+ this.validateSortMenuItem({
+ type: "move",
+ fromEl,
+ toEl: moveToItem.element as BlockGroup["el"],
+ dragEl: item,
+ newIndex: 0,
+ oldIndex: fromElItems.indexOf(item),
+ }),
+ );
+
+ item.addToItems = this.sortHandleMenuItems.filter((moveToItem) =>
+ this.validateSortMenuItem({
+ type: "add",
+ fromEl,
+ toEl: moveToItem.element as BlockGroup["el"],
+ dragEl: item,
+ newIndex: 0,
+ oldIndex: fromElItems.indexOf(item),
+ }),
);
item.dragHandle = dragEnabled;
item.sortDisabled = sortDisabled;
@@ -246,7 +261,7 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
).filter((blockGroup) => !blockGroup.disabled && blockGroup.dragEnabled)
: [];
- this.moveToItems = blockGroups.map((element) => ({
+ this.sortHandleMenuItems = blockGroups.map((element) => ({
element,
label: element.label ?? element.id,
id: guid(),
@@ -267,6 +282,15 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
this.handleReorder(event);
}
+ private handleSortAdd(event: CustomEvent): void {
+ if (this.parentBlockGroupEl || event.defaultPrevented) {
+ return;
+ }
+
+ event.preventDefault();
+ this.handleAdd(event);
+ }
+
private handleSortMove(event: CustomEvent): void {
if (this.parentBlockGroupEl || event.defaultPrevented) {
return;
@@ -344,48 +368,68 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
});
}
- private validateMove({
+ private validateSortMenuItem({
fromEl,
toEl,
dragEl,
newIndex,
oldIndex,
+ type,
}: {
fromEl?: BlockGroup["el"];
toEl?: BlockGroup["el"];
dragEl: Block["el"];
newIndex: number;
oldIndex: number;
+ type: "move" | "add";
}): boolean {
- if (!fromEl || !toEl) {
+ if (!fromEl || !toEl || toEl === fromEl || dragEl.contains(toEl)) {
return false;
}
- if (
+ const canPull =
fromEl.canPull?.({
toEl,
fromEl,
dragEl,
newIndex,
oldIndex,
- }) === false
- ) {
- return false;
- }
+ }) ?? true;
- if (
+ const canPut =
toEl.canPut?.({
toEl,
fromEl,
dragEl,
newIndex,
oldIndex,
- }) === false
- ) {
- return false;
+ }) ?? true;
+
+ return type === "add" ? canPull === "clone" : canPull === true && canPut;
+ }
+
+ private handleAdd(event: CustomEvent): void {
+ const { addTo } = event.detail;
+
+ const dragEl = event.target as Block["el"];
+ const fromEl = dragEl?.parentElement as BlockGroup["el"];
+ const toEl = addTo.element as BlockGroup["el"];
+ const fromElItems = Array.from(fromEl.children).filter(isBlock);
+ const oldIndex = fromElItems.indexOf(dragEl);
+ const newIndex = 0;
+
+ if (!this.validateSortMenuItem({ type: "move", fromEl, toEl, dragEl, newIndex, oldIndex })) {
+ return;
}
- return true;
+ dragEl.sortHandleOpen = false;
+
+ this.disconnectObserver();
+
+ const newEl = dragEl.cloneNode();
+ toEl.prepend(newEl);
+ this.updateBlockItemsDebounced();
+ this.connectObserver();
}
private handleMove(event: CustomEvent): void {
@@ -398,7 +442,7 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
const oldIndex = fromElItems.indexOf(dragEl);
const newIndex = 0;
- if (!this.validateMove({ fromEl, toEl, dragEl, newIndex, oldIndex })) {
+ if (!this.validateSortMenuItem({ type: "move", fromEl, toEl, dragEl, newIndex, oldIndex })) {
return;
}
diff --git a/packages/calcite-components/src/components/block/block.tsx b/packages/calcite-components/src/components/block/block.tsx
index d8c4b59af5b..15892f9e27b 100644
--- a/packages/calcite-components/src/components/block/block.tsx
+++ b/packages/calcite-components/src/components/block/block.tsx
@@ -20,10 +20,10 @@ import {
import { IconNameOrString } from "../icon/interfaces";
import { useT9n } from "../../controllers/useT9n";
import { logger } from "../../utils/logger";
-import { MoveTo } from "../sort-handle/interfaces";
import { SortHandle } from "../sort-handle/sort-handle";
import { useSetFocus } from "../../controllers/useSetFocus";
import { styles as sortableStyles } from "../../assets/styles/_sortable.scss";
+import { SortMenuItem } from "../sort-handle/interfaces";
import { BlockSection } from "../block-section/block-section";
import { CSS, ICONS, IDS, SLOTS } from "./resources";
import T9nStrings from "./assets/t9n/messages.en.json";
@@ -145,11 +145,18 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose
@property() messageOverrides?: typeof this.messages._overrides;
/**
- * Sets the item to display a border.
+ * Defines the "Add to" items.
*
* @private
*/
- @property() moveToItems: MoveTo[] = [];
+ @property() addToItems: SortMenuItem[] = [];
+
+ /**
+ * Defines the "Move to" items.
+ *
+ * @private
+ */
+ @property() moveToItems: SortMenuItem[] = [];
/**
* Prevents reordering the component.
@@ -234,6 +241,12 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose
//#region Events
+ /**
+ *
+ * @private
+ */
+ calciteInternalBlockUpdateSortMenuItems = createEvent({ cancelable: false });
+
/** Fires when the component is requested to be closed and before the closing transition begins. */
calciteBlockBeforeClose = createEvent({ cancelable: false });
@@ -512,6 +525,7 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose
menuFlipPlacements,
menuPlacement,
moveToItems,
+ addToItems,
setPosition,
setSize,
dragDisabled,
@@ -538,6 +552,7 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose
`;
-export const withItems = (): string => html`
+export const withMoveToItems = (): string => html`
@@ -60,6 +60,19 @@ export const withItems = (): string => html`
`;
+export const withAddToItems = (): string => html`
+
+
+
+
+`;
+
export const disabled = (): string => html`
`;
diff --git a/packages/calcite-components/src/components/sort-handle/sort-handle.tsx b/packages/calcite-components/src/components/sort-handle/sort-handle.tsx
index d871d38e907..d2b75549610 100644
--- a/packages/calcite-components/src/components/sort-handle/sort-handle.tsx
+++ b/packages/calcite-components/src/components/sort-handle/sort-handle.tsx
@@ -18,7 +18,13 @@ import type { Dropdown } from "../dropdown/dropdown";
import { useSetFocus } from "../../controllers/useSetFocus";
import T9nStrings from "./assets/t9n/messages.en.json";
import { CSS, ICONS, IDS, REORDER_VALUES, SLOTS, SUBSTITUTIONS } from "./resources";
-import { MoveEventDetail, MoveTo, Reorder, ReorderEventDetail } from "./interfaces";
+import {
+ MoveEventDetail,
+ SortMenuItem,
+ Reorder,
+ ReorderEventDetail,
+ AddEventDetail,
+} from "./interfaces";
import { styles } from "./sort-handle.scss";
declare global {
@@ -57,7 +63,7 @@ export class SortHandle extends LitElement implements InteractiveComponent {
}
@state() get hasNoItems(): boolean {
- return !this.hasReorderItems && this.moveToItems.length < 1;
+ return !this.hasReorderItems && this.moveToItems.length < 1 && this.addToItems.length < 1;
}
// #endregion
@@ -83,8 +89,11 @@ export class SortHandle extends LitElement implements InteractiveComponent {
*/
@property() messages = useT9n({ blocking: true });
+ /** Defines the "Add to" items. */
+ @property() addToItems: SortMenuItem[] = [];
+
/** Defines the "Move to" items. */
- @property() moveToItems: MoveTo[] = [];
+ @property() moveToItems: SortMenuItem[] = [];
/** When `true`, displays and positions the component. */
@property({ reflect: true }) open = false;
@@ -154,6 +163,9 @@ export class SortHandle extends LitElement implements InteractiveComponent {
/** Fires when a move item has been selected. */
calciteSortHandleMove = createEvent({ cancelable: true });
+ /** Fires when an add item has been selected. */
+ calciteSortHandleAdd = createEvent({ cancelable: true });
+
/** Fires when the component is open and animation is complete. */
calciteSortHandleOpen = createEvent({ cancelable: false });
@@ -255,6 +267,12 @@ export class SortHandle extends LitElement implements InteractiveComponent {
this.calciteSortHandleMove.emit({ moveTo });
}
+ private handleAddTo(event: Event): void {
+ const id = (event.target as HTMLElement).dataset.id;
+ const addTo = this.addToItems.find((item) => item.id === id);
+ this.calciteSortHandleAdd.emit({ addTo });
+ }
+
// #endregion
// #region Rendering
@@ -304,12 +322,26 @@ export class SortHandle extends LitElement implements InteractiveComponent {
/>
{this.renderReorderGroup()}
{this.renderMoveToGroup()}
+ {this.renderAddToGroup()}
);
}
- private renderMoveToItem(moveToItem: MoveTo): JsxNode {
+ private renderAddToItem(addToItem: SortMenuItem): JsxNode {
+ return (
+
+ {addToItem.label}
+
+ );
+ }
+
+ private renderMoveToItem(moveToItem: SortMenuItem): JsxNode {
return (
+ {addToItems.map((addToItem) => this.renderAddToItem(addToItem))}
+
+ ) : null;
+ }
+
private renderMoveToGroup(): JsxNode {
const { messages, moveToItems, scale } = this;
diff --git a/packages/calcite-components/src/utils/sortableComponent.ts b/packages/calcite-components/src/utils/sortableComponent.ts
index d2fe4ca4e76..922de979ae9 100644
--- a/packages/calcite-components/src/utils/sortableComponent.ts
+++ b/packages/calcite-components/src/utils/sortableComponent.ts
@@ -49,7 +49,7 @@ export interface SortableComponent {
sortable: Sortable;
/** Whether the element can move from the list. */
- canPull: (detail: DragDetail) => boolean;
+ canPull: (detail: DragDetail) => boolean | "clone";
/** Whether the element can be added from another list. */
canPut: (detail: DragDetail) => boolean;