diff --git a/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilModal_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilModal_spec.ts new file mode 100644 index 000000000000..f9c14d70c4ea --- /dev/null +++ b/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilModal_spec.ts @@ -0,0 +1,167 @@ +import { + agHelper, + anvilLayout, + locators, + propPane, +} from "../../../../support/Objects/ObjectsCore"; +import { ANVIL_EDITOR_TEST } from "../../../../support/Constants"; +import { anvilLocators } from "../../../../support/Pages/Anvil/Locators"; +import EditorNavigation, { + EntityType, +} from "../../../../support/Pages/EditorNavigation"; + +describe( + `${ANVIL_EDITOR_TEST}: Anvil tests for Modals`, + { tags: ["@tag.Anvil"] }, + () => { + before(() => { + // Cleanup the canvas before each test + agHelper.SelectAllWidgets(); + agHelper.PressDelete(); + }); + it("1. Verify opening a modal by clicking on a button", () => { + // drop a modal widget + anvilLayout.dnd.DragDropNewAnvilWidgetNVerify( + anvilLocators.WDSMODAL, + 10, + 10, + { + skipWidgetSearch: true, + dropTargetDetails: { + dropModal: true, + }, + }, + ); + // press escape and close modal + agHelper.PressEscape(); + // add a button + anvilLayout.dnd.DragDropNewAnvilWidgetNVerify( + anvilLocators.WDSBUTTON, + 10, + 10, + { + skipWidgetSearch: true, + }, + ); + propPane.EnterJSContext("onClick", "{{showModal(Modal1.name);}}"); + agHelper.GetNClick(locators._enterPreviewMode); + agHelper.GetNClick(anvilLocators.anvilWidgetNameSelector("Button1")); + agHelper.AssertElementExist( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + }); + it("2. Verify closing a modal using the close icon button", () => { + agHelper.GetNClick( + anvilLocators.anvilModalCloseIconButtonSelector("Modal1"), + ); + agHelper.AssertElementAbsence( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + }); + it("3. Verify closing a modal by clicking outside the modal area", () => { + // open modal + agHelper.GetNClick(anvilLocators.anvilWidgetNameSelector("Button1")); + agHelper.AssertElementExist( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + // click on overlay top position + agHelper.GetNClick( + anvilLocators.anvilModalOverlay, + 0, + false, + 500, + false, + false, + "top", + ); + agHelper.AssertElementAbsence( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + }); + it("4. Verify closing a modal using the ESC key", () => { + // open modal + agHelper.GetNClick(anvilLocators.anvilWidgetNameSelector("Button1")); + agHelper.AssertElementExist( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + // press escape + agHelper.PressEscape(); + agHelper.AssertElementAbsence( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + agHelper.GetNClick(locators._exitPreviewMode); + }); + it("5. verify onClose function of Modal", () => { + EditorNavigation.SelectEntityByName("Modal1", EntityType.Widget); + propPane.EnterJSContext("onClose", "{{showAlert('onCloseTest');}}"); + agHelper.GetNClick(locators._enterPreviewMode); + //close modal via footer close button + agHelper.GetNClick( + anvilLocators.anvilModalFooterCloseButtonSelector("Modal1"), + ); + //verify alert + agHelper.ValidateToastMessage("onCloseTest"); + agHelper.GetNClick(locators._exitPreviewMode); + }); + it("6. Verify onSubmit function on Modal", () => { + EditorNavigation.SelectEntityByName("Modal1", EntityType.Widget); + propPane.EnterJSContext("onSubmit", "{{showAlert('onSubmitTest');}}"); + agHelper.GetNClick(locators._enterPreviewMode); + //close modal via submit button + agHelper.GetNClick( + anvilLocators.anvilModalFooterSubmitButtonSelector("Modal1"), + ); + //verify alert + agHelper.ValidateToastMessage("onSubmitTest"); + agHelper.GetNClick(locators._exitPreviewMode); + }); + it("7. Verify DnD on Modal", () => { + EditorNavigation.SelectEntityByName("Modal1", EntityType.Widget); + // add a widget to modal + anvilLayout.dnd.DragDropNewAnvilWidgetNVerify( + anvilLocators.WDSBUTTON, + 10, + 10, + { + skipWidgetSearch: true, + dropTargetDetails: { + name: "Modal1", + }, + }, + ); + // verify newly added button + agHelper.AssertElementExist( + anvilLocators.anvilWidgetNameSelector("Button2"), + ); + }); + it("8. Verify different modal sizes", () => { + // select all widgets and delete + agHelper.PressEscape(); + agHelper.SelectAllWidgets(); + agHelper.PressDelete(); + // add a modal widget + anvilLayout.dnd.DragDropNewAnvilWidgetNVerify( + anvilLocators.WDSMODAL, + 10, + 10, + { + skipWidgetSearch: true, + dropTargetDetails: { + dropModal: true, + }, + }, + ); + agHelper + .GetElement(anvilLocators.anvilWidgetNameSelector("Modal1")) + .matchImageSnapshot("anvilModalMediumSize"); + propPane.SelectPropertiesDropDown("size", "Small"); + agHelper + .GetElement(anvilLocators.anvilWidgetNameSelector("Modal1")) + .matchImageSnapshot("anvilModalSmallSize"); + propPane.SelectPropertiesDropDown("size", "Large"); + agHelper + .GetElement(anvilLocators.anvilWidgetNameSelector("Modal1")) + .matchImageSnapshot("anvilModalLargeSize"); + }); + }, +); diff --git a/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalLargeSize.snap.png b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalLargeSize.snap.png new file mode 100644 index 000000000000..f91964ccb85a Binary files /dev/null and b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalLargeSize.snap.png differ diff --git a/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalMediumSize.snap.png b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalMediumSize.snap.png new file mode 100644 index 000000000000..af945bafb7f0 Binary files /dev/null and b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalMediumSize.snap.png differ diff --git a/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalSmallSize.snap.png b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalSmallSize.snap.png new file mode 100644 index 000000000000..487c58b37a2e Binary files /dev/null and b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalSmallSize.snap.png differ diff --git a/app/client/cypress/support/Pages/Anvil/AnvilDnDHelper.ts b/app/client/cypress/support/Pages/Anvil/AnvilDnDHelper.ts index 24576a632c69..3056294a1e98 100644 --- a/app/client/cypress/support/Pages/Anvil/AnvilDnDHelper.ts +++ b/app/client/cypress/support/Pages/Anvil/AnvilDnDHelper.ts @@ -11,6 +11,7 @@ import { anvilLocators } from "./Locators"; interface DropTargetDetails { id?: string; name?: string; + dropModal?: boolean; } interface DragDropWidgetOptions { @@ -26,11 +27,14 @@ export class AnvilDnDHelper { dropTarget?: DropTargetDetails, ) => { if (dropTarget) { + if (dropTarget.dropModal) { + return anvilLocators.anvilDetachedWidgetsDropArena; + } if (dropTarget.id) { return `#${dropTarget.id}`; } if (dropTarget.name) { - return `${getWidgetSelector(dropTarget.name.toLowerCase() as any)} ${ + return `${anvilLocators.anvilWidgetNameSelector(dropTarget.name)} ${ anvilLocators.anvilDnDListener }`; } @@ -64,7 +68,10 @@ export class AnvilDnDHelper { eventConstructor: "MouseEvent", force: true, }); - cy.get(this.locator._anvilDnDHighlight); + if (!options.dropTargetDetails?.dropModal) { + // no need to show highlight for modal drop + cy.get(this.locator._anvilDnDHighlight); + } cy.get(dropAreaSelector).first().trigger("mouseup", xPos, yPos, { eventConstructor: "MouseEvent", force: true, diff --git a/app/client/cypress/support/Pages/Anvil/AnvilLayout.ts b/app/client/cypress/support/Pages/Anvil/AnvilLayout.ts index 707ae72eb827..beb7358083a4 100644 --- a/app/client/cypress/support/Pages/Anvil/AnvilLayout.ts +++ b/app/client/cypress/support/Pages/Anvil/AnvilLayout.ts @@ -9,6 +9,11 @@ export class AnvilLayout { const widgetSelector = anvilLocators.anvilWidgetNameSelector(widgetName); cy.get(widgetSelector).should("not.exist"); } + + public verifyAnvilModalIsClosed(widgetName: string) { + this.verifyWidgetDoesNotExist(widgetName); + } + public verifyParentChildRelationship(parentName: string, childName: string) { const parentWidgetSelector = anvilLocators.anvilWidgetNameSelector(parentName); diff --git a/app/client/cypress/support/Pages/Anvil/Locators/index.ts b/app/client/cypress/support/Pages/Anvil/Locators/index.ts index 0195e6883b0b..b6d3e6b90844 100644 --- a/app/client/cypress/support/Pages/Anvil/Locators/index.ts +++ b/app/client/cypress/support/Pages/Anvil/Locators/index.ts @@ -9,12 +9,25 @@ const anvilWidgetBasedSelectors = { anvilWidgetNameSelector: (widgetName: string) => { return `[${AnvilDataAttributes.WIDGET_NAME}="${widgetName}"]`; }, + anvilModalOverlay: 'div[data-floating-ui-portal] > div[data-status="open"]', anvilSelectedWidget: `${anvilWidgetSelector}[data-selected=true]`, anvilWidgetTypeSelector: (widgetType: string) => { return `.t--widget-${widgetType}`; }, }; +const anvilModalWidgetSelectors = { + anvilModalCloseIconButtonSelector: (widgetName: string) => { + return `${anvilWidgetBasedSelectors.anvilWidgetNameSelector(widgetName)} > div > div > button[data-icon-button]`; + }, + anvilModalFooterCloseButtonSelector: (widgetName: string) => { + return `${anvilWidgetBasedSelectors.anvilWidgetNameSelector(widgetName)} > div > div:last-child > button[data-button]:first-child`; + }, + anvilModalFooterSubmitButtonSelector: (widgetName: string) => { + return `${anvilWidgetBasedSelectors.anvilWidgetNameSelector(widgetName)} > div > div:last-child > button[data-button]:last-child`; + }, +}; + // sections and zones based selectors const anvilSectionAndZonesBasedSelectors = { anvilZoneDistributionValue: "[data-testid=t--anvil-zone-distribution-value]", @@ -29,6 +42,8 @@ const anvilSectionAndZonesBasedSelectors = { // dnd based selectors const anvilDnDBasedSelectors = { anvilDnDListener: "[data-type=anvil-dnd-listener]", + anvilDetachedWidgetsDropArena: + "[data-testid=t--anvil-detached-widgets-drop-arena]", mainCanvasSelector: `#${getAnvilCanvasId(MAIN_CONTAINER_WIDGET_ID)}`, }; @@ -39,12 +54,14 @@ const anvilWidgetsLocators = { WDSINPUT: "wdsinputwidget", WDSSWITCH: "wdsswitchwidget", WDSCHECKBOX: "wdscheckboxwidget", + WDSMODAL: "wdsmodalwidget", SECTION: "sectionwidget", ZONE: "zonewidget", }; export const anvilLocators = { ...anvilWidgetBasedSelectors, + ...anvilModalWidgetSelectors, ...anvilWidgetsLocators, ...anvilSectionAndZonesBasedSelectors, ...anvilDnDBasedSelectors, diff --git a/app/client/packages/design-system/widgets/src/components/Modal/src/Modal.tsx b/app/client/packages/design-system/widgets/src/components/Modal/src/Modal.tsx index 7f6c000e728a..a3fdaee54b51 100644 --- a/app/client/packages/design-system/widgets/src/components/Modal/src/Modal.tsx +++ b/app/client/packages/design-system/widgets/src/components/Modal/src/Modal.tsx @@ -7,8 +7,8 @@ import clsx from "clsx"; export const Modal = (props: ModalProps) => { const { children, + dataAttributes = {}, overlayClassName, - size = "medium", triggerRef, ...rest } = props; @@ -17,7 +17,7 @@ export const Modal = (props: ModalProps) => { // don't forget to change the transition-duration CSS as well {children} diff --git a/app/client/packages/design-system/widgets/src/components/Modal/src/types.ts b/app/client/packages/design-system/widgets/src/components/Modal/src/types.ts index 1c5bb1abd77d..d83d30903c44 100644 --- a/app/client/packages/design-system/widgets/src/components/Modal/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Modal/src/types.ts @@ -3,7 +3,6 @@ import type { PopoverProps, } from "@design-system/headless"; import type { ReactNode } from "react"; -import type { SIZES } from "../../../shared"; export interface ModalProps extends Pick< @@ -16,10 +15,7 @@ export interface ModalProps | "dismissClickOutside" >, Pick { - /** Size of the Modal - * @default medium - */ - size?: keyof typeof SIZES; + dataAttributes?: Record; /** The children of the component. */ children: ReactNode; } diff --git a/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx b/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx index 0b0a560b4cce..259a108db717 100644 --- a/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx +++ b/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx @@ -51,10 +51,10 @@ export const ModalExamples = () => { }} /> @@ -121,10 +121,10 @@ export const ModalExamples = () => { diff --git a/app/client/packages/design-system/widgets/src/testing/ComplexForm.tsx b/app/client/packages/design-system/widgets/src/testing/ComplexForm.tsx index ee2ff6d668f3..906860533f5a 100644 --- a/app/client/packages/design-system/widgets/src/testing/ComplexForm.tsx +++ b/app/client/packages/design-system/widgets/src/testing/ComplexForm.tsx @@ -151,10 +151,10 @@ export const ComplexForm = () => { Ok diff --git a/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx b/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx index 8aca04abe879..ab66050c8cbb 100644 --- a/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx +++ b/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx @@ -15,6 +15,7 @@ import { Layers } from "constants/Layers"; import { noop } from "utils/AppsmithUtils"; import { convertFlexGrowToFlexBasis } from "../sectionSpaceDistributor/utils/spaceDistributionEditorUtils"; import styles from "./styles.module.css"; +import { AnvilDataAttributes } from "widgets/anvil/constants"; const anvilWidgetStyleProps: CSSProperties = { position: "relative", @@ -44,13 +45,13 @@ export const AnvilFlexComponent = forwardRef( onClick = noop, onClickCapture = noop, widgetId, + widgetName, widgetSize, widgetType, }: AnvilFlexComponentProps, ref: any, ) => { const _className = `${className} ${styles.anvilWidgetWrapper}`; - const widgetConfigProps = useMemo(() => { const widgetConfig: | (Partial & WidgetConfigProps & { type: string }) @@ -99,12 +100,15 @@ export const AnvilFlexComponent = forwardRef( } return data; }, [widgetConfigProps, widgetSize, flexGrow]); - + const testDataAttributes = { + [AnvilDataAttributes.WIDGET_NAME]: widgetName, + }; // Render the Anvil Flex Component using the Flex component from WDS return ( + { if (ref.current) { - ref.current.setAttribute(AnvilDataAttributes.WIDGET_NAME, widgetName); ref.current.setAttribute( AnvilDataAttributes.IS_SELECTED_WIDGET, isSelected ? "true" : "false", diff --git a/app/client/src/layoutSystems/anvil/viewer/canvas/AnvilDetachedWidgets.tsx b/app/client/src/layoutSystems/anvil/viewer/canvas/AnvilDetachedWidgets.tsx index 650cde35595e..09e5c1d138ff 100644 --- a/app/client/src/layoutSystems/anvil/viewer/canvas/AnvilDetachedWidgets.tsx +++ b/app/client/src/layoutSystems/anvil/viewer/canvas/AnvilDetachedWidgets.tsx @@ -1,6 +1,5 @@ import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; import { useDetachedChildren } from "layoutSystems/anvil/common/hooks/detachedWidgetHooks"; -import { getAnvilWidgetDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils"; import { renderChildWidget } from "layoutSystems/common/utils/canvasUtils"; import React from "react"; import { useSelector } from "react-redux"; @@ -15,9 +14,7 @@ export const AnvilDetachedWidgets = () => { {detachedChildren.map((child) => renderChildWidget({ childWidgetData: child as WidgetProps, - defaultWidgetProps: { - className: `${getAnvilWidgetDOMId(child.widgetId)}`, - }, + defaultWidgetProps: {}, noPad: false, // Adding these properties as the type insists on providing this // while it is not required for detached children diff --git a/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx b/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx index a5d8592d14e8..a6afacbfcc26 100644 --- a/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx +++ b/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx @@ -49,7 +49,7 @@ const NoResult = styled(NonIdealState)` svg { height: 52px; width: 144px; - } + } div { diff --git a/app/client/src/widgets/anvil/constants.ts b/app/client/src/widgets/anvil/constants.ts index 1edc90a76760..c60988dd4bf6 100644 --- a/app/client/src/widgets/anvil/constants.ts +++ b/app/client/src/widgets/anvil/constants.ts @@ -13,6 +13,7 @@ export enum Elevations { * The data attribute that will be used to identify the anvil widget name in the DOM. */ export const AnvilDataAttributes = { + MODAL_SIZE: "data-size", WIDGET_NAME: "data-widget-name", IS_SELECTED_WIDGET: "data-selected", }; diff --git a/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx index b42c6fc5f8fa..c69d4160556d 100644 --- a/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx @@ -24,6 +24,9 @@ import { call } from "redux-saga/effects"; import { pasteWidgetsIntoMainCanvas } from "layoutSystems/anvil/utils/paste/mainCanvasPasteUtils"; import { ModalLayoutProvider } from "layoutSystems/anvil/layoutComponents/ModalLayoutProvider"; import styles from "./styles.module.css"; +import { getAnvilWidgetDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils"; +import { widgetTypeClassname } from "widgets/WidgetUtils"; +import { AnvilDataAttributes } from "widgets/anvil/constants"; class WDSModalWidget extends BaseWidget { static type = "WDS_MODAL_WIDGET"; @@ -121,24 +124,35 @@ class WDSModalWidget extends BaseWidget { return this.props.isVisible; }; + getModalClassNames = () => { + const { disableWidgetInteraction, type, widgetId } = this.props; + return `${getAnvilWidgetDOMId(widgetId)} ${widgetTypeClassname(type)} ${ + disableWidgetInteraction ? styles.disableModalInteraction : "" + }`; + }; + getWidgetView() { const closeText = this.props.cancelButtonText || "Cancel"; const submitText = this.props.showSubmitButton ? this.props.submitButtonText || "Submit" : undefined; - const contentClassName = `${this.props.className} ${ - this.props.disableWidgetInteraction ? styles.disableModalInteraction : "" - }`; + const modalClassNames = `${this.getModalClassNames()}`; return ( this.setState({ isVisible: val })} - size={this.props.size} > {this.state.isVisible && ( - + {this.props.showHeader && (