From c707b63724264f2e472f88d9a8f5d3c4f6b8feaf Mon Sep 17 00:00:00 2001 From: Ashok Kumar M <35134347+marks0351@users.noreply.github.com> Date: Tue, 21 May 2024 14:54:07 +0530 Subject: [PATCH] feat: enable DnD for entire modal widget(including header and footer). (#33580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![workerB](https://img.shields.io/endpoint?url=https%3A%2F%2Fworkerb.linearb.io%2Fv2%2Fbadge%2Fprivate%2FU2FsdGVkX19Ze0CWc0MPiDhT4RTyUsPUbCSPCyn6Lc%2Fcollaboration.svg%3FcacheSeconds%3D60)](https://workerb.linearb.io/v2/badge/collaboration-page?magicLinkId=DEipBn-) ## Description In this PR, we are adding an enhancement to DnD of modal widgets. DnD was implemented only for layouts so in the case of modal, the header and footer are not layouts and DnD is not triggered when a widget is being dragged on top of these components of a modal. However for the user to be able to efficiently use the DnD of modal, mouse events of the modal are also needed to be processed. To enable this we have a new hook that wraps all detached widgets in editor mode only, `useAnvilDetachedWidgetsDnD` It makes sure mouse move events on the widget are dispatched to the dnd listeners via custom event `DETACHED_WIDGET_MOUSE_MOVE_EVENT` we aslo had to special handle mouse leave and enter utilities to activate a canvas for DnD, - for all widgets except modals these are handled on the `useAnvilDnDEventCallbacks` - for modal widgets these events are handled on the `useAnvilDetachedWidgetsDnD` Fixes #33318 _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.Anvil" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: 53925782d15c9998d993f39a0239bee64a7805fc > Cypress dashboard url: Click here! ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No --- .../editor/AnvilEditorDetachedWidgetOnion.tsx | 7 +- .../canvasArenas/AnvilModalDropArena.tsx | 6 +- .../hooks/useAnvilDnDEventCallbacks.ts | 23 +++++- .../canvasArenas/hooks/useAnvilDnDEvents.ts | 37 +++++++--- .../hooks/useAnvilDnDListenerStates.ts | 2 + .../hooks/useAnvilDetachedWidgetsDnD.ts | 71 +++++++++++++++++++ .../src/layoutSystems/common/selectors.ts | 7 ++ .../withLayoutSystemWidgetHOC.test.tsx | 5 ++ 8 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 app/client/src/layoutSystems/anvil/editor/hooks/useAnvilDetachedWidgetsDnD.ts diff --git a/app/client/src/layoutSystems/anvil/editor/AnvilEditorDetachedWidgetOnion.tsx b/app/client/src/layoutSystems/anvil/editor/AnvilEditorDetachedWidgetOnion.tsx index 0ba6030314..2ddab4da05 100644 --- a/app/client/src/layoutSystems/anvil/editor/AnvilEditorDetachedWidgetOnion.tsx +++ b/app/client/src/layoutSystems/anvil/editor/AnvilEditorDetachedWidgetOnion.tsx @@ -7,6 +7,7 @@ import { } from "layoutSystems/anvil/common/hooks/detachedWidgetHooks"; import { AnvilErrorBoundary } from "../common/widgetComponent/AnvilErrorBoundary"; import { SKELETON_WIDGET_TYPE } from "constants/WidgetConstants"; +import { useAnvilDetachedWidgetsDnD } from "./hooks/useAnvilDetachedWidgetsDnD"; /** * AnvilEditorDetachedWidgetOnion @@ -23,7 +24,11 @@ export const AnvilEditorDetachedWidgetOnion = (props: BaseWidgetProps) => { useObserveDetachedWidget(props.widgetId); useHandleDetachedWidgetSelect(props.widgetId); useAddBordersToDetachedWidgets(props.widgetId, props.type); - + useAnvilDetachedWidgetsDnD( + props.widgetId, + props.layout[0].layoutId, + !!props.isVisible, + ); return props.type !== SKELETON_WIDGET_TYPE ? ( {props.children} ) : null; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilModalDropArena.tsx b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilModalDropArena.tsx index 993c9106ed..62e2c1d5a2 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilModalDropArena.tsx +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilModalDropArena.tsx @@ -9,7 +9,6 @@ export const EMPTY_MODAL_PADDING = 4; const StyledModalEditorDropArenaWrapper = styled.div<{ isModalEmpty: boolean }>` position: relative; - height: 100% !important; ${(props) => props.isModalEmpty && ` @@ -64,7 +63,10 @@ export const AnvilModalDropArena = ({ const widget = useSelector(getWidgetByID(modalId)); const isModalEmpty = widget.children?.length === 0; return ( - + { - setDraggingCanvas(""); - }, [setDraggingCanvas]); + if (currentWidgetHierarchy !== widgetHierarchy.WDS_MODAL_WIDGET) { + // mouse out is handled by useAnvilDetachedWidgetsDnD for detached widgets(modal widgets) + setDraggingCanvas(""); + } + }, [setDraggingCanvas, currentWidgetHierarchy]); + + const onMouseMoveForDetachedWidgets = useCallback( + ((e: CustomEvent) => { + if (currentWidgetHierarchy === widgetHierarchy.WDS_MODAL_WIDGET) { + anvilDnDListenerRef.current?.dispatchEvent( + new MouseEvent("mousemove", e.detail.event), + ); + } + }) as EventListener, + [currentWidgetHierarchy], + ); + return { onMouseMove, + onMouseMoveForDetachedWidgets, onMouseOver, onMouseOut, onMouseUp, diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEvents.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEvents.ts index 5b215d4e9b..57d168c076 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEvents.ts +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEvents.ts @@ -5,6 +5,7 @@ import type { AnvilHighlightInfo } from "layoutSystems/anvil/utils/anvilTypes"; import { useAnvilDnDEventCallbacks } from "./useAnvilDnDEventCallbacks"; import { removeDisallowDroppingsUI } from "../utils/utils"; import { useCanvasDragToScroll } from "layoutSystems/common/canvasArenas/useCanvasDragToScroll"; +import { DETACHED_WIDGET_MOUSE_MOVE_EVENT } from "../../hooks/useAnvilDetachedWidgetsDnD"; /** * Hook to handle Anvil DnD events @@ -41,16 +42,23 @@ export const useAnvilDnDEvents = ( } } }, [isCurrentDraggedCanvas]); - const { onMouseMove, onMouseOut, onMouseOver, onMouseUp, resetCanvasState } = - useAnvilDnDEventCallbacks({ - anvilDragStates, - anvilDnDListenerRef, - canvasIsDragging, - deriveAllHighlightsFn, - layoutId, - onDrop, - setHighlightShown, - }); + const { + onMouseMove, + onMouseMoveForDetachedWidgets, + onMouseOut, + onMouseOver, + onMouseUp, + resetCanvasState, + } = useAnvilDnDEventCallbacks({ + anvilDragStates, + anvilDnDListenerRef, + canvasIsDragging, + deriveAllHighlightsFn, + layoutId, + onDrop, + setHighlightShown, + }); + useEffect(() => { if (anvilDnDListenerRef.current && isDragging) { // Initialize listeners @@ -70,6 +78,10 @@ export const useAnvilDnDEvents = ( ); // To make sure drops on the main canvas boundary buffer are processed in the capturing phase. document.addEventListener("mouseup", onMouseUp, true); + document.addEventListener( + DETACHED_WIDGET_MOUSE_MOVE_EVENT, + onMouseMoveForDetachedWidgets, + ); return () => { anvilDnDListenerRef.current?.removeEventListener( @@ -95,6 +107,10 @@ export const useAnvilDnDEvents = ( ); anvilDnDListenerRef.current?.removeEventListener("mouseup", onMouseUp); document.removeEventListener("mouseup", onMouseUp, true); + document.removeEventListener( + DETACHED_WIDGET_MOUSE_MOVE_EVENT, + onMouseMoveForDetachedWidgets, + ); }; } else { if (canvasIsDragging.current) { @@ -110,6 +126,7 @@ export const useAnvilDnDEvents = ( onMouseOver, onMouseUp, resetCanvasState, + onMouseMoveForDetachedWidgets, ]); return { diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts index 45ef1bf44d..7c11ef9d69 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts @@ -28,6 +28,7 @@ export interface AnvilDnDListenerStates { activateOverlayWidgetDrop: boolean; allowToDrop: boolean; canActivate: boolean; + currentWidgetHierarchy: number; draggedBlocks: DraggedWidget[]; dragDetails: DragDetails; isCurrentDraggedCanvas: boolean; @@ -167,6 +168,7 @@ export const useAnvilDnDListenerStates = ({ activateOverlayWidgetDrop, allowToDrop, canActivate, + currentWidgetHierarchy, draggedBlocks, dragDetails, dragMeta: { diff --git a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilDetachedWidgetsDnD.ts b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilDetachedWidgetsDnD.ts new file mode 100644 index 0000000000..ec6920806f --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilDetachedWidgetsDnD.ts @@ -0,0 +1,71 @@ +import { getPositionsByLayoutId } from "layoutSystems/common/selectors"; +import { getAnvilWidgetDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils"; +import { useCallback, useEffect, useRef } from "react"; +import { useSelector } from "react-redux"; +import { getIsDragging } from "selectors/widgetDragSelectors"; +import { useWidgetDragResize } from "utils/hooks/dragResizeHooks"; + +export const DETACHED_WIDGET_MOUSE_MOVE_EVENT = + "DETACHED_WIDGET_MOUSE_MOVE_EVENT"; + +export interface AnvilDetachedWidgetsDnDDetail { + event: MouseEvent; +} + +export const useAnvilDetachedWidgetsDnD = ( + widgetId: string, + layoutId: string, + isVisible: boolean, +) => { + const isDragging = useSelector(getIsDragging); + const layoutPositions = useSelector(getPositionsByLayoutId(layoutId)); + const widgetDomRef = useRef(null); + const { setDraggingCanvas } = useWidgetDragResize(); + const onMouseMove = useCallback( + (e: MouseEvent) => { + if (!isVisible || e.target !== widgetDomRef.current) { + return; + } + // simulate move on the top most edge of the layout + const detail: AnvilDetachedWidgetsDnDDetail = { + event: e, + }; + document.dispatchEvent( + new CustomEvent(DETACHED_WIDGET_MOUSE_MOVE_EVENT, { + detail, + }), + ); + }, + [isVisible], + ); + const onMouseOut = useCallback(() => { + if (isVisible) { + setDraggingCanvas(""); + } + }, [isVisible]); + useEffect(() => { + if (isDragging) { + const widgetClassName = `.${getAnvilWidgetDOMId(widgetId)}`; + const widgetDom = document.querySelector(widgetClassName); + if (widgetDom) { + widgetDomRef.current = widgetDom as HTMLDivElement; + } + } else { + widgetDomRef.current = null; + } + }, [isDragging]); + useEffect(() => { + if (isDragging && isVisible && layoutPositions && widgetDomRef.current) { + widgetDomRef.current.addEventListener("mousemove", onMouseMove); + widgetDomRef.current.addEventListener("mouseenter", onMouseMove); + widgetDomRef.current.addEventListener("mouseleave", onMouseOut); + } + return () => { + if (widgetDomRef.current) { + widgetDomRef.current.removeEventListener("mouseenter", onMouseMove); + widgetDomRef.current.removeEventListener("mousemove", onMouseMove); + widgetDomRef.current.removeEventListener("mouseleave", onMouseOut); + } + }; + }, [isDragging, isVisible, onMouseMove, layoutPositions]); +}; diff --git a/app/client/src/layoutSystems/common/selectors.ts b/app/client/src/layoutSystems/common/selectors.ts index 93e1717916..cc39ff93b4 100644 --- a/app/client/src/layoutSystems/common/selectors.ts +++ b/app/client/src/layoutSystems/common/selectors.ts @@ -1,4 +1,11 @@ import type { AppState } from "@appsmith/reducers"; +import { createSelector } from "reselect"; export const getLayoutElementPositions = (state: AppState) => state.entities.layoutElementPositions; + +export const getPositionsByLayoutId = (layoutId: string) => + createSelector( + getLayoutElementPositions, + (layoutElementPositions) => layoutElementPositions[layoutId], + ); diff --git a/app/client/src/layoutSystems/withLayoutSystemWidgetHOC.test.tsx b/app/client/src/layoutSystems/withLayoutSystemWidgetHOC.test.tsx index 87a17ccfbf..226191dae9 100644 --- a/app/client/src/layoutSystems/withLayoutSystemWidgetHOC.test.tsx +++ b/app/client/src/layoutSystems/withLayoutSystemWidgetHOC.test.tsx @@ -207,6 +207,11 @@ describe("Layout System HOC's Tests", () => { isVisible: true, detachFromLayout: true, renderMode: RenderModes.CANVAS, + layout: [ + { + layoutId: "modalLayoutId", + }, + ], }); jest .spyOn(editorSelectors, "getRenderMode")