diff --git a/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilAppNavigation_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilAppNavigation_spec.ts index e4edb08b8d..4ea0414f3c 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilAppNavigation_spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilAppNavigation_spec.ts @@ -70,8 +70,13 @@ describe( ); agHelper.AssertElementExist(appSettings.locators._sideNavbar); agHelper.GetNClick(locators._canvas); - agHelper.AssertElementExist(locators._widgetInCanvas(WIDGET.WDSINPUT)); - agHelper.AssertElementExist(locators._widgetInCanvas(WIDGET.WDSINPUT), 1); + agHelper.AssertElementExist( + locators._anvilWidgetInCanvas(WIDGET.WDSINPUT), + ); + agHelper.AssertElementExist( + locators._anvilWidgetInCanvas(WIDGET.WDSINPUT), + 1, + ); }); }, ); diff --git a/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilDnD_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilDnD_spec.ts index 0d7d1a1aaa..15bf42b203 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilDnD_spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilDnD_spec.ts @@ -64,7 +64,7 @@ describe( }, ); agHelper.AssertElementLength( - locators._widgetInCanvas(WIDGET.WDSBUTTON), + locators._anvilWidgetInCanvas(WIDGET.WDSBUTTON), 3, ); }); diff --git a/app/client/cypress/support/Objects/CommonLocators.ts b/app/client/cypress/support/Objects/CommonLocators.ts index 661cd054bd..2105806ad3 100644 --- a/app/client/cypress/support/Objects/CommonLocators.ts +++ b/app/client/cypress/support/Objects/CommonLocators.ts @@ -52,6 +52,7 @@ export class CommonLocators { _publishButton = ".t--application-publish-btn"; _widgetInCanvas = (widgetType: string) => `.t--draggable-${widgetType}`; _widgetInDeployed = (widgetType: string) => `.t--widget-${widgetType}`; + _anvilWidgetInCanvas = this._widgetInDeployed; _widgetInputSelector = (widgetType: string) => this._widgetInDeployed(widgetType) + " input"; _textWidgetInDeployed = this._widgetInDeployed("textwidget") + " span"; diff --git a/app/client/cypress/support/Pages/AnvilLayout.ts b/app/client/cypress/support/Pages/AnvilLayout.ts index c09ceb51f6..4039395136 100644 --- a/app/client/cypress/support/Pages/AnvilLayout.ts +++ b/app/client/cypress/support/Pages/AnvilLayout.ts @@ -114,7 +114,9 @@ export class AnvilLayout { ) { this.DragNDropAnvilWidget(widgetType, x, y, options); this.agHelper.AssertAutoSave(); //settling time for widget on canvas! - this.agHelper.AssertElementExist(this.locator._widgetInCanvas(widgetType)); + this.agHelper.AssertElementExist( + this.locator._anvilWidgetInCanvas(widgetType), + ); this.agHelper.Sleep(200); //waiting a bit for widget properties to open } } diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index adec1e97c5..1b1610126c 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -1684,7 +1684,7 @@ export class DataSources { force, ); this.agHelper.AssertElementVisibility( - this.locator._widgetInCanvas(WIDGET.WDSTABLE), + this.locator._anvilWidgetInCanvas(WIDGET.WDSTABLE), ); break; case Widgets.Chart: diff --git a/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx b/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx index 028e6b56a8..c6c16e91e2 100644 --- a/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx +++ b/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx @@ -1,34 +1,26 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import type { CSSProperties, MouseEventHandler } from "react"; +import React, { forwardRef, useEffect, useMemo, useState } from "react"; +import type { CSSProperties } from "react"; import { Flex } from "@design-system/widgets"; -import { useSelector } from "react-redux"; - -import { snipingModeSelector } from "selectors/editorSelectors"; -import { usePositionedContainerZIndex } from "utils/hooks/usePositionedContainerZIndex"; -import { - isCurrentWidgetFocused, - isWidgetSelected, -} from "selectors/widgetSelectors"; -import { widgetTypeClassname } from "widgets/WidgetUtils"; import { FlexVerticalAlignment, ResponsiveBehavior, } from "layoutSystems/common/utils/constants"; import type { FlexProps } from "@design-system/widgets/src/components/Flex/src/types"; -import { checkIsDropTarget } from "WidgetProvider/factory/helpers"; import type { AnvilFlexComponentProps } from "../utils/types"; import WidgetFactory from "WidgetProvider/factory"; import type { WidgetProps } from "widgets/BaseWidget"; import type { WidgetConfigProps } from "WidgetProvider/constants"; -import { usePositionObserver } from "layoutSystems/common/utils/LayoutElementPositionsObserver/usePositionObserver"; -import { useWidgetBorderStyles } from "./hooks/useWidgetBorderStyles"; import { getAnvilWidgetDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils"; -import type { AppState } from "@appsmith/reducers"; -import { SELECT_ANVIL_WIDGET_CUSTOM_EVENT } from "../utils/constants"; + +const anvilWidgetStyleProps: CSSProperties = { + position: "relative", + // overflow is set to make sure widgets internal components/divs don't overflow this boundary causing scrolls + overflow: "hidden", +}; /** - * Adds following functionalities to the widget: - * 1. Click handler to select the widget and open property pane. + * Adds the following functionalities to the widget: + * 1. Click handler to select the widget and open the property pane. * 2. Widget size based on responsiveBehavior: * 2a. Hug widgets will stick to the size provided to them. (flex: 0 0 auto;) * 2b. Fill widgets will automatically take up all available width in the parent container. (flex: 1 1 0%;) @@ -38,136 +30,71 @@ import { SELECT_ANVIL_WIDGET_CUSTOM_EVENT } from "../utils/constants"; * @param props | AnvilFlexComponentProps * @returns Widget */ +export const AnvilFlexComponent = forwardRef( + ( + { + children, + className, + flexGrow, + widgetId, + widgetSize, + widgetType, + }: AnvilFlexComponentProps, + ref: any, + ) => { + // State to manage whether the widget is a fill widget + const [isFillWidget, setIsFillWidget] = useState(false); -export function AnvilFlexComponent(props: AnvilFlexComponentProps) { - const isDropTarget = checkIsDropTarget(props.widgetType); - const isFocused = useSelector(isCurrentWidgetFocused(props.widgetId)); - const isSelected = useSelector(isWidgetSelected(props.widgetId)); - const isSnipingMode = useSelector(snipingModeSelector); - const isDragging = useSelector( - (state: AppState) => state.ui.widgetDragResize.isDragging, - ); + // State to manage vertical alignment of the widget + const [verticalAlignment, setVerticalAlignment] = + useState(FlexVerticalAlignment.Top); - /** POSITIONS OBSERVER LOGIC */ - // Create a ref so that this DOM node can be - // observed by the observer for changes in size - const ref = React.useRef(null); - usePositionObserver( - "widget", - { widgetId: props.widgetId, layoutId: props.layoutId }, - ref, - ); - /** EO POSITIONS OBSERVER LOGIC */ + // Effect to update state based on widget type + useEffect(() => { + const widgetConfig: + | (Partial & WidgetConfigProps & { type: string }) + | undefined = WidgetFactory.getConfig(widgetType); + if (!widgetConfig) return; + setIsFillWidget( + widgetConfig?.responsiveBehavior === ResponsiveBehavior.Fill, + ); + setVerticalAlignment( + widgetConfig?.flexVerticalAlignment || FlexVerticalAlignment.Top, + ); + }, [widgetType]); - const [isFillWidget, setIsFillWidget] = useState(false); - const [verticalAlignment, setVerticalAlignment] = - useState(FlexVerticalAlignment.Top); - - const onClickFn = useCallback( - function () { - if (ref.current && isFocused) { - ref.current.dispatchEvent( - new CustomEvent(SELECT_ANVIL_WIDGET_CUSTOM_EVENT, { - bubbles: true, - cancelable: true, - detail: { widgetId: props.widgetId }, - }), - ); + // Memoize flex props to be passed to the WDS Flex component. + // If the widget is being resized => update width and height to auto. + const flexProps: FlexProps = useMemo(() => { + const data: FlexProps = { + alignSelf: verticalAlignment || FlexVerticalAlignment.Top, + flexGrow: flexGrow ? flexGrow : isFillWidget ? 1 : 0, + flexShrink: isFillWidget ? 1 : 0, + flexBasis: isFillWidget ? "0%" : "auto", + padding: "spacing-1", + alignItems: "center", + }; + if (widgetSize) { + const { maxHeight, maxWidth, minHeight, minWidth } = widgetSize; + data.maxHeight = maxHeight; + data.maxWidth = maxWidth; + data.minHeight = minHeight ?? { base: "sizing-12" }; + data.minWidth = minWidth; } - }, - [props.widgetId, isFocused], - ); + return data; + }, [isFillWidget, widgetSize, verticalAlignment, flexGrow]); - const stopEventPropagation: MouseEventHandler = (e) => { - !isSnipingMode && e.stopPropagation(); - }; - - useEffect(() => { - const widgetConfig: - | (Partial & WidgetConfigProps & { type: string }) - | undefined = WidgetFactory.getConfig(props.widgetType); - if (!widgetConfig) return; - setIsFillWidget( - widgetConfig?.responsiveBehavior === ResponsiveBehavior.Fill, + // Render the Anvil Flex Component using the Flex component from WDS + return ( + +
{children}
+
); - setVerticalAlignment( - widgetConfig?.flexVerticalAlignment || FlexVerticalAlignment.Top, - ); - }, [props.widgetType]); - - const { onHoverZIndex } = usePositionedContainerZIndex( - isDropTarget, - props.widgetId, - isFocused, - isSelected, - ); - - const className = useMemo( - () => - `anvil-layout-parent-${props.parentId} anvil-layout-child-${ - props.widgetId - } ${widgetTypeClassname( - props.widgetType, - )} t--widget-${props.widgetName.toLowerCase()} drop-target-${ - props.layoutId - } row-index-${props.rowIndex} anvil-widget-wrapper`, - [ - props.parentId, - props.widgetId, - props.widgetType, - props.widgetName, - props.layoutId, - props.rowIndex, - ], - ); - - // Memoize flex props to be passed to the WDS Flex component. - // If the widget is being resized => update width and height to auto. - const flexProps: FlexProps = useMemo(() => { - const data: FlexProps = { - alignSelf: verticalAlignment || FlexVerticalAlignment.Top, - flexGrow: props.flexGrow ? props.flexGrow : isFillWidget ? 1 : 0, - flexShrink: isFillWidget ? 1 : 0, - flexBasis: isFillWidget ? "0%" : "auto", - padding: "spacing-1", - alignItems: "center", - }; - if (props.widgetSize) { - const { maxHeight, maxWidth, minHeight, minWidth } = props.widgetSize; - data.maxHeight = maxHeight; - data.maxWidth = maxWidth; - data.minHeight = minHeight ?? { base: "sizing-12" }; - data.minWidth = minWidth; - } - return data; - }, [isFillWidget, props.widgetSize, verticalAlignment, props.flexGrow]); - - const borderStyles = useWidgetBorderStyles(props.widgetId); - - const styleProps: CSSProperties = useMemo(() => { - return { - position: "relative", - // overflow is set to make sure widgets internal components/divs don't overflow this boundary causing scrolls - overflow: "hidden", - opacity: (isDragging && isSelected) || !props.isVisible ? 0.5 : 1, - "&:hover": { - zIndex: onHoverZIndex, - }, - ...borderStyles, - }; - }, [borderStyles, isDragging, isSelected, onHoverZIndex]); - - return ( - -
{props.children}
-
- ); -} + }, +); diff --git a/app/client/src/layoutSystems/anvil/editor/AnvilEditorFlexComponent.tsx b/app/client/src/layoutSystems/anvil/editor/AnvilEditorFlexComponent.tsx new file mode 100644 index 0000000000..076f2b1070 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/AnvilEditorFlexComponent.tsx @@ -0,0 +1,50 @@ +import React, { useMemo } from "react"; +import type { AnvilFlexComponentProps } from "../utils/types"; +import { AnvilFlexComponent } from "../common/AnvilFlexComponent"; +import { widgetTypeClassname } from "widgets/WidgetUtils"; +import { usePositionObserver } from "layoutSystems/common/utils/LayoutElementPositionsObserver/usePositionObserver"; +import { useAnvilWidgetStyles } from "./hooks/useAnvilWidgetStyles"; +import { useAnvilWidgetClick } from "./hooks/useAnvilWidgetClick"; +import { useAnvilWidgetDrag } from "./hooks/useAnvilWidgetDrag"; +import { useAnvilWidgetHover } from "./hooks/useAnvilWidgetHover"; + +export const AnvilEditorFlexComponent = (props: AnvilFlexComponentProps) => { + // Create a ref for the AnvilFlexComponent + const ref = React.useRef(null); + + // Generate a className for the AnvilFlexComponent + const className = useMemo( + () => + `anvil-layout-parent-${props.parentId} anvil-layout-child-${ + props.widgetId + } ${widgetTypeClassname( + props.widgetType, + )} t--widget-${props.widgetName.toLowerCase()} drop-target-${ + props.layoutId + } row-index-${props.rowIndex} anvil-widget-wrapper`, + [ + props.parentId, + props.widgetId, + props.widgetType, + props.widgetName, + props.layoutId, + props.rowIndex, + ], + ); + + // observe the layout element's position + usePositionObserver( + "widget", + { widgetId: props.widgetId, layoutId: props.layoutId }, + ref, + ); + + // Use custom hooks to manage styles, click, drag, and hover behavior exclusive for Edit mode + useAnvilWidgetStyles(props.widgetId, props.widgetName, props.isVisible, ref); + useAnvilWidgetClick(props.widgetId, ref); + useAnvilWidgetDrag(props.widgetId, props.layoutId, ref); + useAnvilWidgetHover(props.widgetId, ref); + + // Render the AnvilFlexComponent + return ; +}; diff --git a/app/client/src/layoutSystems/anvil/editor/AnvilEditorWidgetOnion.tsx b/app/client/src/layoutSystems/anvil/editor/AnvilEditorWidgetOnion.tsx index 96fa739e58..555e95d3cb 100644 --- a/app/client/src/layoutSystems/anvil/editor/AnvilEditorWidgetOnion.tsx +++ b/app/client/src/layoutSystems/anvil/editor/AnvilEditorWidgetOnion.tsx @@ -1,14 +1,12 @@ -import React, { useCallback } from "react"; +import React from "react"; import { useMemo } from "react"; import type { BaseWidgetProps } from "widgets/BaseWidgetHOC/withBaseWidgetHOC"; -import { AnvilFlexComponent } from "../common/AnvilFlexComponent"; import { AnvilWidgetComponent } from "../common/widgetComponent/AnvilWidgetComponent"; -import DraggableComponent from "layoutSystems/common/draggable/DraggableComponent"; -import { generateDragStateForAnvilLayout } from "../utils/widgetUtils"; import type { SizeConfig } from "WidgetProvider/constants"; import { getWidgetSizeConfiguration } from "../utils/widgetUtils"; import { useSelector } from "react-redux"; import { combinedPreviewModeSelector } from "selectors/editorSelectors"; +import { AnvilEditorFlexComponent } from "./AnvilEditorFlexComponent"; /** * AnvilEditorWidgetOnion @@ -26,22 +24,13 @@ import { combinedPreviewModeSelector } from "selectors/editorSelectors"; * @returns Enhanced Widget */ export const AnvilEditorWidgetOnion = (props: BaseWidgetProps) => { - const { layoutId } = props; - // if layoutId is not present on widget props then we need a selector to fetch layout id of a widget. - // const layoutId = useSelector(getLayoutIdByWidgetId(props.widgetId)); - const generateDragState = useCallback(() => { - return generateDragStateForAnvilLayout({ - layoutId, - }); - }, [layoutId]); const isPreviewMode = useSelector(combinedPreviewModeSelector); const widgetSize: SizeConfig = useMemo( () => getWidgetSizeConfiguration(props.type, props, isPreviewMode), [isPreviewMode, props.type], ); - return ( - { widgetSize={widgetSize} widgetType={props.type} > - - {props.children} - - + {props.children} + ); }; diff --git a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetClick.ts b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetClick.ts new file mode 100644 index 0000000000..1ce9b134a2 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetClick.ts @@ -0,0 +1,60 @@ +import { SELECT_ANVIL_WIDGET_CUSTOM_EVENT } from "layoutSystems/anvil/utils/constants"; +import { useCallback, useEffect } from "react"; +import { useSelector } from "react-redux"; +import { snipingModeSelector } from "selectors/editorSelectors"; +import { isCurrentWidgetFocused } from "selectors/widgetSelectors"; + +export const useAnvilWidgetClick = ( + widgetId: string, + ref: React.RefObject, // Ref object to reference the AnvilFlexComponent +) => { + // Retrieve state from the Redux store + const isFocused = useSelector(isCurrentWidgetFocused(widgetId)); + const isSnipingMode = useSelector(snipingModeSelector); + + // Function to stop event propagation if not in sniping mode + // Note: Sniping mode is irrelevant to the Anvil however it becomes relevant if we decide to make Anvil the default editor + const stopEventPropagation = (e: MouseEvent) => { + !isSnipingMode && e.stopPropagation(); + }; + + // Callback function for handling click events on AnvilFlexComponent in Edit mode + const onClickFn = useCallback( + function () { + // Dispatch a custom event when the Anvil widget is clicked and focused + if (ref.current && isFocused) { + ref.current.dispatchEvent( + new CustomEvent(SELECT_ANVIL_WIDGET_CUSTOM_EVENT, { + bubbles: true, + cancelable: true, + detail: { widgetId: widgetId }, + }), + ); + } + }, + [widgetId, isFocused], + ); + + // Effect hook to add and remove click event listeners + useEffect(() => { + if (ref.current) { + // Add click event listener to select the Anvil widget + ref.current.addEventListener("click", onClickFn, { capture: true }); + + // Add click event listener to stop event propagation in certain modes + ref.current.addEventListener("click", stopEventPropagation, { + capture: false, + }); + } + + // Clean up event listeners when the component unmounts + return () => { + if (ref.current) { + ref.current.removeEventListener("click", onClickFn, { capture: true }); + ref.current.removeEventListener("click", stopEventPropagation, { + capture: false, + }); + } + }; + }, [onClickFn, stopEventPropagation]); +}; diff --git a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetDrag.ts b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetDrag.ts new file mode 100644 index 0000000000..55bba48ed9 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetDrag.ts @@ -0,0 +1,77 @@ +import { generateDragStateForAnvilLayout } from "layoutSystems/anvil/utils/widgetUtils"; +import { useCallback, useEffect } from "react"; +import { useSelector } from "react-redux"; +import { SelectionRequestType } from "sagas/WidgetSelectUtils"; +import { getShouldAllowDrag } from "selectors/widgetDragSelectors"; +import { + isCurrentWidgetFocused, + isWidgetSelected, +} from "selectors/widgetSelectors"; +import { useWidgetDragResize } from "utils/hooks/dragResizeHooks"; +import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; + +export const useAnvilWidgetDrag = ( + widgetId: string, + layoutId: string, + ref: React.RefObject, // Ref object to reference the AnvilFlexComponent +) => { + // Retrieve state from the Redux store + const isSelected = useSelector(isWidgetSelected(widgetId)); + const isFocused = useSelector(isCurrentWidgetFocused(widgetId)); + const shouldAllowDrag = useSelector(getShouldAllowDrag); + const { selectWidget } = useWidgetSelection(); + const generateDragState = useCallback(() => { + return generateDragStateForAnvilLayout({ + layoutId, + }); + }, [layoutId]); + const { setDraggingState } = useWidgetDragResize(); + + // Callback function for handling drag start events + const onDragStart = useCallback( + (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (shouldAllowDrag && ref.current && !(e.metaKey || e.ctrlKey)) { + if (!isFocused) return; + + if (!isSelected) { + // Select the widget if not already selected + selectWidget(SelectionRequestType.One, [widgetId]); + } + + // Generate and set the dragging state for the Anvil layout + const draggingState = generateDragState(); + setDraggingState(draggingState); + } + }, + [ + shouldAllowDrag, + isFocused, + isSelected, + selectWidget, + widgetId, + generateDragState, + setDraggingState, + ], + ); + + // Effect hook to add and remove drag start event listeners + useEffect(() => { + if (ref.current) { + // Configure the draggable attribute and cursor style based on drag permission + ref.current.draggable = shouldAllowDrag; + ref.current.style.cursor = shouldAllowDrag ? "grab" : "default"; + + // Add drag start event listener + ref.current.addEventListener("dragstart", onDragStart); + } + + // Clean up event listeners when the component unmounts + return () => { + if (ref.current) { + ref.current.removeEventListener("dragstart", onDragStart); + } + }; + }, [onDragStart, shouldAllowDrag]); +}; diff --git a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts new file mode 100644 index 0000000000..268d4f92d1 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts @@ -0,0 +1,58 @@ +import { getAnvilSpaceDistributionStatus } from "layoutSystems/anvil/integrations/selectors"; +import { useCallback, useEffect } from "react"; +import { useSelector } from "react-redux"; +import { combinedPreviewModeSelector } from "selectors/editorSelectors"; +import { isCurrentWidgetFocused } from "selectors/widgetSelectors"; +import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; + +export const useAnvilWidgetHover = ( + widgetId: string, + ref: React.RefObject, // Ref object to reference the AnvilFlexComponent +) => { + // Retrieve state from the Redux store + const isFocused = useSelector(isCurrentWidgetFocused(widgetId)); + const isPreviewMode = useSelector(combinedPreviewModeSelector); + const isDistributingSpace = useSelector(getAnvilSpaceDistributionStatus); + + // Access the focusWidget function from the useWidgetSelection hook + const { focusWidget } = useWidgetSelection(); + + // Callback function for handling mouseover events + const handleMouseOver = useCallback( + (e: any) => { + // Check conditions before focusing the widget on mouseover + focusWidget && + !isFocused && + !isDistributingSpace && + !isPreviewMode && + focusWidget(widgetId); + + // Prevent the event from propagating further + e.stopPropagation(); + }, + [focusWidget, isFocused, isDistributingSpace, isPreviewMode, widgetId], + ); + + // Callback function for handling mouseleave events + const handleMouseLeave = useCallback(() => { + // On leaving a widget, reset the focused widget + focusWidget && focusWidget(); + }, [focusWidget]); + + // Effect hook to add and remove mouseover and mouseleave event listeners + useEffect(() => { + if (ref.current) { + // Add mouseover and mouseleave event listeners + ref.current.addEventListener("mouseover", handleMouseOver); + ref.current.addEventListener("mouseleave", handleMouseLeave); + } + + // Clean up event listeners when the component unmounts + return () => { + if (ref.current) { + ref.current.removeEventListener("mouseover", handleMouseOver); + ref.current.removeEventListener("mouseleave", handleMouseLeave); + } + }; + }, [handleMouseOver, handleMouseLeave]); +}; diff --git a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetStyles.ts b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetStyles.ts new file mode 100644 index 0000000000..aaa860bc8f --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetStyles.ts @@ -0,0 +1,53 @@ +import { useEffect, useMemo } from "react"; +import { isWidgetSelected } from "selectors/widgetSelectors"; +import { useSelector } from "react-redux"; +import { useWidgetBorderStyles } from "layoutSystems/anvil/common/hooks/useWidgetBorderStyles"; +import type { AppState } from "@appsmith/reducers"; + +export const useAnvilWidgetStyles = ( + widgetId: string, + widgetName: string, + isVisible = true, + ref: React.RefObject, // Ref object to reference the AnvilFlexComponent +) => { + // Get widget border styles using useWidgetBorderStyles + const widgetBorderStyles = useWidgetBorderStyles(widgetId); + + // Effect hook to apply widget border styles to the widget + useEffect(() => { + Object.entries(widgetBorderStyles).forEach(([property, value]) => { + if (ref.current) { + // Set each border style property on the widget's DOM element + ref.current.style[property as any] = value; + } + }); + }, [widgetBorderStyles]); + + // Effect hook to set a data attribute for testing purposes + useEffect(() => { + if (ref.current) { + ref.current.setAttribute("data-widgetname-cy", widgetName); + } + }, [widgetName]); + + // Selectors to determine whether the widget is selected or dragging + const isSelected = useSelector(isWidgetSelected(widgetId)); + const isDragging = useSelector( + (state: AppState) => state.ui.widgetDragResize.isDragging, + ); + + // Calculate whether the widget should fade based on dragging, selection, and visibility + const shouldFadeWidget = (isDragging && isSelected) || !isVisible; + + // Calculate opacity factor based on whether the widget should fade + const opacityFactor = useMemo(() => { + return shouldFadeWidget ? 0.5 : 1; + }, [shouldFadeWidget]); + + // Effect hook to set the opacity of the widget's DOM element + useEffect(() => { + if (ref.current) { + ref.current.style.opacity = opacityFactor.toString(); + } + }, [opacityFactor]); +}; diff --git a/app/client/src/layoutSystems/anvil/utils/types.ts b/app/client/src/layoutSystems/anvil/utils/types.ts index 501214c958..18b4604dfa 100644 --- a/app/client/src/layoutSystems/anvil/utils/types.ts +++ b/app/client/src/layoutSystems/anvil/utils/types.ts @@ -4,6 +4,7 @@ import type { WidgetType } from "WidgetProvider/factory"; export interface AnvilFlexComponentProps { children: ReactNode; + className?: string; isResizeDisabled?: boolean; layoutId: string; focused?: boolean; diff --git a/app/client/src/layoutSystems/withLayoutSystemWidgetHOC.test.tsx b/app/client/src/layoutSystems/withLayoutSystemWidgetHOC.test.tsx index 05e7cf8467..87a17ccfbf 100644 --- a/app/client/src/layoutSystems/withLayoutSystemWidgetHOC.test.tsx +++ b/app/client/src/layoutSystems/withLayoutSystemWidgetHOC.test.tsx @@ -251,9 +251,10 @@ describe("Layout System HOC's Tests", () => { .spyOn(layoutSystemSelectors, "getLayoutSystemType") .mockImplementation(() => LayoutSystemTypes.ANVIL); const component = render(); - const flexPositionedLayer = component.container.getElementsByClassName( - "anvil-layout-child-" + widgetProps.widgetId, - )[0]; + const flexPositionedLayer = + component.container.ownerDocument.getElementById( + "anvil_widget_" + widgetProps.widgetId, + ); expect(flexPositionedLayer).toBeTruthy(); }); }); diff --git a/app/client/src/sagas/ModalSagas.ts b/app/client/src/sagas/ModalSagas.ts index 540027f975..75c40def24 100644 --- a/app/client/src/sagas/ModalSagas.ts +++ b/app/client/src/sagas/ModalSagas.ts @@ -206,6 +206,7 @@ export function* closeModalSaga( // If modalName is not provided, find all open modals // Get all meta prop records const metaProps: Record = yield select(getWidgetsMeta); + const modalWidgetType: string = yield select(getModalWidgetType); // Get widgetIds of all widgets of type MODAL_WIDGET // Note: Not updating this code path for WDS_MODAL_WIDGET, as the functionality @@ -213,7 +214,7 @@ export function* closeModalSaga( // In this, the flow of switching back and forth between multiple modals is to be tested. const modalWidgetIds: string[] = yield select( getWidgetIdsByType, - WidgetTypes.MODAL_WIDGET, + modalWidgetType, ); // Loop through all modal widgetIds diff --git a/app/client/src/sagas/WidgetSelectionSagas.ts b/app/client/src/sagas/WidgetSelectionSagas.ts index 46b551a3ce..4484df8be8 100644 --- a/app/client/src/sagas/WidgetSelectionSagas.ts +++ b/app/client/src/sagas/WidgetSelectionSagas.ts @@ -249,7 +249,15 @@ function* handleWidgetSelectionSaga( } function* openOrCloseModalSaga(action: ReduxAction<{ widgetIds: string[] }>) { - if (action.payload.widgetIds.length !== 1) return; + const widgetsToSelect = action.payload.widgetIds; + if (widgetsToSelect.length !== 1) return; + if ( + widgetsToSelect.length === 1 && + widgetsToSelect[0] === MAIN_CONTAINER_WIDGET_ID + ) { + // for cases where a widget inside modal is deleted and main canvas gets selected post that. + return; + } // Let's assume that the payload widgetId is a modal widget and we need to open the modal as it is selected let modalWidgetToOpen: string = action.payload.widgetIds[0]; @@ -307,6 +315,12 @@ function* openOrCloseModalSaga(action: ReduxAction<{ widgetIds: string[] }>) { if (widgetIsModal || widgetIsChildOfModal) { yield put(showModal(modalWidgetToOpen)); } + if (!widgetIsModal && !widgetIsChildOfModal) { + yield put({ + type: ReduxActionTypes.CLOSE_MODAL, + payload: {}, + }); + } } function* focusOnWidgetSaga(action: ReduxAction<{ widgetIds: string[] }>) {