diff --git a/packages/main/cypress/specs/ListDragAndDrop.cy.tsx b/packages/main/cypress/specs/ListDragAndDrop.cy.tsx new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/main/src/List.ts b/packages/main/src/List.ts index 1de99b6cf2df..b656aed3b5fb 100644 --- a/packages/main/src/List.ts +++ b/packages/main/src/List.ts @@ -52,6 +52,7 @@ import type { import type DropIndicator from "./DropIndicator.js"; import type ListItem from "./ListItem.js"; import type { + MoveStartEventDetail, SelectionRequestEventDetail, } from "./ListItem.js"; import ListSeparator from "./types/ListSeparator.js"; @@ -70,6 +71,7 @@ import { LOAD_MORE_TEXT, ARIA_LABEL_LIST_SELECTABLE, ARIA_LABEL_LIST_MULTISELECTABLE, ARIA_LABEL_LIST_DELETABLE, + LIST_DRAG_GHOST_TEXT, } from "./generated/i18n/i18n-defaults.js"; import type CheckBox from "./CheckBox.js"; import type RadioButton from "./RadioButton.js"; @@ -112,6 +114,9 @@ type ListItemClickEventDetail = { type ListMoveEventDetail = MoveEventDetail; +// Specific template type for the drag ghost +type ListDragElementTemplate = (this: List) => JSX.Element; + /** * @class * @@ -299,6 +304,17 @@ class List extends UI5Element { "move-over": ListMoveEventDetail, "move": ListMoveEventDetail, } + + /** + * Defines the number of dragged items. Use this property to indicate that multiple items are being dragged. + * When a value greater than 1 is set, the component will display a custom ghost element that displays the number of dragged items. + * @default 0 + * @public + * @since 2.10.0 + */ + @property({ type: Number }) + movingItemsCount = 0; + /** * Defines the component header text. * @@ -473,6 +489,13 @@ class List extends UI5Element { @property() mediaRange = "S"; + /** + * The custom template for the drag ghost element. + * @private + */ + @property({ noAttribute: true }) + dragElementTemplate?: ListDragElementTemplate; + /** * Defines the items of the component. * @@ -578,6 +601,18 @@ class List extends UI5Element { onBeforeRendering() { this.detachGroupHeaderEvents(); this.prepareListItems(); + + if (this.showDragGhost) { + // If feature is already loaded (preloaded by the user via importing ListItemStandardExpandableText.js), the template is already available + if (List.ListDragElementTemplate) { + this.dragElementTemplate = List.ListDragElementTemplate; + // If feature is not preloaded, load the template dynamically + } else { + import("./features/ListDragElementTemplate.js").then(module => { + this.dragElementTemplate = module.default; + }); + } + } } onAfterRendering() { @@ -615,6 +650,18 @@ class List extends UI5Element { }); } + get dragGhost() { + return this.shadowRoot!.querySelector(".ui5-list-drag-ghost"); + } + + get showDragGhost() { + return this.movingItemsCount > 1; + } + + get dragGhostText() { + return List.i18nBundle.getText(LIST_DRAG_GHOST_TEXT, this.movingItemsCount); + } + get shouldRenderH1() { return !this.header.length && this.headerText; } @@ -1214,6 +1261,13 @@ class List extends UI5Element { return afterElement && afterElement.id === elementId; } + onItemMoveStart(e: CustomEvent) { + const originalEvent = e.detail.originalEvent; + if (this.dragGhost && originalEvent.dataTransfer) { + originalEvent.dataTransfer.setDragImage(this.dragGhost, 0, 0); + } + } + onItemTabIndexChange(e: CustomEvent) { e.stopPropagation(); const target = e.target as ListItemBase; @@ -1430,6 +1484,8 @@ class List extends UI5Element { return this.growingIntersectionObserver; } + + static ListDragElementTemplate?: ListDragElementTemplate; } List.define(); diff --git a/packages/main/src/ListItem.ts b/packages/main/src/ListItem.ts index d56489ab4594..b8fc470f691b 100644 --- a/packages/main/src/ListItem.ts +++ b/packages/main/src/ListItem.ts @@ -49,6 +49,10 @@ type SelectionRequestEventDetail = { key?: string, } +type MoveStartEventDetail = { + originalEvent: DragEvent, +} + type AccInfo = { role?: AriaRole | undefined; ariaExpanded?: boolean; @@ -97,10 +101,14 @@ type ListItemAccessibilityAttributes = Pick + {this.showDragGhost && dragGhost.call(this)} + {this.showNoDataText &&
  • @@ -112,3 +115,7 @@ function moreRow(this: List) {
    ); } + +function dragGhost(this: List) { + return this.dragElementTemplate?.call(this); +} diff --git a/packages/main/src/features/ListDragElement.ts b/packages/main/src/features/ListDragElement.ts new file mode 100644 index 000000000000..fe44602f054d --- /dev/null +++ b/packages/main/src/features/ListDragElement.ts @@ -0,0 +1,5 @@ +import List from "../List.js"; +import ListDragElementTemplate from "./ListDragElementTemplate.js"; + +// Assigns the template to the component to be used when the feature is preloaded +List.ListDragElementTemplate = ListDragElementTemplate; diff --git a/packages/main/src/features/ListDragElementTemplate.tsx b/packages/main/src/features/ListDragElementTemplate.tsx new file mode 100644 index 000000000000..9583c187dfab --- /dev/null +++ b/packages/main/src/features/ListDragElementTemplate.tsx @@ -0,0 +1,20 @@ +import type List from "../List.js"; +import ListItemStandard from "../ListItemStandard.js"; + +/** + * Provides a template for rendering the drag ghost text in the List component + * when multiple items are dragged. + * + * @returns {JSX.Element} The rendered drag ghost + */ +export default function ListDragElementTemplate( + this: List, +): JSX.Element { + return ( + + ); +} diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index 502bee34a30e..36bf5f2cd0d3 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -267,6 +267,8 @@ LIST_ITEM_NOT_SELECTED=Not Selected #XACT: ARIA announcement for the list item selected=false state LIST_ITEM_GROUP_HEADER=Group Header +LIST_DRAG_GHOST_TEXT={0} items + #XACT: ARIA announcement for grouping with role=list LIST_ROLE_LIST_GROUP_DESCRIPTION=contains {0} sub groups with {1} items diff --git a/packages/main/src/themes/DraggableElement.css b/packages/main/src/themes/DraggableElement.css index b6f44b90ac8e..24466372b819 100644 --- a/packages/main/src/themes/DraggableElement.css +++ b/packages/main/src/themes/DraggableElement.css @@ -5,4 +5,10 @@ [draggable=true][data-moving] { cursor: grabbing !important; opacity: var(--sapContent_DisabledOpacity); +} + +[data-custom-drag-ghost] { + position: absolute; + transform: translate(-10000px); + width: 100%; } \ No newline at end of file diff --git a/packages/main/src/themes/List.css b/packages/main/src/themes/List.css index 0142bf6052b1..32f52ce9f788 100644 --- a/packages/main/src/themes/List.css +++ b/packages/main/src/themes/List.css @@ -1,5 +1,6 @@ @import "./InvisibleTextStyles.css"; @import "./GrowingButton.css"; +@import "./DraggableElement.css"; :host(:not([hidden])) { display: block; diff --git a/packages/main/test/pages/ListDragAndDrop.html b/packages/main/test/pages/ListDragAndDrop.html index c3f1e2701f8c..ed743d5670ca 100644 --- a/packages/main/test/pages/ListDragAndDrop.html +++ b/packages/main/test/pages/ListDragAndDrop.html @@ -8,6 +8,12 @@ + + @@ -46,6 +52,32 @@

    Drag and drop

    +
    +
    + + Sunroof + Navigation + + + Leather + Heated + Massage + + + + LED + Matrix + Laser + + + + + Ceramic Brakes + Tinted Windows + +
    +
    + diff --git a/packages/website/docs/_components_pages/main/List/List.mdx b/packages/website/docs/_components_pages/main/List/List.mdx index 1526a54703ef..1a4f9741add9 100644 --- a/packages/website/docs/_components_pages/main/List/List.mdx +++ b/packages/website/docs/_components_pages/main/List/List.mdx @@ -11,6 +11,7 @@ import GroupHeaders from "../../../_samples/main/List/GroupHeaders/GroupHeaders. import SeparationTypes from "../../../_samples/main/List/SeparationTypes/SeparationTypes.md"; import DragAndDrop from "../../../_samples/main/List/DragAndDrop/DragAndDrop.md"; import WrappingBehavior from "../../../_samples/main/List/WrappingBehavior/WrappingBehavior.md"; +import DragAndDropMultiple from "../../../_samples/main/List/DragAndDropMultiple/DragAndDropMultiple.md"; <%COMPONENT_OVERVIEW%> @@ -57,6 +58,11 @@ The list items are draggable through the use of the movable property on < +### Drag And Drop of Multiple Items +You can show a multiple drag image by using the movingItemsCount property on List. + + + ### Wrapping Behavior The standard list item `` supports text wrapping through the wrappingType property. When set to "Normal", long text content (title and description) will wrap to multiple lines instead of truncating with an ellipsis. For very long content, the text is displayed with a "Show More/Show Less" mechanism. diff --git a/packages/website/docs/_samples/main/List/DragAndDropMultiple/DragAndDropMultiple.md b/packages/website/docs/_samples/main/List/DragAndDropMultiple/DragAndDropMultiple.md new file mode 100644 index 000000000000..e11458217b49 --- /dev/null +++ b/packages/website/docs/_samples/main/List/DragAndDropMultiple/DragAndDropMultiple.md @@ -0,0 +1,4 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; + + diff --git a/packages/website/docs/_samples/main/List/DragAndDropMultiple/main.js b/packages/website/docs/_samples/main/List/DragAndDropMultiple/main.js new file mode 100644 index 000000000000..16a4369e0c66 --- /dev/null +++ b/packages/website/docs/_samples/main/List/DragAndDropMultiple/main.js @@ -0,0 +1,56 @@ +import "@ui5/webcomponents/dist/List.js"; +import "@ui5/webcomponents/dist/ListItemStandard.js"; +import "@ui5/webcomponents/dist/ListItemGroup.js"; +import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; + +const listMultipleDnd = document.getElementById("listMultipleDnd"); +const listMultipleDnd1 = document.getElementById("listMultipleDnd1"); +const group1 = document.getElementById("group1"); +const group2 = document.getElementById("group2"); + +let selectedItems = []; + +const handleBeforeItemMove = (e) => { + const { destination, source } = e.detail; + const isOn = destination.placement === MovePlacement.On; + const isAfter = destination.placement === MovePlacement.After; + const isBefore = destination.placement === MovePlacement.Before; + const isNesting = "allowsNesting" in destination.element.dataset; + + if (isBefore || isAfter || (isOn && isNesting)) { + e.preventDefault(); + } +}; + +function updateMovingItemsCount() { + listMultipleDnd.movingItemsCount = listMultipleDnd.getSelectedItems().length; + listMultipleDnd1.movingItemsCount = listMultipleDnd1.getSelectedItems().length; +} + +const onMove = async (e) => { + const { destination, source, originalEvent } = e.detail; + const movedElements = selectedItems.length > 1 ? selectedItems : [source.element]; + + // stop the propagation of the event to prevent when the items are moved + // inside of a group to prevent double handling by the list handler + originalEvent?.stopPropagation(); + + if (destination.placement === MovePlacement.Before) { + destination.element.before(...movedElements); + } else if (destination.placement === MovePlacement.After) { + destination.element.after(...movedElements); + } else if (destination.placement === MovePlacement.On) { + destination.element.prepend(...movedElements); + } + await window["sap-ui-webcomponents-bundle"].renderFinished(); + updateMovingItemsCount(); +}; + +[group1, group2, listMultipleDnd1, listMultipleDnd].forEach((el) => { + if (el.tagName === "UI5-LIST") { + el.addEventListener("dragstart", () => selectedItems = el.getSelectedItems()); + el.addEventListener("ui5-selection-change", updateMovingItemsCount); + el.addEventListener("ui5-move-over", handleBeforeItemMove); + } + el.addEventListener("ui5-move", onMove); +}); \ No newline at end of file diff --git a/packages/website/docs/_samples/main/List/DragAndDropMultiple/sample.html b/packages/website/docs/_samples/main/List/DragAndDropMultiple/sample.html new file mode 100644 index 000000000000..a25a50d31401 --- /dev/null +++ b/packages/website/docs/_samples/main/List/DragAndDropMultiple/sample.html @@ -0,0 +1,43 @@ + + + + + + + + Sample + + + + + +
    + + Sunroof + Navigation + + + Leather + Heated + Massage + + + + LED + Matrix + Laser + + + + + Ceramic Brakes + Tinted Windows + +
    + + + + + + +