Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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,
});
}
Expand All @@ -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`
<calcite-block-group label="First Letters" 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 label="Second Letters" 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 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();

Expand Down
102 changes: 73 additions & 29 deletions packages/calcite-components/src/components/block-group/block-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -207,25 +213,34 @@ 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;
const fromElItems = Array.from(fromEl.children).filter(isBlock);

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;
Expand All @@ -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(),
Expand All @@ -267,6 +282,15 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort
this.handleReorder(event);
}

private handleSortAdd(event: CustomEvent<AddEventDetail>): void {
if (this.parentBlockGroupEl || event.defaultPrevented) {
return;
}

event.preventDefault();
this.handleAdd(event);
}

private handleSortMove(event: CustomEvent<MoveEventDetail>): void {
if (this.parentBlockGroupEl || event.defaultPrevented) {
return;
Expand Down Expand Up @@ -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<AddEventDetail>): 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);
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.

We could use filterDirectChildren if we update it to support a predicate in addition to a selector.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, if we want to update that. I'll create a follow up issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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<MoveEventDetail>): void {
Expand All @@ -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;
}

Expand Down
21 changes: 18 additions & 3 deletions packages/calcite-components/src/components/block/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 });

Expand Down Expand Up @@ -512,6 +525,7 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose
menuFlipPlacements,
menuPlacement,
moveToItems,
addToItems,
setPosition,
setSize,
dragDisabled,
Expand All @@ -538,6 +552,7 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose
<div class={CSS.headerContainer}>
{this.dragHandle ? (
<calcite-sort-handle
addToItems={addToItems}
disabled={dragDisabled}
label={heading || label}
moveToItems={moveToItems}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
* @prop --calcite-input-suffix-text-color: Specifies the text color of the suffix element.
*/

// AUTO-GENERATED — do not modify. Changes will be overwritten.
//
// Internal CSS custom properties for component use only. Overwriting is not recommended.
//
// --calcite-internal-input-number-alignment

:host {
@apply block;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
* @prop --calcite-input-text-text-color-focus: Specifies the component's text color when focused.
*/

// AUTO-GENERATED — do not modify. Changes will be overwritten.
//
// Internal CSS custom properties for component use only. Overwriting is not recommended.
//
// --calcite-internal-input-text-alignment

:host {
@apply block;
}
Expand Down
Loading
Loading