Skip to content
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

PoC: Table Drag & Drop #9955

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
98 changes: 98 additions & 0 deletions packages/base/src/util/dragAndDrop/DragRowUtil.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the idea of this util. Please move both functions to separate files. Something like handleDragOver.ts and handleDrop.ts

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type UI5Element from "../../UI5Element.js";
import type MovePlacement from "../../types/MovePlacement.js";
import DragRegistry from "./DragRegistry.js";
import { findClosestPosition } from "./findClosestPosition.js";
import Orientation from "../../types/Orientation.js";

type DragOverResult = {
targetReference: HTMLElement | null;
placement: any;
}

/**
* Drags an element over a list of items and returns the target reference and the placement of the dragged element.
*
* @param e {DragEvent} The drag event.
* @param component {UI5Element} The component, which contains the items.
* @param items {Array<HTMLElement>} The items, which the dragged element is dragged over.
* @returns {DragOverResult} The target reference and the placement of the dragged element.
*/
function dragOver(e: DragEvent, component: UI5Element, items: Array<HTMLElement>): DragOverResult {
const dragOverResult: DragOverResult = {
targetReference: null,
placement: null,
};

const draggedElement = DragRegistry.getDraggedElement();

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

const closestPosition = findClosestPosition(
items,
e.clientY,
Orientation.Vertical,
);
Comment on lines +21 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

Although this code is the same for List and Table, it is not for the Tree and TabContainer. Maybe put this back to the components and only reuse the code below?


if (closestPosition) {
const placements = closestPosition.placements;
dragOverResult.targetReference = e.target;

const placementAccepted = placements.some(placement => {
const beforeItemMovePrevented = !component.fireEvent("move-over", {
originalEvent: e,
source: {
element: draggedElement,
},
destination: {
element: closestPosition.element,
placement,
},
}, true);

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

return false;
});

if (!placementAccepted) {
dragOverResult.targetReference = null;
}
}

return dragOverResult;
}

function dropRow(e: DragEvent, component: UI5Element, target: HTMLElement, placement: `${MovePlacement}`): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thumbs up for making this reusable. It will foster proper maintenance of the DragRegistry and can be directly reused by Tree and TabContainer.

e.preventDefault();
const draggedElement = DragRegistry.getDraggedElement();

if (!draggedElement) {
return;
}

component.fireEvent("move", {
originalEvent: e,
source: {
element: draggedElement,
},
destination: {
element: target,
placement,
},
});

draggedElement?.focus();
}

export {
DragOverResult,
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems unnecessary to export DragOverResult

dragOver,
dropRow,
};
77 changes: 11 additions & 66 deletions packages/main/src/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ import {
isTabPrevious,
isCtrl,
} from "@ui5/webcomponents-base/dist/Keys.js";
import { dragOver, dropRow } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRowUtil.js";
import DragRegistry 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 { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AriaLabelHelper.js";
import getNormalizedTarget from "@ui5/webcomponents-base/dist/util/getNormalizedTarget.js";
import getEffectiveScrollbarStyle from "@ui5/webcomponents-base/dist/util/getEffectiveScrollbarStyle.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import debounce from "@ui5/webcomponents-base/dist/util/debounce.js";
import isElementInView from "@ui5/webcomponents-base/dist/util/isElementInView.js";
import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js";
import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js";
import type MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js";
import ListSelectionMode from "./types/ListSelectionMode.js";
import ListGrowingMode from "./types/ListGrowingMode.js";
import type ListAccessibleRole from "./types/ListAccessibleRole.js";
Expand Down Expand Up @@ -1068,73 +1068,18 @@ class List extends UI5Element {
}

_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.fireEvent<ListMoveEventDetail>("move-over", {
originalEvent: e,
source: {
element: draggedElement,
},
destination: {
element: closestPosition.element,
placement,
},
}, true);

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

return false;
});

if (!placementAccepted) {
this.dropIndicatorDOM!.targetReference = null;
}
const { targetReference, placement } = dragOver(e, this, this.items);
this.dropIndicatorDOM!.targetReference = targetReference;
this.dropIndicatorDOM!.placement = placement;
}

_ondrop(e: DragEvent) {
e.preventDefault();
const draggedElement = DragRegistry.getDraggedElement()!;

this.fireEvent<ListMoveEventDetail>("move", {
originalEvent: e,
source: {
element: draggedElement,
},
destination: {
element: this.dropIndicatorDOM!.targetReference!,
placement: this.dropIndicatorDOM!.placement,
},
});
if (!this.dropIndicatorDOM?.targetReference || !this.dropIndicatorDOM?.placement) {
return;
}

this.dropIndicatorDOM!.targetReference = null;
draggedElement.focus();
dropRow(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement);
this.dropIndicatorDOM.targetReference = null;
}

