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 01d0f01e08b..f65a30035af 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 @@ -44,6 +44,10 @@ describe("calcite-block-group", () => { propertyName: "loading", defaultValue: false, }, + { + propertyName: "sortDisabled", + defaultValue: false, + }, ]); }); @@ -65,6 +69,10 @@ describe("calcite-block-group", () => { propertyName: "loading", value: true, }, + { + propertyName: "sortDisabled", + value: true, + }, ]); }); @@ -109,9 +117,9 @@ describe("calcite-block-group", () => { expect(await items[i].getProperty("dragHandle")).toBe(true); } - const root = await page.find("#root"); + const blockGroup = await page.find("#root"); - root.setProperty("dragEnabled", false); + blockGroup.setProperty("dragEnabled", false); await page.waitForChanges(); await page.waitForTimeout(DEBOUNCE.nextTick); @@ -120,6 +128,36 @@ describe("calcite-block-group", () => { } }); + it("should set the sortDisabled property on items", async () => { + const page = await newE2EPage(); + await page.setContent( + html` + + + + `, + ); + + await page.waitForChanges(); + await page.waitForTimeout(DEBOUNCE.nextTick); + + const items = await findAll(page, "calcite-block"); + + for (let i = 0; i < items.length; i++) { + expect(await items[i].getProperty("sortDisabled")).toBe(true); + } + + const blockGroup = await page.find("#root"); + + blockGroup.setProperty("sortDisabled", false); + await page.waitForChanges(); + await page.waitForTimeout(DEBOUNCE.nextTick); + + for (let i = 0; i < items.length; i++) { + expect(await items[i].getProperty("sortDisabled")).toBe(false); + } + }); + describe("drag and drop", () => { async function createSimpleBlockGroup(): Promise { const page = await newE2EPage(); 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 a36c76697eb..9ee471249a7 100755 --- a/packages/calcite-components/src/components/block-group/block-group.tsx +++ b/packages/calcite-components/src/components/block-group/block-group.tsx @@ -106,6 +106,9 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort /** When `true`, a busy indicator is displayed. */ @property({ reflect: true }) loading = false; + /** When `true`, and a `group` is defined, `calcite-block`s are no longer sortable. */ + @property({ reflect: true }) sortDisabled = false; + // #endregion // #region Public Methods @@ -172,7 +175,8 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort override willUpdate(changes: PropertyValues): void { if ( changes.has("group") || - (changes.has("dragEnabled") && (this.hasUpdated || this.dragEnabled !== false)) + (changes.has("dragEnabled") && (this.hasUpdated || this.dragEnabled !== false)) || + (changes.has("sortDisabled") && (this.hasUpdated || this.sortDisabled !== false)) ) { this.updateBlockItemsDebounced(); } @@ -193,7 +197,7 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort private updateBlockItems(): void { this.updateGroupItems(); - const { dragEnabled, el, moveToItems } = this; + const { dragEnabled, el, moveToItems, sortDisabled } = this; const items = Array.from(this.el.querySelectorAll(blockSelector)); @@ -203,6 +207,7 @@ export class BlockGroup extends LitElement implements InteractiveComponent, Sort (moveToItem) => moveToItem.element !== el && !item.contains(moveToItem.element), ); item.dragHandle = dragEnabled; + item.sortDisabled = sortDisabled; } }); diff --git a/packages/calcite-components/src/components/block/block.e2e.ts b/packages/calcite-components/src/components/block/block.e2e.ts index a968f63a933..a399256353a 100644 --- a/packages/calcite-components/src/components/block/block.e2e.ts +++ b/packages/calcite-components/src/components/block/block.e2e.ts @@ -71,6 +71,10 @@ describe("calcite-block", () => { propertyName: "sortHandleOpen", defaultValue: false, }, + { + propertyName: "sortDisabled", + defaultValue: false, + }, ]); }); diff --git a/packages/calcite-components/src/components/block/block.tsx b/packages/calcite-components/src/components/block/block.tsx index 77db2b9d197..32d74df5966 100644 --- a/packages/calcite-components/src/components/block/block.tsx +++ b/packages/calcite-components/src/components/block/block.tsx @@ -147,6 +147,13 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose */ @property() moveToItems: MoveTo[] = []; + /** + * Prevents reordering the component. + * + * @private + */ + @property() sortDisabled = false; + /** * When `true`, expands the component and its contents. * @@ -480,6 +487,7 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose setPosition, setSize, dragDisabled, + sortDisabled, } = this; const toggleLabel = expanded ? messages.collapse : messages.expand; @@ -513,6 +521,7 @@ export class Block extends LitElement implements InteractiveComponent, OpenClose ref={this.setSortHandleEl} setPosition={setPosition} setSize={setSize} + sortDisabled={sortDisabled} /> ) : null} {collapsible ? ( diff --git a/packages/calcite-components/src/components/list-item/list-item.e2e.ts b/packages/calcite-components/src/components/list-item/list-item.e2e.ts index d476f6d84ea..e66999d6a58 100755 --- a/packages/calcite-components/src/components/list-item/list-item.e2e.ts +++ b/packages/calcite-components/src/components/list-item/list-item.e2e.ts @@ -89,6 +89,10 @@ describe("calcite-list-item", () => { propertyName: "sortHandleOpen", defaultValue: false, }, + { + propertyName: "sortDisabled", + defaultValue: false, + }, ]); }); diff --git a/packages/calcite-components/src/components/list-item/list-item.tsx b/packages/calcite-components/src/components/list-item/list-item.tsx index 52c29848928..5bca489b559 100644 --- a/packages/calcite-components/src/components/list-item/list-item.tsx +++ b/packages/calcite-components/src/components/list-item/list-item.tsx @@ -114,6 +114,13 @@ export class ListItem extends LitElement implements InteractiveComponent, Sortab */ @property() bordered = false; + /** + * Prevents reordering the component. + * + * @private + */ + @property() sortDisabled = false; + /** When `true`, a close button is added to the component. */ @property({ reflect: true }) closable = false; @@ -753,7 +760,8 @@ export class ListItem extends LitElement implements InteractiveComponent, Sortab } private renderDragHandle(): JsxNode { - const { label, dragHandle, dragDisabled, setPosition, setSize, moveToItems } = this; + const { label, dragHandle, dragDisabled, setPosition, setSize, moveToItems, sortDisabled } = + this; return dragHandle ? (
) : null; diff --git a/packages/calcite-components/src/components/list/list.e2e.ts b/packages/calcite-components/src/components/list/list.e2e.ts index 078b23d0429..50acfda8f9d 100755 --- a/packages/calcite-components/src/components/list/list.e2e.ts +++ b/packages/calcite-components/src/components/list/list.e2e.ts @@ -104,6 +104,10 @@ describe("calcite-list", () => { propertyName: "displayMode", defaultValue: "flat", }, + { + propertyName: "sortDisabled", + defaultValue: false, + }, ]); }); @@ -113,6 +117,10 @@ describe("calcite-list", () => { propertyName: "displayMode", value: "nested", }, + { + propertyName: "sortDisabled", + value: true, + }, ]); }); @@ -320,6 +328,36 @@ describe("calcite-list", () => { } }); + it("should set the sortDisabled property on items", async () => { + const page = await newE2EPage(); + await page.setContent( + html` + + + + `, + ); + + await page.waitForChanges(); + await page.waitForTimeout(DEBOUNCE.nextTick); + + const items = await findAll(page, "calcite-list-item"); + + for (let i = 0; i < items.length; i++) { + expect(await items[i].getProperty("sortDisabled")).toBe(true); + } + + const list = await page.find("#root"); + + list.setProperty("sortDisabled", false); + await page.waitForChanges(); + await page.waitForTimeout(DEBOUNCE.nextTick); + + for (let i = 0; i < items.length; i++) { + expect(await items[i].getProperty("sortDisabled")).toBe(false); + } + }); + it("should set the dragHandle property on items which are not direct children", async () => { const page = await newE2EPage(); await page.setContent( diff --git a/packages/calcite-components/src/components/list/list.tsx b/packages/calcite-components/src/components/list/list.tsx index 745f6dc63a5..d1df15beeb3 100755 --- a/packages/calcite-components/src/components/list/list.tsx +++ b/packages/calcite-components/src/components/list/list.tsx @@ -279,6 +279,9 @@ export class List extends LitElement implements InteractiveComponent, SortableCo SelectionMode > = "none"; + /** When `true`, and a `group` is defined, `calcite-list-item`s are no longer sortable. */ + @property({ reflect: true }) sortDisabled = false; + //#endregion //#region Public Methods @@ -393,6 +396,7 @@ export class List extends LitElement implements InteractiveComponent, SortableCo if ( (changes.has("filterEnabled") && (this.hasUpdated || this.filterEnabled !== false)) || changes.has("group") || + (changes.has("sortDisabled") && (this.hasUpdated || this.sortDisabled !== false)) || (changes.has("dragEnabled") && (this.hasUpdated || this.dragEnabled !== false)) || (changes.has("selectionMode") && (this.hasUpdated || this.selectionMode !== "none")) || (changes.has("selectionAppearance") && @@ -431,6 +435,7 @@ export class List extends LitElement implements InteractiveComponent, SortableCo moveToItems, displayMode, scale, + sortDisabled, } = this; const items = Array.from(this.el.querySelectorAll(listItemSelector)); @@ -447,6 +452,7 @@ export class List extends LitElement implements InteractiveComponent, SortableCo item.dragHandle = dragEnabled; item.displayMode = displayMode; + item.sortDisabled = sortDisabled; } }); 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 0c729ace1cb..7245c00044c 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 @@ -1,14 +1,42 @@ // @ts-strict-ignore import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; -import { accessible, disabled, hidden, renders, t9n, openClose, focusable } from "../../tests/commonTests"; +import { + accessible, + disabled, + hidden, + renders, + t9n, + openClose, + focusable, + reflects, + defaults, +} from "../../tests/commonTests"; import { skipAnimations } from "../../tests/utils/puppeteer"; import T9nStrings from "./assets/t9n/messages.en.json"; -import { CSS, REORDER_VALUES, SUBSTITUTIONS } from "./resources"; +import { CSS, IDS, REORDER_VALUES, SUBSTITUTIONS } from "./resources"; import type { MoveEventDetail } from "./interfaces"; import type { ReorderEventDetail } from "./interfaces"; describe("calcite-sort-handle", () => { + describe("defaults", () => { + defaults("calcite-sort-handle", [ + { + propertyName: "sortDisabled", + defaultValue: false, + }, + ]); + }); + + describe("reflects", () => { + reflects("calcite-sort-handle", [ + { + propertyName: "sortDisabled", + value: true, + }, + ]); + }); + describe("renders", () => { renders("calcite-sort-handle", { display: "flex" }); }); @@ -22,7 +50,7 @@ describe("calcite-sort-handle", () => { }); describe("focusable", () => { - focusable("calcite-sort-handle"); + focusable(``); }); describe("accessible", () => { @@ -118,7 +146,7 @@ describe("calcite-sort-handle", () => { expect(calciteSortHandleMoveSpy.lastEvent.cancelable).toBe(true); }); - it("is disabled when no moveToItems, setPosition < 1 or setSize < 2", async () => { + it("is disabled when no moveToItems and sortDisabled, setPosition < 1 or setSize < 2", async () => { const page = await newE2EPage(); await page.setContent(``); await skipAnimations(page); @@ -140,6 +168,7 @@ describe("calcite-sort-handle", () => { expect(await dropdown.getProperty("disabled")).toBe(false); + sortHandle.setProperty("moveToItems", []); sortHandle.setProperty("setPosition", 0); await page.waitForChanges(); @@ -163,14 +192,43 @@ describe("calcite-sort-handle", () => { expect(await dropdown.getProperty("disabled")).toBe(false); + sortHandle.setProperty("sortDisabled", true); sortHandle.setProperty("moveToItems", []); - sortHandle.setProperty("setSize", undefined); - sortHandle.setProperty("setPosition", undefined); + await page.waitForChanges(); + + expect(await dropdown.getProperty("disabled")).toBe(true); + + sortHandle.setProperty("sortDisabled", false); await page.waitForChanges(); expect(await dropdown.getProperty("disabled")).toBe(false); }); + it("doesn't render reorder group when sortDisabled is true", async () => { + const page = await newE2EPage(); + await page.setContent(``); + await skipAnimations(page); + + const sortHandle = await page.find("calcite-sort-handle"); + + const moveToItems = [ + { label: "List 2", id: "list2" }, + { label: "List 3", id: "list3" }, + ]; + + sortHandle.setProperty("setSize", 2); + sortHandle.setProperty("setPosition", 1); + sortHandle.setProperty("moveToItems", moveToItems); + await page.waitForChanges(); + + expect(await page.find(`calcite-sort-handle >>> #${IDS.reorder}`)).toBeDefined(); + + sortHandle.setProperty("sortDisabled", true); + await page.waitForChanges(); + + expect(await page.find(`calcite-sort-handle >>> #${IDS.reorder}`)).toBeNull(); + }); + describe("translation support", () => { t9n("calcite-sort-handle"); }); 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 f5596663484..d871d38e907 100644 --- a/packages/calcite-components/src/components/sort-handle/sort-handle.tsx +++ b/packages/calcite-components/src/components/sort-handle/sort-handle.tsx @@ -48,12 +48,16 @@ export class SortHandle extends LitElement implements InteractiveComponent { return typeof this.setPosition === "number" && typeof this.setSize === "number"; } - @state() get isSetDisabled(): boolean { - const { setPosition, setSize, moveToItems } = this; + @state() get hasValidSetInfo(): boolean { + return this.hasSetInfo ? this.setPosition > 0 && this.setSize > 1 : true; + } + + @state() get hasReorderItems(): boolean { + return !this.sortDisabled && this.hasValidSetInfo; + } - return this.hasSetInfo - ? setPosition < 1 || setSize < 1 || (setSize < 2 && moveToItems.length < 1) - : false; + @state() get hasNoItems(): boolean { + return !this.hasReorderItems && this.moveToItems.length < 1; } // #endregion @@ -110,6 +114,9 @@ export class SortHandle extends LitElement implements InteractiveComponent { /** The total number of sortable items. */ @property() setSize: number; + /** When `true`, items are no longer sortable. */ + @property({ reflect: true }) sortDisabled = false; + /** Specifies the width of the component. */ @property({ reflect: true }) widthScale: Scale; @@ -196,9 +203,9 @@ export class SortHandle extends LitElement implements InteractiveComponent { } private getLabel(): string { - const { label, messages, setPosition, setSize } = this; + const { label, messages, setPosition, setSize, hasSetInfo } = this; - if (!this.hasSetInfo) { + if (!hasSetInfo) { return label ?? ""; } @@ -253,11 +260,19 @@ export class SortHandle extends LitElement implements InteractiveComponent { // #region Rendering override render(): JsxNode { - const { disabled, flipPlacements, open, overlayPositioning, placement, scale, widthScale } = - this; + const { + disabled, + flipPlacements, + open, + overlayPositioning, + placement, + scale, + widthScale, + hasNoItems, + } = this; const text = this.getLabel(); - const isDisabled = disabled || this.isSetDisabled; + const isDisabled = disabled || hasNoItems; return ( @@ -287,7 +302,7 @@ export class SortHandle extends LitElement implements InteractiveComponent { text={text} title={text} /> - {this.renderGroup()} + {this.renderReorderGroup()} {this.renderMoveToGroup()} @@ -307,8 +322,8 @@ export class SortHandle extends LitElement implements InteractiveComponent { ); } - private renderGroup(): JsxNode { - return this.hasSetInfo ? ( + private renderReorderGroup(): JsxNode { + return this.hasReorderItems ? ( { `, ); - const root = await page.find("#root"); + const list = await page.find("#root"); const parent = await page.find("#parent"); const child2 = await page.find("#child2"); - await root.focus(); + await list.focus(); await page.waitForChanges(); expect(await getFocusedElementProp(page, "id")).toEqual("root-item-1"); diff --git a/packages/calcite-components/src/demos/sortable-list.html b/packages/calcite-components/src/demos/sortable-list.html index 0dae60feb29..e3100f8f88a 100644 --- a/packages/calcite-components/src/demos/sortable-list.html +++ b/packages/calcite-components/src/demos/sortable-list.html @@ -73,7 +73,7 @@

Sortable list

calcite-sortable-list of calcite-block elements
- + demo @@ -101,7 +101,7 @@

Sortable list

vertical
- + demo @@ -148,7 +148,7 @@

Sortable list

Nested Example
- + content A diff --git a/packages/calcite-components/src/utils/sortableComponent.ts b/packages/calcite-components/src/utils/sortableComponent.ts index 327257113c8..c1b6853a59b 100644 --- a/packages/calcite-components/src/utils/sortableComponent.ts +++ b/packages/calcite-components/src/utils/sortableComponent.ts @@ -33,6 +33,9 @@ export interface SortableComponent { /** When `true`, dragging is enabled. */ dragEnabled: boolean; + /** When `true`, sorting is disabled. */ + sortDisabled?: boolean; + /** Specifies which items inside the element should be draggable. */ dragSelector?: string; @@ -97,13 +100,14 @@ export function connectSortableComponent(component: SortableComponent): void { sortableComponentSet.add(component); const dataIdAttr = "id"; - const { group, handleSelector: handle, dragSelector: draggable } = component; + const { group, handleSelector: handle, dragSelector: draggable, sortDisabled } = component; component.sortable = Sortable.create(component.el, { dataIdAttr, ...CSS, ...(!!draggable && { draggable }), ...(!!group && { + sort: !sortDisabled, group: { name: group, ...(!!component.canPull && {