diff --git a/packages/base/src/util/dragAndDrop/DragRegistry.ts b/packages/base/src/util/dragAndDrop/DragRegistry.ts index 721769c2e1a3..c3d1ab26352a 100644 --- a/packages/base/src/util/dragAndDrop/DragRegistry.ts +++ b/packages/base/src/util/dragAndDrop/DragRegistry.ts @@ -1,4 +1,3 @@ -import type UI5Element from "../../UI5Element.js"; import type MovePlacement from "../../types/MovePlacement.js"; import MultipleDragGhostCss from "../../generated/css/MultipleDragGhost.css.js"; @@ -10,55 +9,16 @@ import { const MIN_MULTI_DRAG_COUNT = 2; -let customDragElementPromise: Promise | null = null; let draggedElement: HTMLElement | null = null; -let globalHandlersAttached = false; -const subscribers = new Set(); -const selfManagedDragAreas = new Set(); -const ondragstart = (e: DragEvent) => { - if (!e.dataTransfer || !(e.target instanceof HTMLElement)) { - return; - } - - if (!selfManagedDragAreas.has(e.target)) { - draggedElement = e.target; - } - - handleMultipleDrag(e); -}; - -const handleMultipleDrag = async (e: DragEvent) => { - if (!customDragElementPromise || !e.dataTransfer) { - return; - } - const dragElement = await customDragElementPromise; - // Add to document body temporarily - document.body.appendChild(dragElement); - - e.dataTransfer.setDragImage(dragElement, 0, 0); - - // Clean up the temporary element after the drag operation starts - requestAnimationFrame(() => { - dragElement.remove(); - }); -}; - -const ondragend = () => { - draggedElement = null; - customDragElementPromise = null; +const setDraggedElement = (element: HTMLElement | null) => { + draggedElement = element; }; -const ondrop = () => { +const clearDraggedElement = () => { draggedElement = null; - customDragElementPromise = null; }; -const setDraggedElement = (element: HTMLElement | null) => { - draggedElement = element; -}; -type SetDraggedElementFunction = typeof setDraggedElement; - const getDraggedElement = () => { return draggedElement; }; @@ -86,58 +46,27 @@ const createDefaultMultiDragElement = async (count: number): Promise { +const startMultipleDrag = async (count: number, e: DragEvent) => { if (count < MIN_MULTI_DRAG_COUNT) { console.warn(`Cannot start multiple drag with count ${count}. Minimum is ${MIN_MULTI_DRAG_COUNT}.`); // eslint-disable-line return; } - customDragElementPromise = createDefaultMultiDragElement(count); -}; - -const attachGlobalHandlers = () => { - if (globalHandlersAttached) { + if (!e.dataTransfer) { return; } - document.body.addEventListener("dragstart", ondragstart); - document.body.addEventListener("dragend", ondragend); - document.body.addEventListener("drop", ondrop); - globalHandlersAttached = true; -}; - -const detachGlobalHandlers = () => { - document.body.removeEventListener("dragstart", ondragstart); - document.body.removeEventListener("dragend", ondragend); - document.body.removeEventListener("drop", ondrop); - globalHandlersAttached = false; -}; - -const subscribe = (subscriber: UI5Element) => { - subscribers.add(subscriber); - - if (!globalHandlersAttached) { - attachGlobalHandlers(); - } -}; + const customDragElement = await createDefaultMultiDragElement(count); -const unsubscribe = (subscriber: UI5Element) => { - subscribers.delete(subscriber); - - if (subscribers.size === 0 && globalHandlersAttached) { - detachGlobalHandlers(); - } -}; - -const addSelfManagedArea = (area: HTMLElement | ShadowRoot) => { - selfManagedDragAreas.add(area); + // Add to document body temporarily + document.body.appendChild(customDragElement); - return setDraggedElement; -}; + e.dataTransfer.setDragImage(customDragElement, 0, 0); -const removeSelfManagedArea = (area: HTMLElement | ShadowRoot) => { - selfManagedDragAreas.delete(area); + // Clean up the temporary element after the drag operation starts + requestAnimationFrame(() => { + customDragElement.remove(); + }); }; type DragAndDropSettings = { @@ -163,10 +92,8 @@ type MoveEventDetail = { }; const DragRegistry = { - subscribe, - unsubscribe, - addSelfManagedArea, - removeSelfManagedArea, + setDraggedElement, + clearDraggedElement, getDraggedElement, startMultipleDrag, }; @@ -175,8 +102,8 @@ export default DragRegistry; export { startMultipleDrag, }; + export type { - SetDraggedElementFunction, DragAndDropSettings, MoveEventDetail, }; diff --git a/packages/main/cypress/specs/List.cy.tsx b/packages/main/cypress/specs/List.cy.tsx index b349c98105a5..18b9fa8ba580 100644 --- a/packages/main/cypress/specs/List.cy.tsx +++ b/packages/main/cypress/specs/List.cy.tsx @@ -1777,7 +1777,6 @@ describe("List - Drag and Drop", () => {
- http://sap.com

Drag and drop

@@ -1981,38 +1980,6 @@ describe("List - Drag and Drop", () => { cy.get("[ui5-list]").first().find("[ui5-li]").should("have.length.at.least", 3); cy.get("[ui5-list]").eq(1).find("[ui5-li]").should("have.length.at.least", 2); }); - - it("Moving link to list that doesn't accept it", () => { - const dataTransfer = new DataTransfer(); - - cy.get("a[href='http://sap.com']") - .trigger("dragstart", { dataTransfer }); - - cy.get("[ui5-list]").first().find("[ui5-li]").first() - .trigger("dragover", { dataTransfer }) - .trigger("drop", { dataTransfer }); - - cy.get("a[href='http://sap.com']") - .trigger("dragend", { dataTransfer }); - - cy.get("[ui5-list]").first().find("[ui5-li]").should("have.length", 3); - }); - - it("Moving link to list that accepts it", () => { - const dataTransfer = new DataTransfer(); - - cy.get("a[href='http://sap.com']") - .trigger("dragstart", { dataTransfer }); - - cy.get("[ui5-list]").eq(1).find("[ui5-li]").eq(1) - .trigger("dragover", { dataTransfer }) - .trigger("drop", { dataTransfer }); - - cy.get("a[href='http://sap.com']") - .trigger("dragend", { dataTransfer }); - - cy.get("[ui5-list]").eq(1).find("[ui5-li]").should("have.length.at.least", 3); - }); }); describe("List keyboard drag and drop tests", () => { diff --git a/packages/main/cypress/specs/ListItemGroup.cy.tsx b/packages/main/cypress/specs/ListItemGroup.cy.tsx index 009c18e8dd9e..529145662751 100644 --- a/packages/main/cypress/specs/ListItemGroup.cy.tsx +++ b/packages/main/cypress/specs/ListItemGroup.cy.tsx @@ -7,7 +7,7 @@ describe("ListItemGroup Tests", () => { cy.mount(); cy.get("[ui5-li-group]").should("exist"); - + cy.get("[ui5-li-group]") .shadow() .find("ui5-li-group-header") @@ -95,7 +95,7 @@ describe("List drag and drop tests", () => { destination: { element: $target[0], placement } } }); - + const listElement = $target[0].closest("[ui5-li-group]"); if (listElement) { listElement.dispatchEvent(moveEvent); @@ -248,68 +248,6 @@ describe("List drag and drop tests", () => { cy.get("@list1").find("ui5-li").should("have.length", 4); cy.get("@list2").find("ui5-li").should("have.length", 2); }); - - it("Moving link to list that doesn't accept it", () => { - cy.mount( - - ); - - cy.get("[ui5-li-group]").eq(0).as("list1").should("exist"); - setupDragAndDrop("@list1", false); - - cy.get("@list1").then($list => { - $list[0].innerHTML = ` - 1. Bulgaria - 1. Germany - 1. Spain - `; - }); - - cy.get("@list1").find("ui5-li").should("have.length", 3); - cy.get("a").as("link").should("contain.text", "http://sap.com"); - cy.get("@list1").find("ui5-li").eq(0).as("first").should("contain.text", "1. Bulgaria"); - - dispatchMoveEvent("@link", "@first", "After"); - - cy.get("@list1").find("ui5-li").should("have.length", 3); - cy.get("a").should("exist").and("contain.text", "http://sap.com"); - }); - - it("Moving link to list that accepts it", () => { - cy.mount( - - ); - - cy.get("[ui5-li-group]").eq(0).as("list2").should("exist"); - setupDragAndDrop("@list2", true); - - cy.get("@list2").then($list => { - $list[0].innerHTML = ` - 2. Bulgaria - 2. Germany (Allows nesting) - 2. Spain - `; - }); - - cy.get("@list2").find("ui5-li").should("have.length", 3); - cy.get("a").as("link").should("contain.text", "http://sap.com"); - cy.get("@list2").find("ui5-li").eq(1).as("second").should("contain.text", "2. Germany (Allows nesting)"); - - dispatchMoveEvent("@link", "@second", "Before"); - - cy.get("@list2").children().should("have.length", 4); - cy.get("@list2").find("a").should("exist").and("contain.text", "http://sap.com"); - }); }); describe("Focus", () => { diff --git a/packages/main/cypress/specs/TabContainerDragAndDropShadowDom.cy.tsx b/packages/main/cypress/specs/TabContainerDragAndDropShadowDom.cy.tsx new file mode 100644 index 000000000000..6de62a805278 --- /dev/null +++ b/packages/main/cypress/specs/TabContainerDragAndDropShadowDom.cy.tsx @@ -0,0 +1,764 @@ +import TabContainer from "../../src/TabContainer.js"; +import type { TabContainerMoveEventDetail } from "../../src/TabContainer.js"; +import Tab from "../../src/Tab.js"; +import type { TabInOverflow, TabInStrip } from "../../src/Tab.js"; +import TabSeparator from "../../src/TabSeparator.js"; +import Button from "../../src/Button.js"; +import type MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; +import type ResponsivePopover from "../../src/ResponsivePopover.js"; + +const verifyMoveOverEvent = (sourceElementId: string, destinationPlacement: `${MovePlacement}`, destinationElementId: string) => { + cy.get>>("@handleMoveOverSpy") + .then((spy) => { + const event = spy.getCall(0).args[0]; + const { source, destination } = event.detail; + + expect(source.element.id).to.equal(sourceElementId); + expect(destination.element.id).to.equal(destinationElementId); + expect(destination.placement).to.equal(destinationPlacement); + }); +}; + +const verifyMoveEvent = (sourceElementId: string, destinationPlacement: `${MovePlacement}`, destinationElementId: string) => { + cy.get>>("@handleMoveSpy") + .then((spy) => { + const event = spy.getCall(0).args[0]; + const { source, destination } = event.detail; + + expect(source.element.id).to.equal(sourceElementId); + expect(destination.element.id).to.equal(destinationElementId); + expect(destination.placement).to.equal(destinationPlacement); + }); +}; + +const tabShouldBeFocusedInStrip = (tabId: string, tabContainerId: string) => { + cy.get(`@${tabContainerId}Shadow`) + .should("be.focused"); + + cy.get("#customElId") + .shadow() + .find(`#${tabId}`) + .should(($el) => { + const tabContainer = document.activeElement.shadowRoot.activeElement; + + expect(($el[0]).getDomRefInStrip()?.id).to.equal(tabContainer.shadowRoot.activeElement.id); + }); +}; + +const tabShouldBeFocusedInPopover = (id: string) => { + cy.focused() + .closest(".ui5-tab-overflow-item") + .should(($el) => { + expect($el).to.have.class("ui5-tab-overflow-item"); + expect(($el[0] as TabInOverflow).realTabReference.id).to.equal(id); + }); +}; + +describe("TabContainer Drag and Drop Generic Tests", () => { + beforeEach(() => { + const handlers = { + moveOver: (e: CustomEvent) => { + e.preventDefault(); + }, + move: (e: CustomEvent) => { + const { destination, source } = e.detail; + + switch (destination.placement) { + case "Before": + destination.element.before(source.element); + break; + case "After": + destination.element.after(source.element); + break; + case "On": + destination.element.prepend(source.element); + break; + } + + const newParent = source.element.parentElement; + + if (newParent.hasAttribute("ui5-tab")) { + source.element.slot = "items"; + } else { + source.element.slot = ""; + } + } + }; + + cy.spy(handlers, "moveOver").as("handleMoveOverSpy"); + cy.spy(handlers, "move").as("handleMoveSpy"); + + cy.mount( + <> +
+
+ + + + + + + + + + + + + + + + + + + + + + content + + + + + text + text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + + cy.get("#customElId").then($customEl => { + const iconTabBar = document.querySelector("#tabContainer"); + $customEl.get(0).attachShadow({ mode: 'open' }).prepend(iconTabBar); + }); + + cy.get("#customElId") + .shadow() + .find("#tabContainer") + .as("tabContainerShadow"); + + cy.get("@tabContainerShadow") + .should("have.attr", "media-range", "M"); + + cy.get("@tabContainerShadow") + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .should(($elements) => { + $elements.each((index, element) => { + expect(element).to.have.attr("tabindex"); + }); + }); + }); + + describe("Using Mouse", () => { + it("Moving first strip item 'After' second", () => { + cy.get("#customElId") + .shadow() + .find("#tabOne, #tabTwo") + .then(($el) => { + const firstItem = $el[0]; + const secondItem = $el[1]; + + cy.ui5TabContainerDragAndDrop(firstItem.getDomRefInStrip()!, "After", secondItem.getDomRefInStrip()!) + + verifyMoveOverEvent(firstItem.id, "After", secondItem.id); + verifyMoveEvent(firstItem.id, "After", secondItem.id); + tabShouldBeFocusedInStrip(firstItem.id, "tabContainer"); + }); + }); + + it("Moving first strip item 'After' last", () => { + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .then(($el) => { + const firstItem = $el[0]; + const lastItem = $el[$el.length - 1]; + + cy.ui5TabContainerDragAndDrop(firstItem, "After", lastItem); + + verifyMoveOverEvent(firstItem.realTabReference.id, "After", lastItem.realTabReference.id); + verifyMoveEvent(firstItem.realTabReference.id, "After", lastItem.realTabReference.id); + tabShouldBeFocusedInStrip(firstItem.realTabReference.id, "tabContainer"); + }); + }); + + it("Moving last strip item 'Before' last but one", () => { + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .then(($el) => { + const lastItem = $el[$el.length - 1]; + const lastButOneItem = $el[$el.length - 2]; + + cy.ui5TabContainerDragAndDrop(lastItem, "Before", lastButOneItem); + + verifyMoveOverEvent(lastItem.realTabReference.id, "Before", lastButOneItem.realTabReference.id); + verifyMoveEvent(lastItem.realTabReference.id, "Before", lastButOneItem.realTabReference.id); + tabShouldBeFocusedInStrip(lastItem.realTabReference.id, "tabContainer"); + }); + }); + + it("Moving last strip item 'Before' first", () => { + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .then(($el) => { + const firstItem = $el[0]; + const lastItem = $el[$el.length - 1]; + + cy.ui5TabContainerDragAndDrop(lastItem, "Before", firstItem); + + verifyMoveOverEvent(lastItem.realTabReference.id, "Before", firstItem.realTabReference.id); + verifyMoveEvent(lastItem.realTabReference.id, "Before", firstItem.realTabReference.id); + tabShouldBeFocusedInStrip(lastItem.realTabReference.id, "tabContainer"); + }); + }); + + it("Moving strip item 'On' another", () => { + cy.get("#customElId") + .shadow() + .find("#tabFour, #tabSix") + .then(($el) => { + const fifthItem = $el[0]; + const sixthItem = $el[1]; + + cy.ui5TabContainerDragAndDrop(fifthItem.getDomRefInStrip()!, "On", sixthItem.getDomRefInStrip()!) + + verifyMoveOverEvent(fifthItem.id, "On", sixthItem.id); + verifyMoveEvent(fifthItem.id, "On", sixthItem.id); + // tabShouldBeFocusedInStrip(sixthItem.id, "tabContainer"); // TODO: uncomment after focus issue is resolved + }); + }); + + it("Moving item 'After' another in end overflow popover", () => { + cy.get("@tabContainerShadow") + .ui5TabContainerOpenEndOverflow(); + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-container-responsive-popover [ui5-li-custom]") + .then(($el) => { + const firstPopoverItem = $el[0]; + const thirdPopoverItem = $el[2]; + + cy.ui5TabContainerDragAndDrop(firstPopoverItem, "After", thirdPopoverItem, "Vertical"); + + verifyMoveOverEvent(firstPopoverItem.realTabReference.id, "After", thirdPopoverItem.realTabReference.id); + verifyMoveEvent(firstPopoverItem.realTabReference.id, "After", thirdPopoverItem.realTabReference.id); + tabShouldBeFocusedInPopover(firstPopoverItem.realTabReference.id); + }); + }); + + it("Moving item 'Before' another in end overflow popover", () => { + cy.get("@tabContainerShadow") + .ui5TabContainerOpenEndOverflow(); + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-container-responsive-popover [ui5-li-custom]") + .then(($el) => { + const thirdPopoverItem = $el[2]; + const firstPopoverItem = $el[0]; + + cy.ui5TabContainerDragAndDrop(thirdPopoverItem, "Before", firstPopoverItem, "Vertical"); + + verifyMoveEvent(thirdPopoverItem.realTabReference.id, "Before", firstPopoverItem.realTabReference.id); + verifyMoveOverEvent(thirdPopoverItem.realTabReference.id, "Before", firstPopoverItem.realTabReference.id); + tabShouldBeFocusedInPopover(thirdPopoverItem.realTabReference.id); + }); + }); + + it("Moving item 'On' another in end overflow popover", () => { + cy.get("@tabContainerShadow") + .ui5TabContainerOpenEndOverflow(); + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-container-responsive-popover [ui5-li-custom]") + .then(($el) => { + const firstPopoverItem = $el[0]; + const fifthPopoverItem = $el[5]; + + cy.ui5TabContainerDragAndDrop(firstPopoverItem, "On", fifthPopoverItem, "Vertical"); + + verifyMoveEvent(firstPopoverItem.realTabReference.id, "On", fifthPopoverItem.realTabReference.id); + verifyMoveOverEvent(firstPopoverItem.realTabReference.id, "On", fifthPopoverItem.realTabReference.id); + tabShouldBeFocusedInPopover(firstPopoverItem.realTabReference.id); + }); + }); + }); + + describe("Using Keyboard", () => { + describe("Moving strip items", () => { + it("Moving strip items using arrow keys", () => { + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item") + .first() + .realClick(); + + tabShouldBeFocusedInStrip("tabOne", "tabContainer"); + cy.realPress(["ControlLeft", "ArrowRight"]); + + verifyMoveOverEvent("tabOne", "After", "tabTwo"); + verifyMoveEvent("tabOne", "After", "tabTwo"); + + cy.get("@tabContainerShadow") + .children().eq(0) + .should("have.id", "tabTwo") + + cy.get("@tabContainerShadow") + .children().eq(1) + .should("have.id", "tabOne"); + + cy.get("@handleMoveOverSpy") + .invoke("resetHistory"); + + cy.get("@handleMoveSpy") + .invoke("resetHistory"); + tabShouldBeFocusedInStrip("tabOne", "tabContainer"); + cy.realPress(["ControlLeft", "ArrowDown"]); + + verifyMoveOverEvent("tabOne", "After", "tabThree"); + verifyMoveEvent("tabOne", "After", "tabThree"); + + cy.get("@tabContainerShadow") + .children().eq(1) + .should("have.id", "tabThree") + + cy.get("@tabContainerShadow") + .children().eq(2) + .should("have.id", "tabOne"); + + cy.get("@handleMoveOverSpy") + .invoke("resetHistory"); + + cy.get("@handleMoveSpy") + .invoke("resetHistory"); + tabShouldBeFocusedInStrip("tabOne", "tabContainer"); + cy.realPress(["ControlLeft", "ArrowLeft"]); + + verifyMoveOverEvent("tabOne", "Before", "tabThree"); + verifyMoveEvent("tabOne", "Before", "tabThree"); + + cy.get("@tabContainerShadow") + .children().eq(1) + .should("have.id", "tabOne"); + + cy.get("@tabContainerShadow") + .children().eq(2) + .should("have.id", "tabThree"); + + cy.get("@handleMoveOverSpy") + .invoke("resetHistory"); + + cy.get("@handleMoveSpy") + .invoke("resetHistory"); + + cy.get("@handleMoveOverSpy") + .invoke("resetHistory"); + + cy.get("@handleMoveSpy") + .invoke("resetHistory"); + tabShouldBeFocusedInStrip("tabOne", "tabContainer"); + cy.realPress(["ControlLeft", "ArrowUp"]); + + verifyMoveOverEvent("tabOne", "Before", "tabTwo"); + verifyMoveEvent("tabOne", "Before", "tabTwo"); + + cy.get("@tabContainerShadow") + .children().eq(0) + .should("have.id", "tabOne") + .prev() + .should("not.exist"); + + cy.get("@tabContainerShadow") + .children().eq(1) + .should("have.id", "tabTwo"); + }); + + it.skip("Moving strip item beyond the end using 'Arrow Right'", () => { + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item") + .first() + .realClick() + + for (let i = 0; i < 20; i++) { + tabShouldBeFocusedInStrip("tabOne", "tabContainer"); + cy.realPress(["ControlLeft", "ArrowRight"]); + } + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .last() + .should($lastItem => { + expect($lastItem[0].realTabReference.id).to.equal("tabOne"); + }); + }); + + it.skip("Moving strip item beyond the beginning with 'Arrow Left'", () => { + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow]") + .last() + .as("lastTabInStrip"); + + cy.get("@lastTabInStrip") + .realClick(); + + cy.get("@lastTabInStrip") + .then(($lastTab) => { + const lastTabId = $lastTab[0].realTabReference.id; + + for (let i = 0; i < 20; i++) { + tabShouldBeFocusedInStrip(lastTabId, "tabContainer"); + cy.realPress(["ControlLeft", "ArrowLeft"]); + } + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .first() + .should($firstItem => { + expect($firstItem[0].realTabReference.id).to.equal(lastTabId); + }); + }); + }); + + it.skip("Moving strip item with 'End'", () => { + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .first() + .realClick(); + + tabShouldBeFocusedInStrip("tabOne", "tabContainer"); + + cy.realPress(["ControlLeft", "End"]); + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .last() + .should($lastItem => { + expect($lastItem[0].realTabReference.id).to.equal("tabOne"); + }); + }); + + it("Moving strip item with 'Home'", () => { + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .eq(-2) // get the item before the last to 'more' button appearance doesn't disturb the test + .as("lastTabInStrip"); + + cy.get("@lastTabInStrip") + .realClick(); + + cy.get("@lastTabInStrip") + .then(($lastTab) => { + const lastTabId = $lastTab[0].realTabReference.id; + + tabShouldBeFocusedInStrip(lastTabId, "tabContainer"); + + cy.realPress(["ControlLeft", "Home"]); + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .first() + .should($firstItem => { + expect($firstItem[0].realTabReference.id).to.equal(lastTabId); + }); + }); + }); + }); + + describe("Moving popover items", () => { + it("Moving sub items with arrow keys", () => { + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-strip-item:nth-child(3) [ui5-button]") + .realClick(); + + cy.get("@tabContainerShadow") + .shadow() + .find("[ui5-responsive-popover]") + .ui5PopoverOpened(); + + cy.get("#customElId") + .shadow() + .find("#tabThree1") + .prev() + .should("not.exist"); + + tabShouldBeFocusedInPopover("tabThree1"); + cy.realPress(["ControlLeft", "ArrowDown"]); + + cy.get("#customElId") + .shadow() + .find("#tabThree1") + .prev() + .should("have.id", "tabThree2"); + + tabShouldBeFocusedInPopover("tabThree1"); + cy.realPress(["ControlLeft", "ArrowUp"]); + + cy.get("#customElId") + .shadow() + .find("#tabThree1") + .prev() + .should("not.exist"); + + cy.realPress(["ArrowDown"]); + cy.realPress(["ArrowDown"]); + tabShouldBeFocusedInPopover("tabThree21"); + + cy.get("#customElId") + .shadow() + .find("#tabThree21") + .prev() + .should("not.exist"); + + cy.realPress(["ControlLeft", "ArrowDown"]); + + cy.get("#customElId") + .shadow() + .find("#tabThree21") + .prev() + .should("have.id", "tabThree22"); + + tabShouldBeFocusedInPopover("tabThree21"); + cy.realPress(["ControlLeft", "ArrowDown"]); + cy.get("#customElId") + .shadow() + .find("#tabThree21") + .prev() + .should("have.id", "tabThree22"); + + tabShouldBeFocusedInPopover("tabThree21"); + cy.realPress(["ControlLeft", "ArrowUp"]); + cy.get("#customElId") + .shadow() + .find("#tabThree21") + .prev() + .should("not.exist"); + }); + + it("Moving overflow item with arrow keys", () => { + cy.get("@tabContainerShadow") + .ui5TabContainerOpenEndOverflow(); + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-container-responsive-popover [ui5-li-custom]") + .then(($el) => { + const firstItemId = $el[0].realTabReference.id; + const secondItemId = $el[1].realTabReference.id; + + tabShouldBeFocusedInPopover(firstItemId); + cy.realPress(["ControlLeft", "ArrowDown"]); + + cy.get("#customElId") + .shadow() + .find(`#${firstItemId}`) + .prev() + .should("have.id", secondItemId); + + tabShouldBeFocusedInPopover(firstItemId); + cy.realPress(["ControlLeft", "ArrowUp"]); + + cy.get("#customElId") + .shadow() + .find(`#${firstItemId}`) + .next() + .should("have.id", secondItemId); + }); + }); + + it("Moving overflow item with 'End'", () => { + cy.get("@tabContainerShadow") + .ui5TabContainerOpenEndOverflow(); + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-container-responsive-popover [ui5-li-custom]") + .first() + .then(($firstItem) => { + const firstItemId = $firstItem[0].realTabReference.id; + + tabShouldBeFocusedInPopover(firstItemId); + + cy.realPress(["ControlLeft", "End"]); + + cy.get("@tabContainerShadow") + .children() + .last() + .should("have.id", firstItemId); + }); + }); + + it("Moving overflow item with 'Home'", () => { + cy.get("@tabContainerShadow") + .ui5TabContainerOpenEndOverflow(); + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-container-responsive-popover [ui5-li-custom]") + .last() + .then(($lastItem) => { + const lastItemId = $lastItem[0].realTabReference.id; + + cy.realPress("End"); + tabShouldBeFocusedInPopover(lastItemId); + + cy.realPress(["ControlLeft", "Home"]); + + cy.get("@tabContainerShadow") + .shadow() + .find(".ui5-tab-container-responsive-popover [ui5-li-custom]") + .first() + .should(($firstItem) => { + expect($firstItem[0].realTabReference.id).to.equal(lastItemId); + }); + }); + }); + }); + }); +}); + +describe("TabContainer Drag and Drop when There are Fixed Tabs", () => { + beforeEach(() => { + const handlers = { + moveOver: (e: CustomEvent) => { + if (!e.detail.destination.element.dataset.fixed) { + e.preventDefault(); + } + }, + move: (e: CustomEvent) => { + const { destination, source } = e.detail; + + switch (destination.placement) { + case "Before": + destination.element.before(source.element); + break; + case "After": + destination.element.after(source.element); + break; + case "On": + destination.element.prepend(source.element); + break; + } + + const newParent = source.element.parentElement; + + if (newParent.hasAttribute("ui5-tab")) { + source.element.slot = "items"; + } else { + source.element.slot = ""; + } + } + }; + + cy.spy(handlers, "moveOver").as("handleMoveOverSpy"); + cy.spy(handlers, "move").as("handleMoveSpy"); + + cy.mount( + + + + + + + + + + + + + + + + + ); + + cy.get("@tabContainerShadow") + .should("have.attr", "media-range", "M"); + + cy.get("@tabContainerShadow") + .find(".ui5-tab-strip-item:not([start-overflow]):not([end-overflow])") + .should(($elements) => { + $elements.each((index, element) => { + expect(element).to.have.attr("tabindex"); + }); + }); + }); + + it.skip("Moving strip item beyond fixed items with arrow keys", () => { + cy.get("#tabNine") + .then(($el) => { + return $el[0].getDomRefInStrip(); + }) + .realClick(); + + for (let i = 0; i < 20; i++) { + tabShouldBeFocusedInStrip("tabNine", "tabContainer"); + cy.realPress(["ControlLeft", "ArrowLeft"]); + } + + cy.get("#customElId") + .shadow() + .find("#tabNine") + .prev() + .should("have.id", "fixedItemsSeparator"); + }); + + it.skip("Moving strip item beyond fixed items with 'Home;", () => { + cy.get("#tabTen") + .then(($el) => { + return $el[0].getDomRefInStrip(); + }) + .realClick(); + + tabShouldBeFocusedInStrip("tabTen", "tabContainer"); + cy.realPress(["ControlLeft", "Home"]); + + verifyMoveEvent("tabTen", "Before", "tabFour"); + + cy.get("#customElId") + .shadow() + .find("#tabTen") + .prev() + .should("have.id", "fixedItemsSeparator"); + }); +}); \ No newline at end of file diff --git a/packages/main/src/List.ts b/packages/main/src/List.ts index 195c18227460..7e5d18ae2b38 100644 --- a/packages/main/src/List.ts +++ b/packages/main/src/List.ts @@ -22,7 +22,6 @@ import { isDown, isUp, } from "@ui5/webcomponents-base/dist/Keys.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 { findClosestPositionsByKey } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js"; @@ -599,7 +598,6 @@ class List extends UI5Element { onEnterDOM() { registerUI5Element(this, this._updateAssociatedLabelsTexts.bind(this)); - DragRegistry.subscribe(this); ResizeHandler.register(this.getDomRef()!, this._handleResizeCallback); } @@ -607,7 +605,6 @@ class List extends UI5Element { deregisterUI5Element(this); this.unobserveListEnd(); ResizeHandler.deregister(this.getDomRef()!, this._handleResizeCallback); - DragRegistry.unsubscribe(this); } onBeforeRendering() { diff --git a/packages/main/src/ListItem.ts b/packages/main/src/ListItem.ts index d56489ab4594..ee24d52adc80 100644 --- a/packages/main/src/ListItem.ts +++ b/packages/main/src/ListItem.ts @@ -13,6 +13,7 @@ import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; import "@ui5/webcomponents-icons/dist/decline.js"; import "@ui5/webcomponents-icons/dist/edit.js"; +import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; import Highlight from "./types/Highlight.js"; import ListItemType from "./types/ListItemType.js"; import ListSelectionMode from "./types/ListSelectionMode.js"; @@ -330,6 +331,7 @@ abstract class ListItem extends ListItemBase { } if (e.target === this._listItem) { + DragRegistry.setDraggedElement(this); this.setAttribute("data-moving", ""); e.dataTransfer.dropEffect = "move"; e.dataTransfer.effectAllowed = "move"; @@ -338,6 +340,7 @@ abstract class ListItem extends ListItemBase { _ondragend(e: DragEvent) { if (e.target === this._listItem) { + DragRegistry.clearDraggedElement(); this.removeAttribute("data-moving"); } } diff --git a/packages/main/src/ListItemGroup.ts b/packages/main/src/ListItemGroup.ts index 9ddfa567fb3d..de3c8b73e47e 100644 --- a/packages/main/src/ListItemGroup.ts +++ b/packages/main/src/ListItemGroup.ts @@ -4,7 +4,6 @@ import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; 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 DragAndDropHandler from "./delegate/DragAndDropHandler.js"; import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; import type DropIndicator from "./DropIndicator.js"; @@ -161,14 +160,6 @@ class ListItemGroup extends UI5Element { }); } - onEnterDOM() { - DragRegistry.subscribe(this); - } - - onExitDOM() { - DragRegistry.unsubscribe(this); - } - get groupHeaderItem() { return this.shadowRoot!.querySelector("[ui5-li-group-header]")!; } diff --git a/packages/main/src/Tab.ts b/packages/main/src/Tab.ts index 0b201bd53d9a..ad6e6f379ec3 100644 --- a/packages/main/src/Tab.ts +++ b/packages/main/src/Tab.ts @@ -38,6 +38,7 @@ import css from "./generated/themes/Tab.css.js"; import stripCss from "./generated/themes/TabInStrip.css.js"; import draggableElementStyles from "./generated/themes/DraggableElement.css.js"; import overflowCss from "./generated/themes/TabInOverflow.css.js"; +import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; const DESIGN_DESCRIPTIONS = { [SemanticColor.Positive]: TAB_ARIA_DESIGN_POSITIVE, @@ -490,12 +491,14 @@ class Tab extends UI5Element implements ITabbable, ITab { _ondragstart(e: DragEvent) { if (e.target instanceof HTMLElement) { + DragRegistry.setDraggedElement(this); e.target.setAttribute("data-moving", ""); } } _ondragend(e: DragEvent) { if (e.target instanceof HTMLElement) { + DragRegistry.clearDraggedElement(); e.target.removeAttribute("data-moving"); } } diff --git a/packages/main/src/TabContainer.ts b/packages/main/src/TabContainer.ts index c337944204e1..6f85434d683e 100644 --- a/packages/main/src/TabContainer.ts +++ b/packages/main/src/TabContainer.ts @@ -34,7 +34,6 @@ import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.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 type { SetDraggedElementFunction } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; import longDragOverHandler from "@ui5/webcomponents-base/dist/util/dragAndDrop/longDragOverHandler.js"; import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; import { @@ -354,7 +353,6 @@ class TabContainer extends UI5Element { responsivePopover?: ResponsivePopover; _hasScheduledPopoverOpen = false; _handleResizeBound: () => void; - _setDraggedElement?: SetDraggedElementFunction; static registerTabStyles(styles: string) { tabStyles.push(styles); @@ -433,8 +431,6 @@ class TabContainer extends UI5Element { onEnterDOM() { ResizeHandler.register(this._getHeader(), this._handleResizeBound); - DragRegistry.subscribe(this); - this._setDraggedElement = DragRegistry.addSelfManagedArea(this); if (isDesktop()) { this.setAttribute("desktop", ""); } @@ -442,9 +438,6 @@ class TabContainer extends UI5Element { onExitDOM() { ResizeHandler.deregister(this._getHeader(), this._handleResizeBound); - DragRegistry.unsubscribe(this); - DragRegistry.removeSelfManagedArea(this); - this._setDraggedElement = undefined; } _handleResize() { @@ -503,7 +496,7 @@ class TabContainer extends UI5Element { e.dataTransfer.dropEffect = "move"; e.dataTransfer.effectAllowed = "move"; - this._setDraggedElement!((e.target as TabInStrip).realTabReference); + DragRegistry.setDraggedElement((e.target as TabInStrip).realTabReference); } _onHeaderDragEnter(e: DragEvent) { @@ -716,7 +709,7 @@ class TabContainer extends UI5Element { _onPopoverListKeyDown(e: KeyboardEvent) { if (isCtrl(e)) { - this._setDraggedElement!((e.target as TabInOverflow).realTabReference); + DragRegistry.setDraggedElement((e.target as TabInOverflow).realTabReference); } } diff --git a/packages/main/src/Table.ts b/packages/main/src/Table.ts index e6fd5444d6ec..639178f8c095 100644 --- a/packages/main/src/Table.ts +++ b/packages/main/src/Table.ts @@ -404,7 +404,7 @@ class Table extends UI5Element { @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; - _events = ["keydown", "keyup", "click", "focusin", "focusout", "dragenter", "dragleave", "dragover", "drop"]; + _events = ["keydown", "keyup", "click", "focusin", "focusout", "dragstart", "dragenter", "dragleave", "dragover", "drop", "dragend"]; _onEventBound: (e: Event) => void; _onResizeBound: ResizeObserverCallback; _tableNavigation?: TableNavigation; diff --git a/packages/main/src/TableDragAndDrop.ts b/packages/main/src/TableDragAndDrop.ts index b25a21ad16bc..17bfe155e3a8 100644 --- a/packages/main/src/TableDragAndDrop.ts +++ b/packages/main/src/TableDragAndDrop.ts @@ -12,7 +12,14 @@ export default class TableDragAndDrop extends TableExtension { constructor(table: Table) { super(); this._table = table; - DragRegistry.subscribe(this._table); // TODO: Where unsubscribe? + } + + _ondragstart(e: DragEvent) { + DragRegistry.setDraggedElement(e.target as HTMLElement); + } + + _ondragend() { + DragRegistry.clearDraggedElement(); } _ondragenter(e: DragEvent) { diff --git a/packages/main/src/Tree.ts b/packages/main/src/Tree.ts index 9d9bed5e4f12..d4ece8b3ac05 100644 --- a/packages/main/src/Tree.ts +++ b/packages/main/src/Tree.ts @@ -2,7 +2,6 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; 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 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"; @@ -324,14 +323,6 @@ class Tree extends UI5Element { }); } - onEnterDOM() { - DragRegistry.subscribe(this); - } - - onExitDOM() { - DragRegistry.unsubscribe(this); - } - onBeforeRendering() { this._prepareTreeItems(); } diff --git a/packages/main/test/pages/ListDragAndDrop.html b/packages/main/test/pages/ListDragAndDrop.html index c3f1e2701f8c..c038f99ff314 100644 --- a/packages/main/test/pages/ListDragAndDrop.html +++ b/packages/main/test/pages/ListDragAndDrop.html @@ -13,7 +13,6 @@ - http://sap.com

Drag and drop

diff --git a/packages/main/test/pages/ListDragAndDropShadowDom.html b/packages/main/test/pages/ListDragAndDropShadowDom.html new file mode 100644 index 000000000000..2a86092becc2 --- /dev/null +++ b/packages/main/test/pages/ListDragAndDropShadowDom.html @@ -0,0 +1,139 @@ + + + + + + + List Drag and Drop + + + + + + + + + +
+

Drag and drop

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/packages/main/test/pages/ListItemGroupDragAndDrop.html b/packages/main/test/pages/ListItemGroupDragAndDrop.html index 0a434621dbed..7e07d5cc9071 100644 --- a/packages/main/test/pages/ListItemGroupDragAndDrop.html +++ b/packages/main/test/pages/ListItemGroupDragAndDrop.html @@ -13,7 +13,6 @@ - http://sap.com

Drag and drop

@@ -48,7 +47,7 @@

Drag and drop

console.log(`Moving "${source.element.id}" ${destination.placement.toLowerCase()} "${destination.element.id}"`); }; - + const list1HandleMoveOver = (e) => { const { destination, source } = e.detail; diff --git a/packages/main/test/pages/MultipleDragDemo.html b/packages/main/test/pages/MultipleDragDemo.html index f85c44d17324..f40ce31e8daf 100644 --- a/packages/main/test/pages/MultipleDragDemo.html +++ b/packages/main/test/pages/MultipleDragDemo.html @@ -156,9 +156,60 @@

ui5-li (Direct)

} }); - // Initialize [0, 1].forEach(updateUI); + + function handleMoveOver(event) { + const { source, destination } = event.detail; + + // Allow drops from both lists + const sourceList = source.element.closest('ui5-list'); + if (lists.includes(sourceList)) { + // Allow reordering within lists + if (destination.placement === "Before" || + destination.placement === "After") { + event.preventDefault(); + } + } + } + + function handleMove(event) { + const { source, destination } = event.detail; + + // Get the source list to find all selected items + const sourceList = source.element.closest('ui5-list'); + const selectedItems = getSelectedItems(sourceList); + + // Determine which items to move: all selected items or just the dragged item + const itemsToMove = selectedItems.length > 1 && selectedItems.includes(source.element) + ? selectedItems + : [source.element]; + + // Move the items using spread operator + switch (destination.placement) { + case "Before": + destination.element.before(...itemsToMove); + break; + case "After": + destination.element.after(...itemsToMove); + break; + case "On": + destination.element.prepend(...itemsToMove); + break; + } + + // Update selection counts after move + setTimeout(() => { + [0, 1].forEach(updateUI); + }, 0); + } + + lists.forEach((list, index) => { + // setupSelectionChangeListener(index); + // list.addEventListener("dragstart", handleDragStart(index)); + list.addEventListener("ui5-move-over", handleMoveOver); + list.addEventListener("ui5-move", handleMove); + }); \ No newline at end of file diff --git a/packages/main/test/pages/TabContainerDragAndDropShadowDom.html b/packages/main/test/pages/TabContainerDragAndDropShadowDom.html new file mode 100644 index 000000000000..665f931003ce --- /dev/null +++ b/packages/main/test/pages/TabContainerDragAndDropShadowDom.html @@ -0,0 +1,211 @@ + + + + + + + Tab Container Drag and Drop + + + + + + + + + +
+

Max Nesting Level

+ +
+
+ +
+

Fixed Tabs

+
+ +
+
+ + + + + \ No newline at end of file