fix: fix modal position and styles (#30805)

## Description
Fixes for the modal widget:
1. Added support for clickOutside. The modal is closed only by clicking
on the backdrop overlay. A click on the widget name has been added to
the exceptions for close event.
2. Fixed the positioning of the modal. Now it is located in the center
of the provider.
3. For the correct positioning of the modal, it was necessary to make
fixes for canvas height. The fixes also affect fixed and autolayout.

#### PR fixes following issue(s)
Fixes #30788

Also fixed this
https://www.notion.so/appsmith/Canvas-gets-cut-off-on-preview-mode-525b95f26c6e4644bf5ab7389c02e434

#### Media


https://github.com/appsmithorg/appsmith/assets/11555074/6db9ecde-595b-4fa4-a21b-9ed08930d58f


#### Type of change
> Please delete options that are not relevant.
- Bug fix (non-breaking change which fixes an issue)
- Chore (housekeeping or task changes that don't impact user perception)
>
>
>
## 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
- [x] Manual
- [ ] JUnit
- [x] Jest
- [x] 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
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] 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

- **New Features**
- Popover and Modal components now support dismissing by clicking
outside. Custom logic and selectors can be specified to control this
behavior.
- **Enhancements**
	- Simplified logic for closing Modals by directly setting open state.
- Enhanced Modal styling options, allowing for better customization of
width and height.
	- ErrorBoundary component now supports custom styles.
- **Refactor**
- Removed redundant code and unused properties across various components
and layout systems.
- Simplified state management and styling adjustments in editor
components.
- **Style**
- Updated Modal component styles for improved layout and responsiveness.
- **Chores**
- Codebase cleanup including removal of unused classes, variables, and
adjustments for more efficient layout rendering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Valera Melnikov 2024-02-06 10:26:47 +03:00 committed by GitHub
parent fa2b44c646
commit f14b40cef6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 99 additions and 108 deletions

View File

@ -47,6 +47,10 @@ export interface PopoverProps {
triggerRef?: MutableRefObject<HTMLElement | null>;
/** Which element to initially focus. Can be either a number (tabbable index as specified by the order) or a ref. */
initialFocus?: number | MutableRefObject<HTMLElement | null>;
/** Determines whether clickOutside is work or not.
* @default false
*/
dismissClickOutside?: boolean;
}
export interface PopoverContentProps {

View File

@ -17,6 +17,7 @@ const DEFAULT_POPOVER_OFFSET = 10;
export function usePopover({
defaultOpen = false,
dismissClickOutside = false,
duration = 0,
initialFocus,
isOpen: controlledOpen,
@ -56,7 +57,17 @@ export function usePopover({
const click = useClick(context, {
enabled: controlledOpen == null,
});
const dismiss = useDismiss(context);
const dismiss = useDismiss(context, {
escapeKey: !dismissClickOutside,
outsidePress: (event) => {
if (dismissClickOutside) return false;
// By default, click to close popup only work inside the provider
return Boolean(
(event?.target as HTMLElement).closest("[data-theme-provider]"),
);
},
});
const role = useRole(context);
const interactions = useInteractions([click, dismiss, role]);

View File

@ -7,7 +7,7 @@ import type { ModalFooterProps } from "./types";
export const ModalFooter = (props: ModalFooterProps) => {
const { closeText = "Close", onSubmit, submitText = "Submit" } = props;
const { onClose, setOpen } = usePopoverContext();
const { setOpen } = usePopoverContext();
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async () => {
@ -19,14 +19,9 @@ export const ModalFooter = (props: ModalFooterProps) => {
}
};
const closeHandler = () => {
onClose && onClose();
setOpen(false);
};
return (
<Flex alignItems="center" gap="spacing-4" justifyContent="end">
<Button onPress={closeHandler} variant="ghost">
<Button onPress={() => setOpen(false)} variant="ghost">
{closeText}
</Button>

View File

@ -10,7 +10,7 @@ import type { ModalHeaderProps } from "./types";
export const ModalHeader = (props: ModalHeaderProps) => {
const { title } = props;
const { onClose, setLabelId, setOpen } = usePopoverContext();
const { setLabelId, setOpen } = usePopoverContext();
const id = useId();
// Only sets `aria-labelledby` on the Dialog root element
@ -20,17 +20,12 @@ export const ModalHeader = (props: ModalHeaderProps) => {
return () => setLabelId(undefined);
}, [id, setLabelId]);
const closeHandler = () => {
onClose && onClose();
setOpen(false);
};
return (
<Flex alignItems="center" gap="spacing-4" justifyContent="space-between">
<Text id={id} lineClamp={1} title={title} variant="caption">
{title}
</Text>
<IconButton icon="x" onPress={closeHandler} variant="ghost" />
<IconButton icon="x" onPress={() => setOpen(false)} variant="ghost" />
</Flex>
);
};

View File

@ -1,18 +1,18 @@
.overlay {
/* Redefining the overlay positioning so that the dialog is centered relative to the provider, not the viewport */
position: absolute !important;
background: var(--color-bg-neutral-opacity);
display: grid;
place-items: center;
z-index: var(--z-index-99);
max-width: var(--provider-width);
margin: 0 auto;
}
.content {
background: var(--color-bg);
border-radius: var(--border-radius-1);
box-shadow: var(--box-shadow-1);
max-width: calc(100vw - var(--outer-spacing-6));
max-height: calc(100vh - var(--outer-spacing-6));
max-width: calc(var(--provider-width) - var(--outer-spacing-8));
max-height: calc(var(--provider-width) - var(--outer-spacing-8));
outline: none;
display: flex;
flex-direction: column;
@ -38,7 +38,7 @@
}
[data-size="large"] .content {
width: calc(var(--provider-width) - var(--outer-spacing-6));
width: calc(var(--provider-width) - var(--outer-spacing-8));
height: 100%;
}

View File

@ -8,7 +8,12 @@ import type { SIZES } from "../../../shared";
export interface ModalProps
extends Pick<
PopoverProps,
"isOpen" | "setOpen" | "onClose" | "triggerRef" | "initialFocus"
| "isOpen"
| "setOpen"
| "onClose"
| "triggerRef"
| "initialFocus"
| "dismissClickOutside"
>,
Pick<PopoverModalContentProps, "overlayClassName"> {
/** Size of the Modal

View File

@ -1,11 +1,13 @@
import type { ReactNode } from "react";
import React from "react";
import styled from "styled-components";
import * as Sentry from "@sentry/react";
import * as log from "loglevel";
import type { ReactNode, CSSProperties } from "react";
interface Props {
children: ReactNode;
style?: CSSProperties;
}
interface State {
hasError: boolean;
@ -39,7 +41,10 @@ class ErrorBoundary extends React.Component<Props, State> {
render() {
return (
<ErrorBoundaryContainer className="error-boundary">
<ErrorBoundaryContainer
className="error-boundary"
style={this.props.style}
>
{this.state.hasError ? (
<p>
Oops, Something went wrong.

View File

@ -3,8 +3,3 @@
width: 100%;
height: 100%;
}
.main-anvil-canvas {
height: calc(100% - 1rem);
overflow-y: auto;
}

View File

@ -22,7 +22,8 @@ export const AnvilWidgetComponent = (props: BaseWidgetProps) => {
if (!detachFromLayout) return props.children;
return (
<ErrorBoundary>
// delete style as soon as we switch to Anvil layout completely
<ErrorBoundary style={{ height: "auto", width: "auto" }}>
<WidgetComponentBoundary widgetType={type}>
{props.children}
</WidgetComponentBoundary>

View File

@ -22,7 +22,6 @@ export function anvilDSLTransformer(dsl: DSLWidget) {
layoutStyle: {
border: "none",
height: "100%",
minHeight: "var(--canvas-height)",
padding: "spacing-1",
},
isDropTarget: true,

View File

@ -125,7 +125,6 @@ export function renderWidgetsInAlignedRow(
> = {
alignSelf: "stretch",
canvasId,
columnGap: "4px",
direction: "row",
flexBasis: { base: "auto", [`${MOBILE_BREAKPOINT}px`]: "0%" },
flexGrow: 1,

View File

@ -13,13 +13,12 @@ const CanvasResizerIcon = importSvg(
);
const AutoLayoutCanvasResizer = styled.div`
position: sticky;
position: relative;
z-index: var(--on-canvas-ui-z-index);
cursor: col-resize;
user-select: none;
-webkit-user-select: none;
width: 2px;
height: 100%;
display: flex;
background: var(--ads-v2-color-border);
align-items: center;
@ -59,12 +58,9 @@ const AutoLayoutCanvasResizer = styled.div`
export function MainContainerResizer({
currentPageId,
enableMainCanvasResizer,
heightWithTopMargin,
isPageInitiated,
isPreview,
shouldHaveTopMargin,
}: {
heightWithTopMargin: string;
isPageInitiated: boolean;
shouldHaveTopMargin: boolean;
isPreview: boolean;
@ -74,6 +70,7 @@ export function MainContainerResizer({
const appLayout = useSelector(getCurrentApplicationLayout);
const ref = useRef<HTMLDivElement>(null);
const dispatch = useDispatch();
const topHeaderHeight = "48px";
useEffect(() => {
const ele: HTMLElement | null = document.getElementById(CANVAS_VIEWPORT);
@ -174,8 +171,8 @@ export function MainContainerResizer({
}}
ref={ref}
style={{
top: "100%",
height: shouldHaveTopMargin ? heightWithTopMargin : "100vh",
top: isPreview ? topHeaderHeight : "0",
height: isPreview ? `calc(100% - ${topHeaderHeight})` : "100%",
}}
>
<div className="canvas-resizer-icon">

View File

@ -71,10 +71,7 @@ export function AppPage(props: AppPageProps) {
>
<PageView className="t--app-viewer-page" width={width}>
{props.widgetsStructure.widgetId &&
renderAppsmithCanvas({
...props.widgetsStructure,
classList: isAnvilLayout ? ["main-anvil-canvas"] : [],
} as WidgetProps)}
renderAppsmithCanvas(props.widgetsStructure as WidgetProps)}
</PageView>
</PageViewWrapper>
);

View File

@ -18,8 +18,6 @@ import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettings
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
import { renderAppsmithCanvas } from "layoutSystems/CanvasFactory";
import type { WidgetProps } from "widgets/BaseWidget";
import { LayoutSystemTypes } from "layoutSystems/types";
import { getLayoutSystemType } from "selectors/layoutSystemSelectors";
import { getAppThemeSettings } from "@appsmith/selectors/applicationSelectors";
interface CanvasProps {
@ -28,11 +26,17 @@ interface CanvasProps {
enableMainCanvasResizer?: boolean;
}
const StyledWDSThemeProvider = styled(WDSThemeProvider)`
min-height: 100%;
display: flex;
`;
const Wrapper = styled.section<{
background: string;
width: number;
$enableMainCanvasResizer: boolean;
}>`
flex: 1;
background: ${({ background }) => background};
width: ${({ $enableMainCanvasResizer, width }) =>
$enableMainCanvasResizer ? `100%` : `${width}px`};
@ -45,7 +49,6 @@ const Canvas = (props: CanvasProps) => {
);
const selectedTheme = useSelector(getSelectedAppTheme);
const isWDSEnabled = useFeatureFlag("ab_wds_enabled");
const layoutSystemType: LayoutSystemTypes = useSelector(getLayoutSystemType);
const themeSetting = useSelector(getAppThemeSettings);
const themeProps = {
@ -82,14 +85,12 @@ const Canvas = (props: CanvasProps) => {
: `mx-auto`;
const paddingBottomClass = props.enableMainCanvasResizer ? "" : "pb-52";
const height = layoutSystemType === LayoutSystemTypes.ANVIL ? "h-full" : "";
const renderChildren = () => {
return (
<Wrapper
$enableMainCanvasResizer={!!props.enableMainCanvasResizer}
background={isWDSEnabled ? "" : backgroundForCanvas}
className={`relative t--canvas-artboard ${height} ${paddingBottomClass} transition-all duration-400 ${marginHorizontalClass} ${getViewportClassName(
className={`relative t--canvas-artboard ${paddingBottomClass} transition-all duration-400 ${marginHorizontalClass} ${getViewportClassName(
canvasWidth,
)}`}
data-testid={"t--canvas-artboard"}
@ -98,13 +99,7 @@ const Canvas = (props: CanvasProps) => {
width={canvasWidth}
>
{props.widgetsStructure.widgetId &&
renderAppsmithCanvas({
...props.widgetsStructure,
classList:
layoutSystemType === LayoutSystemTypes.ANVIL
? ["main-anvil-canvas"]
: [],
} as WidgetProps)}
renderAppsmithCanvas(props.widgetsStructure as WidgetProps)}
</Wrapper>
);
};
@ -112,7 +107,9 @@ const Canvas = (props: CanvasProps) => {
try {
if (isWDSEnabled) {
return (
<WDSThemeProvider theme={theme}>{renderChildren()}</WDSThemeProvider>
<StyledWDSThemeProvider theme={theme}>
{renderChildren()}
</StyledWDSThemeProvider>
);
}

View File

@ -21,12 +21,10 @@ import {
getAppThemeIsChanging,
getSelectedAppTheme,
} from "selectors/appThemingSelectors";
import { getCurrentThemeDetails } from "selectors/themeSelectors";
import { getCanvasWidgetsStructure } from "@appsmith/selectors/entitiesSelector";
import {
AUTOLAYOUT_RESIZER_WIDTH_BUFFER,
useDynamicAppLayout,
} from "utils/hooks/useDynamicAppLayout";
import { useDynamicAppLayout } from "utils/hooks/useDynamicAppLayout";
import { LayoutSystemTypes } from "../../../layoutSystems/types";
import { getLayoutSystemType } from "../../../selectors/layoutSystemSelectors";
import Canvas from "../Canvas";
import type { AppState } from "@appsmith/reducers";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
@ -51,14 +49,8 @@ const Wrapper = styled.section<{
isPreviewingNavigation?: boolean;
isAppSettingsPaneWithNavigationTabOpen?: boolean;
navigationHeight?: number;
$heightWithTopMargin: string;
}>`
/* Create a custom variable that will allow us to measure the height of the canvas down the road */
--canvas-height: ${(props) => props.$heightWithTopMargin};
width: ${({ $enableMainCanvasResizer }) =>
$enableMainCanvasResizer
? `calc(100% - ${AUTOLAYOUT_RESIZER_WIDTH_BUFFER}px)`
: `100%`};
width: 100%;
position: relative;
overflow-x: auto;
overflow-y: auto;
@ -131,7 +123,6 @@ function MainContainerWrapper(props: MainCanvasWrapperProps) {
const isFetchingPage = useSelector(getIsFetchingPage);
const widgetsStructure = useSelector(getCanvasWidgetsStructure, equal);
const pages = useSelector(getViewModePageList);
const theme = useSelector(getCurrentThemeDetails);
const selectedTheme = useSelector(getSelectedAppTheme);
const shouldHaveTopMargin =
!(isPreviewMode || isProtectedMode) ||
@ -145,6 +136,9 @@ function MainContainerWrapper(props: MainCanvasWrapperProps) {
const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled");
const { canShowResizer, enableMainContainerResizer } =
useMainContainerResizer();
const layoutSystemType: LayoutSystemTypes = useSelector(getLayoutSystemType);
const isAnvilLayout = layoutSystemType === LayoutSystemTypes.ANVIL;
const headerHeight = "40px";
useEffect(() => {
return () => {
@ -181,31 +175,10 @@ function MainContainerWrapper(props: MainCanvasWrapperProps) {
const isPreviewingNavigation =
isPreviewMode || isProtectedMode || isAppSettingsPaneWithNavigationTabOpen;
/**
* calculating exact height to not allow scroll at this component,
* calculating total height of the canvas minus
* - 1. navigation height
* - 1.1 height for top + stacked or top + inline nav style is calculated
* - 1.2 in case of sidebar nav, height is 0
* - 2. top bar (header with preview/share/deploy buttons)
* - 3. bottom bar (footer with debug/logs buttons)
*/
const topMargin = shouldShowSnapShotBanner ? "4rem" : "0rem";
const bottomBarHeight =
isPreviewMode || isProtectedMode ? "0px" : theme.bottomBarHeight;
const smallHeaderHeight = showCanvasTopSection
? theme.smallHeaderHeight
: "0px";
const scrollBarHeight =
isPreviewMode || isProtectedMode || isPreviewingNavigation ? "8px" : "40px";
// calculating exact height to not allow scroll at this component,
// calculating total height minus margin on top, top bar and bottom bar and scrollbar height at the bottom
const heightWithTopMargin = `calc(100vh - 2rem - ${topMargin} - ${smallHeaderHeight} - ${bottomBarHeight} - ${scrollBarHeight} - ${navigationHeight}px)`;
return (
<>
<Wrapper
$enableMainCanvasResizer={enableMainContainerResizer}
$heightWithTopMargin={heightWithTopMargin}
background={
isPreviewMode ||
isProtectedMode ||
@ -226,7 +199,8 @@ function MainContainerWrapper(props: MainCanvasWrapperProps) {
shouldHaveTopMargin &&
!showCanvasTopSection &&
!isPreviewingNavigation &&
!showAnonymousDataPopup,
!showAnonymousDataPopup &&
!isAnvilLayout,
"mt-24": shouldShowSnapShotBanner,
})}
id={CANVAS_VIEWPORT}
@ -236,7 +210,7 @@ function MainContainerWrapper(props: MainCanvasWrapperProps) {
isPreviewingNavigation={isPreviewingNavigation}
navigationHeight={navigationHeight}
style={{
height: shouldHaveTopMargin ? heightWithTopMargin : "100vh",
height: isPreviewMode ? `calc(100% - ${headerHeight})` : "auto",
fontFamily: fontFamily,
pointerEvents: isAutoCanvasResizing ? "none" : "auto",
}}
@ -257,7 +231,6 @@ function MainContainerWrapper(props: MainCanvasWrapperProps) {
<MainContainerResizer
currentPageId={currentPageId}
enableMainCanvasResizer={enableMainContainerResizer && canShowResizer}
heightWithTopMargin={heightWithTopMargin}
isPageInitiated={!isPageInitializing && !!widgetsStructure}
isPreview={isPreviewMode || isProtectedMode}
shouldHaveTopMargin={shouldHaveTopMargin}

View File

@ -9,6 +9,9 @@ import {
getCurrentPageName,
previewModeSelector,
} from "selectors/editorSelectors";
import styled from "styled-components";
import { LayoutSystemTypes } from "../../../layoutSystems/types";
import { getLayoutSystemType } from "../../../selectors/layoutSystemSelectors";
import NavigationPreview from "./NavigationPreview";
import AnalyticsUtil from "utils/AnalyticsUtil";
import PerformanceTracker, {
@ -49,6 +52,10 @@ import {
import OverlayCanvasContainer from "layoutSystems/common/WidgetNamesCanvas";
import { protectedModeSelector } from "selectors/gitSyncSelectors";
const BannerWrapper = styled.div`
z-index: calc(var(--on-canvas-ui-z-index) + 1);
`;
function WidgetsEditor() {
const dispatch = useDispatch();
const currentPageId = useSelector(getCurrentPageId);
@ -87,6 +94,9 @@ function WidgetsEditor() {
LayoutSystemFeatures.ENABLE_CANVAS_OVERLAY_FOR_EDITOR_UI,
]);
const layoutSystemType: LayoutSystemTypes = useSelector(getLayoutSystemType);
const isAnvilLayout = layoutSystemType === LayoutSystemTypes.ANVIL;
useEffect(() => {
if (navigationPreviewRef?.current) {
const { offsetHeight } = navigationPreviewRef.current;
@ -176,7 +186,7 @@ function WidgetsEditor() {
PerformanceTracker.stopTracking();
return (
<EditorContextProvider renderMode="CANVAS">
<div className="relative flex flex-row w-full overflow-hidden">
<div className="relative flex flex-row h-full w-full overflow-hidden">
<div
className={classNames({
"relative flex flex-col w-full overflow-hidden": true,
@ -189,7 +199,7 @@ function WidgetsEditor() {
)}
<AnonymousDataPopup />
<div
className="relative flex flex-row w-full overflow-hidden"
className="relative flex flex-row h-full w-full overflow-hidden"
data-testid="widgets-editor"
draggable
id="widgets-editor"
@ -218,11 +228,19 @@ function WidgetsEditor() {
isPreview={isPreviewMode || isProtectedMode}
isPublished={isPublished}
sidebarWidth={isPreviewingNavigation ? sidebarWidth : 0}
style={
isAnvilLayout
? {
//This is necessary in order to place WDS modal with position: fixed; relatively to the canvas.
transform: "scale(1)",
}
: {}
}
>
{shouldShowSnapShotBanner && (
<div className="absolute top-0 z-1 w-full">
<BannerWrapper className="absolute top-0 w-full">
<SnapShotBannerCTA />
</div>
</BannerWrapper>
)}
<MainContainerWrapper
canvasWidth={canvasWidth}

View File

@ -24,15 +24,17 @@ import type {
import { call } from "redux-saga/effects";
import { pasteWidgetsIntoMainCanvas } from "layoutSystems/anvil/utils/paste/mainCanvasPasteUtils";
const modalBodyStyles: React.CSSProperties = {
minHeight: "var(--sizing-16)",
maxHeight:
"calc(var(--canvas-height) - var(--outer-spacing-4) - var(--outer-spacing-4) - var(--outer-spacing-4) - 100px)",
};
class WDSModalWidget extends BaseWidget<ModalWidgetProps, WidgetState> {
static type = "WDS_MODAL_WIDGET";
constructor(props: ModalWidgetProps) {
super(props);
this.state = {
isVisible: this.getModalVisibility(),
};
}
static getConfig() {
return config.metaConfig;
}
@ -117,16 +119,14 @@ class WDSModalWidget extends BaseWidget<ModalWidgetProps, WidgetState> {
return (
<Modal
isOpen={this.getModalVisibility()}
isOpen={this.state.isVisible as boolean}
onClose={this.onModalClose}
setOpen={(val) => this.setState({ isVisible: val })}
size={this.props.size}
>
<ModalContent className={this.props.className}>
{this.props.showHeader && <ModalHeader title={this.props.title} />}
<ModalBody
className={WDS_MODAL_WIDGET_CLASSNAME}
style={modalBodyStyles}
>
<ModalBody className={WDS_MODAL_WIDGET_CLASSNAME}>
<LayoutProvider {...this.props} />
</ModalBody>
{this.props.showFooter && (