isForwardElement(element: HTMLElement) {
Expand Down
8 changes: 8 additions & 0 deletions packages/main/src/Table.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
style="{{styles.table}}"
aria-label="{{_ariaLabel}}"
aria-multiselectable="{{_ariaMultiSelectable}}"
@dragover="{{_ondragover}}"
@drop="{{_ondrop}}"
@dragleave="{{_ondragleave}}"
>
<slot name="headerRow"></slot>
<slot></slot>
Expand All @@ -25,6 +28,11 @@
</div>
{{/if}}

<ui5-drop-indicator
orientation="Horizontal"
.ownerReference="{{this}}"
></ui5-drop-indicator>

{{> tableEndRow}}
</div>

Expand Down
52 changes: 52 additions & 0 deletions packages/main/src/Table.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { findClosestPosition } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js";
import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js";
import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
Expand All @@ -10,6 +13,7 @@ import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delega
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import type MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js";
import TableTemplate from "./generated/templates/TableTemplate.lit.js";
import TableStyles from "./generated/themes/Table.css.js";
import TableRow from "./TableRow.js";
Expand All @@ -19,12 +23,14 @@ import TableExtension from "./TableExtension.js";
import type TableSelection from "./TableSelection.js";
import TableOverflowMode from "./types/TableOverflowMode.js";
import TableNavigation from "./TableNavigation.js";
import DropIndicator from "./DropIndicator.js";
import {
TABLE_NO_DATA,
} from "./generated/i18n/i18n-defaults.js";
import BusyIndicator from "./BusyIndicator.js";
import TableCell from "./TableCell.js";
import { findVerticalScrollContainer, scrollElementIntoView, isFeature } from "./TableUtils.js";
import { dragOver, dropRow } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRowUtil.js";

/**
* Interface for components that can be slotted inside the <code>features</code> slot of the <code>ui5-table</code>.
Expand Down Expand Up @@ -72,6 +78,17 @@ type TableRowClickEventDetail = {
row: TableRow,
};

type TableMoveEventDetail = {
Copy link
Contributor

Choose a reason for hiding this comment

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

You should also describe the move and move-overs events using @event decorator

originalEvent: Event,
source: {
element: HTMLElement,
},
destination: {
element: HTMLElement,
placement: `${MovePlacement}`,
}
}

/**
* @class
*
Expand Down Expand Up @@ -164,6 +181,7 @@ type TableRowClickEventDetail = {
TableHeaderRow,
TableCell,
TableRow,
DropIndicator,
],
})

Expand Down Expand Up @@ -322,6 +340,7 @@ class Table extends UI5Element {
this._events.forEach(eventType => this.addEventListener(eventType, this._onEventBound));
this.features.forEach(feature => feature.onTableActivate(this));
this._tableNavigation = new TableNavigation(this);
DragRegistry.subscribe(this);
}

onExitDOM() {
Expand All @@ -330,6 +349,7 @@ class Table extends UI5Element {
if (this.overflowMode === TableOverflowMode.Popin) {
ResizeHandler.deregister(this, this._onResizeBound);
}
DragRegistry.unsubscribe(this);
}

onBeforeRendering(): void {
Expand Down Expand Up @@ -462,6 +482,33 @@ class Table extends UI5Element {
this.fireEvent<TableRowClickEventDetail>("row-click", { row });
}

_ondragenter(e: DragEvent) {
Copy link
Member Author

Choose a reason for hiding this comment

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

_ondragenter, _ondragover, _ondragleave are pretty much the same implementation as in List.ts.

So maybe there is a way to reuse the code in a Utility or something?

e.preventDefault();
}

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

this.dropIndicatorDOM!.targetReference = null;
}

_ondragover(e: DragEvent) {
const { targetReference, placement } = dragOver(e, this, this.rows);
this.dropIndicatorDOM!.targetReference = targetReference;
this.dropIndicatorDOM!.placement = placement;
}

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

dropRow(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement);
this.dropIndicatorDOM.targetReference = null;
}

get styles() {
const headerStyleMap = this.headerRow?.[0]?.cells?.reduce((headerStyles, headerCell) => {
if (headerCell.horizontalAlign !== undefined) {
Expand Down Expand Up @@ -563,6 +610,10 @@ class Table extends UI5Element {
get isTable() {
return true;
}

get dropIndicatorDOM(): DropIndicator | null {
return this.shadowRoot!.querySelector("[ui5-drop-indicator]");
}
}

Table.define();
Expand All @@ -573,4 +624,5 @@ export type {
ITableFeature,
ITableGrowing,
TableRowClickEventDetail,
TableMoveEventDetail,
};
14 changes: 14 additions & 0 deletions packages/main/src/TableRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ class TableRow extends TableRowBase {
@property({ type: Boolean })
navigated = false;

/**
* Defines whether the row is movable.
*
* @default false
* @public
*/
@property({ type: Boolean })
movable = false;
Copy link
Member Author

Choose a reason for hiding this comment

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

technically not required, as you could just simply set draggable="true" directly on the ui5-table-row as it is in the light DOM.

But I guess, this would make sense for consistency across the API

Copy link
Contributor

@dimovpetar dimovpetar Oct 8, 2024

Choose a reason for hiding this comment

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

Yes, please use movable for consistency. It is also more meaningful in conjunction with the:

  • move and move-over events
  • potential keyboard support from your side

Last but not least, it won't work like that if you put the attribute to the root element in the shadow DOM.


@property({ type: Boolean, noAttribute: true })
_renderNavigated = false;

Expand All @@ -89,6 +98,11 @@ class TableRow extends TableRowBase {
} else {
this.removeAttribute("aria-current");
}
if (this.movable) {
this.setAttribute("draggable", "true");
} else {
this.removeAttribute("draggable");
}
Comment on lines +101 to +105
Copy link
Contributor

Choose a reason for hiding this comment

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

Better do this in TableRow.hbs, to encapsulate this attribute in a shadow DOM.

}

async focus(focusOptions?: FocusOptions | undefined): Promise<void> {
Expand Down
Loading
Loading