feat: enable DnD for entire modal widget(including header and footer). (#33580)
[](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:
parent
88111acba8
commit
c707b63724
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
};
|
||||
|
|
@ -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],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -207,6 +207,11 @@ describe("Layout System HOC's Tests", () => {
|
|||
isVisible: true,
|
||||
detachFromLayout: true,
|
||||
renderMode: RenderModes.CANVAS,
|
||||
layout: [
|
||||
{
|
||||
layoutId: "modalLayoutId",
|
||||
},
|
||||
],
|
||||
});
|
||||
jest
|
||||
.spyOn(editorSelectors, "getRenderMode")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user