Skip to content

refactor(ui5-list, ui5-tree, ui5-list-item-group): extract drag-and-drop logic #11928

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 6, 2025
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
51 changes: 16 additions & 35 deletions packages/main/src/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ import {
isDown,
isUp,
} from "@ui5/webcomponents-base/dist/Keys.js";
import handleDragOver from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDragOver.js";
import handleDrop from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDrop.js";
import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js";
import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
import DragAndDropHandler from "./delegate/DragAndDropHandler.js";
import type { MoveEventDetail } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
import { findClosestPosition, findClosestPositionsByKey } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js";
import { findClosestPositionsByKey } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js";
import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js";
import {
getAllAccessibleDescriptionRefTexts,
Expand Down Expand Up @@ -543,6 +541,8 @@ class List extends UI5Element {
onForwardBeforeBound: (e: CustomEvent) => void;
onItemTabIndexChangeBound: (e: CustomEvent) => void;

_dragAndDropHandler: DragAndDropHandler;

constructor() {
super();

Expand Down Expand Up @@ -572,6 +572,14 @@ class List extends UI5Element {
this.onForwardAfterBound = this.onForwardAfter.bind(this);
this.onForwardBeforeBound = this.onForwardBefore.bind(this);
this.onItemTabIndexChangeBound = this.onItemTabIndexChange.bind(this);

// Initialize the DragAndDropHandler with the necessary configurations
// The handler will manage the drag and drop operations for the list items.
this._dragAndDropHandler = new DragAndDropHandler(this, {
getItems: () => this.items,
getDropIndicator: () => this.dropIndicatorDOM,
useOriginalEvent: true,
});
}

/**
Expand Down Expand Up @@ -1193,46 +1201,19 @@ class List extends UI5Element {
}

_ondragenter(e: DragEvent) {
e.preventDefault();
this._dragAndDropHandler.ondragenter(e);
}

_ondragleave(e: DragEvent) {
if (e.relatedTarget instanceof Node && this.shadowRoot!.contains(e.relatedTarget)) {
return;
}

this.dropIndicatorDOM!.targetReference = null;
this._dragAndDropHandler.ondragleave(e);
}

_ondragover(e: DragEvent) {
if (!(e.target instanceof HTMLElement)) {
return;
}

const closestPosition = findClosestPosition(
this.items,
e.clientY,
Orientation.Vertical,
);

if (!closestPosition) {
this.dropIndicatorDOM!.targetReference = null;
return;
}

const { targetReference, placement } = handleDragOver(e, this, closestPosition, closestPosition.element, { originalEvent: true });
this.dropIndicatorDOM!.targetReference = targetReference;
this.dropIndicatorDOM!.placement = placement;
this._dragAndDropHandler.ondragover(e);
}

_ondrop(e: DragEvent) {
if (!this.dropIndicatorDOM?.targetReference || !this.dropIndicatorDOM?.placement) {
e.preventDefault();
return;
}

handleDrop(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement, { originalEvent: true });
this.dropIndicatorDOM.targetReference = null;
this._dragAndDropHandler.ondrop(e);
}

isForwardElement(element: HTMLElement) {
Expand Down
93 changes: 26 additions & 67 deletions packages/main/src/ListItemGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
import { findClosestPosition } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js";
import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js";
import DragAndDropHandler from "./delegate/DragAndDropHandler.js";
import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js";
import type DropIndicator from "./DropIndicator.js";
import type ListItemBase from "./ListItemBase.js";
Expand Down Expand Up @@ -148,6 +147,20 @@ class ListItemGroup extends UI5Element {
@slot({ type: HTMLElement })
header!: Array<ListItemBase>;

_dragAndDropHandler: DragAndDropHandler;

constructor() {
super();

// Initialize the DragAndDropHandler with the necessary configurations
// The handler will manage the drag and drop operations for the list items.
this._dragAndDropHandler = new DragAndDropHandler(this, {
getItems: () => this.items,
getDropIndicator: () => this.dropIndicatorDOM,
filterPlacements: this._filterPlacements.bind(this),
});
}

onEnterDOM() {
DragRegistry.subscribe(this);
}
Expand Down Expand Up @@ -177,81 +190,27 @@ class ListItemGroup extends UI5Element {
}

_ondragenter(e: DragEvent) {
e.preventDefault();
this._dragAndDropHandler.ondragenter(e);
}

_ondragleave(e: DragEvent) {
if (e.relatedTarget instanceof Node && this.shadowRoot!.contains(e.relatedTarget)) {
return;
}

this.dropIndicatorDOM!.targetReference = null;
this._dragAndDropHandler.ondragleave(e);
}

_ondragover(e: DragEvent) {
const draggedElement = DragRegistry.getDraggedElement();

if (!(e.target instanceof HTMLElement) || !draggedElement) {
return;
}

const closestPosition = findClosestPosition(
this.items,
e.clientY,
Orientation.Vertical,
);

if (!closestPosition) {
this.dropIndicatorDOM!.targetReference = null;
return;
}

let placements = closestPosition.placements;

if (closestPosition.element === draggedElement) {
placements = placements.filter(placement => placement !== MovePlacement.On);
}

const placementAccepted = placements.some(placement => {
const beforeItemMovePrevented = !this.fireDecoratorEvent("move-over", {
source: {
element: draggedElement,
},
destination: {
element: closestPosition.element,
placement,
},
});

if (beforeItemMovePrevented) {
e.preventDefault();
this.dropIndicatorDOM!.targetReference = closestPosition.element;
this.dropIndicatorDOM!.placement = placement;
return true;
}

return false;
});

if (!placementAccepted) {
this.dropIndicatorDOM!.targetReference = null;
}
this._dragAndDropHandler.ondragover(e);
}

_ondrop(e: DragEvent) {
e.preventDefault();

this.fireDecoratorEvent("move", {
source: {
element: DragRegistry.getDraggedElement()!,
},
destination: {
element: this.dropIndicatorDOM!.targetReference!,
placement: this.dropIndicatorDOM!.placement,
},
});
this._dragAndDropHandler.ondrop(e);
}

this.dropIndicatorDOM!.targetReference = null;
_filterPlacements(placements: MovePlacement[], draggedElement: HTMLElement, targetElement: HTMLElement): MovePlacement[] {
// Filter out MovePlacement.On when dragged element is the same as target
if (targetElement === draggedElement) {
return placements.filter(placement => placement !== MovePlacement.On);
}
return placements;
}
}

Expand Down
92 changes: 47 additions & 45 deletions packages/main/src/Tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
import handleDragOver from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDragOver.js";
import handleDrop from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDrop.js";
import { findClosestPosition } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js";
import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js";
import DragAndDropHandler from "./delegate/DragAndDropHandler.js";
import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
Expand Down Expand Up @@ -311,6 +308,22 @@ class Tree extends UI5Element {
@slot()
header!: Array<HTMLElement>;

_dragAndDropHandler: DragAndDropHandler;

constructor() {
super();

// Initialize the DragAndDropHandler with the necessary configurations
// The handler will manage the drag and drop operations for the tree items.
this._dragAndDropHandler = new DragAndDropHandler(this, {
getItems: this._getItems.bind(this),
getDropIndicator: () => this.dropIndicatorDOM,
transformElement: this._transformElement.bind(this),
validateDraggedElement: this._validateDraggedElement.bind(this),
filterPlacements: this._filterPlacements.bind(this),
});
}

onEnterDOM() {
DragRegistry.subscribe(this);
}
Expand Down Expand Up @@ -346,56 +359,19 @@ class Tree extends UI5Element {
}

_ondragenter(e: DragEvent) {
e.preventDefault();
this._dragAndDropHandler.ondragenter(e);
}

_ondragleave(e: DragEvent) {
if (e.relatedTarget instanceof Node && this.shadowRoot!.contains(e.relatedTarget)) {
return;
}

this.dropIndicatorDOM!.targetReference = null;
this._dragAndDropHandler.ondragleave(e);
}

_ondragover(e: DragEvent) {
const draggedElement = DragRegistry.getDraggedElement();
const allLiNodesTraversed: Array<HTMLElement> = []; // use the only <li> nodes to determine positioning
if (!(e.target instanceof HTMLElement) || !draggedElement) {
return;
}

this.walk(item => {
allLiNodesTraversed.push(item.shadowRoot!.querySelector("li")!);
});

const closestPosition = findClosestPosition(
allLiNodesTraversed,
e.clientY,
Orientation.Vertical,
);

if (!closestPosition) {
this.dropIndicatorDOM!.targetReference = null;
return;
}

closestPosition.element = <HTMLElement>(<ShadowRoot>closestPosition.element.getRootNode()).host;
if (draggedElement.contains(closestPosition.element)) { return; }
if (closestPosition.element === draggedElement) {
closestPosition.placements = closestPosition.placements.filter(placement => placement !== MovePlacement.On);
}

const { targetReference, placement } = handleDragOver(e, this, closestPosition, closestPosition.element);
this.dropIndicatorDOM!.targetReference = targetReference;
this.dropIndicatorDOM!.placement = placement;
this._dragAndDropHandler.ondragover(e);
}

_ondrop(e: DragEvent) {
if (!this.dropIndicatorDOM?.targetReference || !this.dropIndicatorDOM?.placement) {
return;
}
handleDrop(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement);
this.dropIndicatorDOM.targetReference = null;
this._dragAndDropHandler.ondrop(e);
}

_onListItemStepIn(e: CustomEvent<TreeItemBaseStepInEventDetail>) {
Expand Down Expand Up @@ -531,6 +507,32 @@ class Tree extends UI5Element {
walkTree(this, 1, callback);
}

_getItems(): Array<HTMLElement> {
const allLiNodesTraversed: Array<HTMLElement> = [];
this.walk(item => {
allLiNodesTraversed.push(item.shadowRoot!.querySelector("li")!);
});
return allLiNodesTraversed;
}

_transformElement(element: HTMLElement): HTMLElement {
// Get the host element from shadow DOM
return <HTMLElement>(<ShadowRoot>element.getRootNode()).host;
}

_validateDraggedElement(draggedElement: HTMLElement, targetElement: HTMLElement): boolean {
// Don't allow dropping on itself or its children
return !draggedElement.contains(targetElement);
}

_filterPlacements(placements: MovePlacement[], draggedElement: HTMLElement, targetElement: HTMLElement): MovePlacement[] {
// Filter out MovePlacement.On when dragged element is the same as target
if (targetElement === draggedElement) {
return placements.filter(placement => placement !== MovePlacement.On);
}
return placements;
}

_isInstanceOfTreeItemBase(object: any): object is TreeItemBase {
return "isTreeItem" in object;
}
Expand Down
Loading
Loading