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
{this.dragHandle ? ( { expect(moveToItemIds.length).toBe(6); - const uniqueMoveToItemIds = new Set(moveToItemIds); - - expect(uniqueMoveToItemIds.size).toBe(2); - const moveToItemElementIds = await page.evaluate((letterItemSelector) => { return Array.from(document.querySelectorAll(letterItemSelector)) .map((item: ListItem["el"]) => item.moveToItems.map((moveToItem) => moveToItem.element.id)) @@ -1905,7 +1901,7 @@ describe("calcite-list", () => { 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, }); } @@ -1925,6 +1921,47 @@ describe("calcite-list", () => { 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 List["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 createSimpleList(); diff --git a/packages/calcite-components/src/components/list/list.tsx b/packages/calcite-components/src/components/list/list.tsx index 326059ce077..091762ab16f 100755 --- a/packages/calcite-components/src/components/list/list.tsx +++ b/packages/calcite-components/src/components/list/list.tsx @@ -27,7 +27,12 @@ import { } from "../../utils/sortableComponent"; import { SLOTS as STACK_SLOTS } from "../stack/resources"; import { NumberingSystem, numberStringFormatter } from "../../utils/locale"; -import { MoveEventDetail, MoveTo, ReorderEventDetail } from "../sort-handle/interfaces"; +import { + MoveEventDetail, + SortMenuItem, + ReorderEventDetail, + AddEventDetail, +} from "../sort-handle/interfaces"; import { guid } from "../../utils/guid"; import { useT9n } from "../../controllers/useT9n"; import { useCancelable } from "../../controllers/useCancelable"; @@ -126,7 +131,7 @@ export class List extends LitElement implements InteractiveComponent, SortableCo @state() hasFilterNoResults = false; - @state() moveToItems: MoveTo[] = []; + @state() sortHandleMenuItems: SortMenuItem[] = []; @state() get hasActiveFilter(): boolean { return ( @@ -151,7 +156,7 @@ export class List extends LitElement implements InteractiveComponent, SortableCo //#region Public Properties /** When provided, the method will be called to determine whether the element can move from the list. */ - @property() canPull: (detail: ListDragDetail) => boolean; + @property() canPull: (detail: ListDragDetail) => boolean | "clone"; /** When provided, the method will be called to determine whether the element can be added from another list. */ @property() canPut: (detail: ListDragDetail) => boolean; @@ -352,6 +357,7 @@ export class List extends LitElement implements InteractiveComponent, SortableCo ); this.listen("calciteSortHandleReorder", this.handleSortReorder); this.listen("calciteSortHandleMove", this.handleSortMove); + this.listen("calciteSortHandleAdd", this.handleSortAdd); this.listen("calciteInternalListItemSelect", this.handleCalciteInternalListItemSelect); this.listen( "calciteInternalListItemSelectMultiple", @@ -433,10 +439,10 @@ export class List extends LitElement implements InteractiveComponent, SortableCo dragEnabled, el, filterEl, - moveToItems, displayMode, scale, sortDisabled, + sortHandleMenuItems, } = this; const items = Array.from(this.el.querySelectorAll(listItemSelector)); @@ -449,17 +455,26 @@ export class List extends LitElement implements InteractiveComponent, SortableCo item.selectionMode = selectionMode; item.interactionMode = interactionMode; if (item.closest(listSelector) === el) { - item.moveToItems = moveToItems.filter( - (moveToItem) => - moveToItem.element !== el && - !item.contains(moveToItem.element) && - this.validateMove({ - fromEl, - toEl: moveToItem.element as List["el"], - dragEl: item, - newIndex: 0, - oldIndex: fromElItems.indexOf(item), - }), + item.moveToItems = sortHandleMenuItems.filter((moveToItem) => + this.validateSortMenuItem({ + type: "move", + fromEl, + toEl: moveToItem.element as List["el"], + dragEl: item, + newIndex: 0, + oldIndex: fromElItems.indexOf(item), + }), + ); + + item.addToItems = this.sortHandleMenuItems.filter((moveToItem) => + this.validateSortMenuItem({ + type: "add", + fromEl, + toEl: moveToItem.element as List["el"], + dragEl: item, + newIndex: 0, + oldIndex: fromElItems.indexOf(item), + }), ); item.dragHandle = dragEnabled; @@ -559,6 +574,15 @@ export class List extends LitElement implements InteractiveComponent, SortableCo this.handleReorder(event); } + private handleSortAdd(event: CustomEvent): void { + if (this.parentListEl || event.defaultPrevented) { + return; + } + + event.preventDefault(); + this.handleAdd(event); + } + private handleSortMove(event: CustomEvent): void { if (this.parentListEl || event.defaultPrevented) { return; @@ -880,7 +904,7 @@ export class List extends LitElement implements InteractiveComponent, SortableCo ).filter((list) => !list.disabled && list.dragEnabled) : []; - this.moveToItems = lists.map((element) => ({ + this.sortHandleMenuItems = lists.map((element) => ({ element, label: element.label ?? element.id, id: guid(), @@ -971,48 +995,69 @@ export class List extends LitElement implements InteractiveComponent, SortableCo } } - private validateMove({ + private validateSortMenuItem({ fromEl, toEl, dragEl, newIndex, oldIndex, + type, }: { fromEl?: List["el"]; toEl?: List["el"]; dragEl: ListItem["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 ListItem["el"]; + const fromEl = dragEl?.parentElement as List["el"]; + const toEl = addTo.element as List["el"]; + const fromElItems = Array.from(fromEl.children).filter(isListItem); + const oldIndex = fromElItems.indexOf(dragEl); + const newIndex = 0; + + if (!this.validateSortMenuItem({ type: "add", fromEl, toEl, dragEl, newIndex, oldIndex })) { + return; } - return true; + dragEl.sortHandleOpen = false; + + this.disconnectObserver(); + + const newEl = dragEl.cloneNode(); + toEl.prepend(newEl); + expandedAncestors(dragEl); + this.updateListItemsDebounced(); + this.connectObserver(); } private handleMove(event: CustomEvent): void { @@ -1025,7 +1070,7 @@ export class List extends LitElement implements InteractiveComponent, SortableCo 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/sort-handle/assets/t9n/messages.en.json b/packages/calcite-components/src/components/sort-handle/assets/t9n/messages.en.json index 9aa75ac68d5..6e0f5709da9 100644 --- a/packages/calcite-components/src/components/sort-handle/assets/t9n/messages.en.json +++ b/packages/calcite-components/src/components/sort-handle/assets/t9n/messages.en.json @@ -1,5 +1,6 @@ { "reorder": "Reorder", + "addTo": "Add to", "moveTo": "Move to", "moveToTop": "Move to top", "moveToBottom": "Move to bottom", diff --git a/packages/calcite-components/src/components/sort-handle/assets/t9n/messages.json b/packages/calcite-components/src/components/sort-handle/assets/t9n/messages.json index 9aa75ac68d5..6e0f5709da9 100644 --- a/packages/calcite-components/src/components/sort-handle/assets/t9n/messages.json +++ b/packages/calcite-components/src/components/sort-handle/assets/t9n/messages.json @@ -1,5 +1,6 @@ { "reorder": "Reorder", + "addTo": "Add to", "moveTo": "Move to", "moveToTop": "Move to top", "moveToBottom": "Move to bottom", diff --git a/packages/calcite-components/src/components/sort-handle/interfaces.ts b/packages/calcite-components/src/components/sort-handle/interfaces.ts index 55c3ce7d933..f583596db5c 100644 --- a/packages/calcite-components/src/components/sort-handle/interfaces.ts +++ b/packages/calcite-components/src/components/sort-handle/interfaces.ts @@ -1,6 +1,6 @@ export type Reorder = "up" | "down" | "top" | "bottom"; -export interface MoveTo { +export interface SortMenuItem { element: HTMLElement; id: string; label: string; @@ -11,5 +11,9 @@ export interface ReorderEventDetail { } export interface MoveEventDetail { - moveTo: MoveTo; + moveTo: SortMenuItem; +} + +export interface AddEventDetail { + addTo: SortMenuItem; } diff --git a/packages/calcite-components/src/components/sort-handle/resources.ts b/packages/calcite-components/src/components/sort-handle/resources.ts index 2e39dde5d8d..48c322e0fbb 100644 --- a/packages/calcite-components/src/components/sort-handle/resources.ts +++ b/packages/calcite-components/src/components/sort-handle/resources.ts @@ -23,6 +23,7 @@ export const SLOTS = { }; export const IDS = { + add: "add", move: "move", reorder: "reorder", }; diff --git a/packages/calcite-components/src/components/sort-handle/sort-handle.e2e.ts b/packages/calcite-components/src/components/sort-handle/sort-handle.e2e.ts index 7245c00044c..9db22d5f2c2 100644 --- a/packages/calcite-components/src/components/sort-handle/sort-handle.e2e.ts +++ b/packages/calcite-components/src/components/sort-handle/sort-handle.e2e.ts @@ -15,7 +15,7 @@ import { import { skipAnimations } from "../../tests/utils/puppeteer"; import T9nStrings from "./assets/t9n/messages.en.json"; import { CSS, IDS, REORDER_VALUES, SUBSTITUTIONS } from "./resources"; -import type { MoveEventDetail } from "./interfaces"; +import type { AddEventDetail, MoveEventDetail } from "./interfaces"; import type { ReorderEventDetail } from "./interfaces"; describe("calcite-sort-handle", () => { @@ -25,6 +25,22 @@ describe("calcite-sort-handle", () => { propertyName: "sortDisabled", defaultValue: false, }, + { + propertyName: "setPosition", + defaultValue: undefined, + }, + { + propertyName: "setSize", + defaultValue: undefined, + }, + { + propertyName: "moveToItems", + defaultValue: [], + }, + { + propertyName: "addToItems", + defaultValue: [], + }, ]); }); @@ -146,6 +162,38 @@ describe("calcite-sort-handle", () => { expect(calciteSortHandleMoveSpy.lastEvent.cancelable).toBe(true); }); + it("fires calciteSortHandleAdd event", async () => { + const page = await newE2EPage(); + await page.setContent(``); + await skipAnimations(page); + + const addToItems = [ + { label: "List 2", id: "list2" }, + { label: "List 3", id: "list3" }, + ]; + + const sortHandle = await page.find("calcite-sort-handle"); + sortHandle.setProperty("addToItems", addToItems); + await page.waitForChanges(); + + const calciteSortHandleAddSpy = await page.spyOnEvent("calciteSortHandleAdd"); + + const action = await page.find(`calcite-sort-handle >>> .${CSS.handle}`); + await action.callMethod("setFocus"); + + const openEventSpy = await page.spyOnEvent("calciteSortHandleOpen"); + await page.keyboard.press("ArrowUp"); + await page.waitForChanges(); + await openEventSpy.next(); + expect(await sortHandle.getProperty("open")).toBe(true); + + await page.keyboard.press(" "); + await page.waitForChanges(); + expect(calciteSortHandleAddSpy.lastEvent.detail.addTo.id).toBe(addToItems[1].id); + expect(calciteSortHandleAddSpy).toHaveReceivedEventTimes(1); + expect(calciteSortHandleAddSpy.lastEvent.cancelable).toBe(true); + }); + it("is disabled when no moveToItems and sortDisabled, setPosition < 1 or setSize < 2", async () => { const page = await newE2EPage(); await page.setContent(``); diff --git a/packages/calcite-components/src/components/sort-handle/sort-handle.stories.ts b/packages/calcite-components/src/components/sort-handle/sort-handle.stories.ts index efa09476cbb..96de7ac700d 100644 --- a/packages/calcite-components/src/components/sort-handle/sort-handle.stories.ts +++ b/packages/calcite-components/src/components/sort-handle/sort-handle.stories.ts @@ -47,7 +47,7 @@ export const positions = (): string => html`
`; -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;