fix: Anvil Widgets not accessible when widget has no content. (#30780)

> Pull Request Template
>
> Use this template to quickly create a well written pull request.
Delete all quotes before creating the pull request.
>
## Description
In this PR, we are fixing a few issues and also destructuring
AnvilFlexComponent for edit and view.

Issues Fixed
- Widgets without any content like a text widget without text are not
hoverable
- Modal once opened does not close when other widgets on the canvas are
selected via the enitty explorer.

Anvil Flex Component was common component inspired from
PositionedContainer of Fixed layout. It had all features of Edit and
View together in one place. This mean viewer was unnecessarily
interpreting more code.
Now AnvilFlexComponent has been broken into AnvilFlexComponent and
AnvilEditorFlexComponent.
AnvilEditorFlexComponent is a wrapper around AnvilFlexComponent with
abilities needed for Edit Mode.

Another issue addressed in the PR is removal of DraggableComponennt,
which was just making dragging possible and providing a few styles like
fading the widget when it is being dragged.
With this PR all the above mentioned functions will be taken care of by
AnvilEditorFlexComponent.

#### PR fixes following issue(s)
Fixes #30734
> if no issue exists, please create an issue and ask the maintainers
about this first
>
>
#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
> Please delete options that are not relevant.
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- Chore (housekeeping or task changes that don't impact user perception)
- This change requires a documentation update
>
>
>
## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [ ] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Enhanced feature flag management with additional flags for better
control over application features.
- Introduced a new editor component for Anvil layout system, improving
layout and behavior management in edit mode.
- Added a custom hook for managing hover states on Anvil widgets,
enhancing user interaction.

- **Refactor**
- Updated AnvilFlexComponent to use `forwardRef` for better ref
management and optimized widget configuration and rendering logic.
- Modified selector logic to simplify the retrieval of layout system
type, enhancing code maintainability.
	- Adjusted test methodologies to improve reliability and accuracy.

- **Bug Fixes**
- Corrected assertions in Cypress end-to-end tests to accurately locate
and interact with widgets in the Anvil canvas, ensuring test
reliability.

- **Chores**
- Updated common locators and assertion methods in Cypress support files
for consistency and clarity in test scripts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ashok Kumar M 2024-02-05 18:00:50 +05:30 committed by GitHub
parent 4be051f143
commit c37d0c283f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 410 additions and 181 deletions

View File

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

View File

@ -64,7 +64,7 @@ describe(
},
);
agHelper.AssertElementLength(
locators._widgetInCanvas(WIDGET.WDSBUTTON),
locators._anvilWidgetInCanvas(WIDGET.WDSBUTTON),
3,
);
});

View File

@ -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";

View File

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

View File

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

View File

@ -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<boolean>(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>(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<HTMLDivElement>(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<WidgetProps> & 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<boolean>(false);
const [verticalAlignment, setVerticalAlignment] =
useState<FlexVerticalAlignment>(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<HTMLDivElement> = (e) => {
!isSnipingMode && e.stopPropagation();
};
useEffect(() => {
const widgetConfig:
| (Partial<WidgetProps> & 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 (
<Flex
{...flexProps}
className={className}
id={getAnvilWidgetDOMId(widgetId)}
ref={ref}
style={anvilWidgetStyleProps}
>
<div className="h-full w-full">{children}</div>
</Flex>
);
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 (
<Flex
{...flexProps}
className={className}
id={getAnvilWidgetDOMId(props.widgetId)}
onClick={stopEventPropagation}
onClickCapture={onClickFn}
ref={ref}
style={styleProps}
>
<div className="h-full w-full">{props.children}</div>
</Flex>
);
}
},
);

View File

@ -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<HTMLDivElement>(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 <AnvilFlexComponent {...props} className={className} ref={ref} />;
};

View File

@ -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 (
<AnvilFlexComponent
<AnvilEditorFlexComponent
flexGrow={props.flexGrow}
isResizeDisabled={props.resizeDisabled}
isVisible={!!props.isVisible}
@ -53,17 +42,7 @@ export const AnvilEditorWidgetOnion = (props: BaseWidgetProps) => {
widgetSize={widgetSize}
widgetType={props.type}
>
<DraggableComponent
dragDisabled={!!props.dragDisabled}
generateDragState={generateDragState}
isFlexChild
parentId={props.parentId}
resizeDisabled={props.resizeDisabled}
type={props.type}
widgetId={props.widgetId}
>
<AnvilWidgetComponent {...props}>{props.children}</AnvilWidgetComponent>
</DraggableComponent>
</AnvilFlexComponent>
<AnvilWidgetComponent {...props}>{props.children}</AnvilWidgetComponent>
</AnvilEditorFlexComponent>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import type { WidgetType } from "WidgetProvider/factory";
export interface AnvilFlexComponentProps {
children: ReactNode;
className?: string;
isResizeDisabled?: boolean;
layoutId: string;
focused?: boolean;

View File

@ -251,9 +251,10 @@ describe("Layout System HOC's Tests", () => {
.spyOn(layoutSystemSelectors, "getLayoutSystemType")
.mockImplementation(() => LayoutSystemTypes.ANVIL);
const component = render(<HOC {...widgetProps} />);
const flexPositionedLayer = component.container.getElementsByClassName(
"anvil-layout-child-" + widgetProps.widgetId,
)[0];
const flexPositionedLayer =
component.container.ownerDocument.getElementById(
"anvil_widget_" + widgetProps.widgetId,
);
expect(flexPositionedLayer).toBeTruthy();
});
});

View File

@ -206,6 +206,7 @@ export function* closeModalSaga(
// If modalName is not provided, find all open modals
// Get all meta prop records
const metaProps: Record<string, any> = 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

View File

@ -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[] }>) {