feat: enable DnD for entire modal widget(including header and footer). (#33580)

[![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"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/9171877023>
> Commit: 53925782d15c9998d993f39a0239bee64a7805fc
> Cypress dashboard url: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9171877023&attempt=1"
target="_blank">Click here!</a>

<!-- end of auto-generated comment: Cypress test results  -->






## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No
This commit is contained in:
Ashok Kumar M 2024-05-21 14:54:07 +05:30 committed by GitHub
parent 88111acba8
commit c707b63724
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 143 additions and 15 deletions

View File

@ -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 ? (
<AnvilErrorBoundary {...props}>{props.children}</AnvilErrorBoundary>
) : null;

View File

@ -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 (
<StyledModalEditorDropArenaWrapper isModalEmpty={isModalEmpty}>
<StyledModalEditorDropArenaWrapper
isModalEmpty={isModalEmpty}
style={{ height: "100%" }}
>
<StyledEmptyModalDropArena
isActive={isCurrentDraggedCanvas}
isModalEmpty={isModalEmpty}

View File

@ -16,6 +16,8 @@ import {
import { useWidgetDragResize } from "utils/hooks/dragResizeHooks";
import type { AnvilDnDListenerStates } from "./useAnvilDnDListenerStates";
import type { LayoutElementPositions } from "layoutSystems/common/types";
import type { AnvilDetachedWidgetsDnDDetail } from "../../hooks/useAnvilDetachedWidgetsDnD";
import { widgetHierarchy } from "layoutSystems/anvil/utils/constants";
export const useAnvilDnDEventCallbacks = ({
anvilDnDListenerRef,
@ -41,6 +43,7 @@ export const useAnvilDnDEventCallbacks = ({
activateOverlayWidgetDrop,
allowToDrop,
canActivate,
currentWidgetHierarchy,
draggedBlocks,
edgeCompensatorValues,
isCurrentDraggedCanvas,
@ -208,10 +211,26 @@ export const useAnvilDnDEventCallbacks = ({
);
const onMouseOut = useCallback(() => {
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<AnvilDetachedWidgetsDnDDetail>) => {
if (currentWidgetHierarchy === widgetHierarchy.WDS_MODAL_WIDGET) {
anvilDnDListenerRef.current?.dispatchEvent(
new MouseEvent("mousemove", e.detail.event),
);
}
}) as EventListener,
[currentWidgetHierarchy],
);
return {
onMouseMove,
onMouseMoveForDetachedWidgets,
onMouseOver,
onMouseOut,
onMouseUp,

View File

@ -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 {

View File

@ -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: {

View File

@ -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<HTMLDivElement | null>(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]);
};

View File

@ -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],
);

View File

@ -207,6 +207,11 @@ describe("Layout System HOC's Tests", () => {
isVisible: true,
detachFromLayout: true,
renderMode: RenderModes.CANVAS,
layout: [
{
layoutId: "modalLayoutId",
},
],
});
jest
.spyOn(editorSelectors, "getRenderMode")