From cc5a21c9577dd703600e2edb98a5d8053f09623b Mon Sep 17 00:00:00 2001 From: Abhinav Jha Date: Thu, 23 May 2024 15:02:34 +0530 Subject: [PATCH] feat: Anvil: Interact with a focused widget's widget name component (#33646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - Add a ghost component (`AnvilWidgetNameComponentWrapper`) that prevents the widget underneath the widget name component from being focused - Select widget when dragging a focused widget from the widget name component - Adjust offsets and sizes of the ghost component and the widget name button - Remove `onMouseLeave` events from widgets and add an `onMouseLeave` event to the Canvas. - Change the pointer to `grab` for the widget name component Fixes #33385 ## Automation /ok-to-test tags="@tag.All" ### :mag: Cypress test results > [!CAUTION] > 🔴 🔴 🔴 Some tests have failed. > Workflow run: > Commit: 4373df84f255534a6eb839b1cad532ae327947ec > Cypress dashboard: Click here! > The following are new failures, please fix them before merging the PR:
    >
  1. cypress/e2e/Regression/ClientSide/BugTests/GitBugs_Spec.ts >
  2. cypress/e2e/Regression/ClientSide/PartialImportExport/PartialImport_spec.ts
> To know the list of identified flaky tests - Refer here ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No --- .../AnvilWidgetNameComponent.tsx | 38 +++++++--- .../editor/AnvilWidgetName/SplitButton.tsx | 75 ++++++++----------- .../anvil/editor/AnvilWidgetName/index.tsx | 16 +++- .../anvil/editor/AnvilWidgetName/utils.ts | 11 +-- .../anvil/editor/hooks/useAnvilWidgetHover.ts | 10 +-- app/client/src/pages/Editor/Canvas.tsx | 11 ++- app/client/src/selectors/widgetSelectors.ts | 4 +- 7 files changed, 85 insertions(+), 80 deletions(-) diff --git a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/AnvilWidgetNameComponent.tsx b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/AnvilWidgetNameComponent.tsx index 1ef4b05855..db005c914f 100644 --- a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/AnvilWidgetNameComponent.tsx +++ b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/AnvilWidgetNameComponent.tsx @@ -1,4 +1,4 @@ -import type { ForwardedRef } from "react"; +import type { CSSProperties, ForwardedRef } from "react"; import React, { forwardRef, useCallback, useMemo } from "react"; import { SelectionRequestType } from "sagas/WidgetSelectUtils"; import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; @@ -11,6 +11,22 @@ import { createMessage } from "@appsmith/constants/messages"; import { debugWidget } from "layoutSystems/anvil/integrations/actions"; import { useDispatch } from "react-redux"; +/** + * Floating UI doesn't seem to respect initial styles from styled components or modules + * So, we're passing the styles as a react prop + */ +const styles: CSSProperties = { + display: "inline-flex", + height: "32px", // This is 2px more than the ones in the designs. + width: "max-content", + position: "fixed", + top: 0, + left: 0, + visibility: "hidden", + isolation: "isolate", + background: "transparent", +}; + /** * * This component is responsible for rendering the widget name in the canvas. @@ -72,16 +88,16 @@ export function _AnvilWidgetNameComponent( }, [props.showError, handleDebugClick]); return ( - +
+ +
); } diff --git a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/SplitButton.tsx b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/SplitButton.tsx index 67e8ab1cba..dbbd6ce29c 100644 --- a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/SplitButton.tsx +++ b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/SplitButton.tsx @@ -1,20 +1,8 @@ -import type { ForwardedRef, CSSProperties } from "react"; -import React, { forwardRef } from "react"; +import React from "react"; import styled from "styled-components"; import { UpArrowSVG } from "./UpArrowIcon"; import { ErrorSVG } from "./ErrorIcon"; -const styles: CSSProperties = { - display: "inline-flex", - height: "24px", // This is 2px more than the ones in the designs. - width: "max-content", - position: "fixed", - top: 0, - left: 0, - visibility: "hidden", - isolation: "isolate", -}; - const SplitButtonWrapper = styled.div<{ $BGCSSVar: string; $ColorCSSVar: string; @@ -25,6 +13,11 @@ const SplitButtonWrapper = styled.div<{ color: var(${(props) => props.$ColorCSSVar}); fill: var(${(props) => props.$ColorCSSVar}); stroke: var(${(props) => props.$ColorCSSVar}); + margin-block-end: 8px; + + height: 24px; + width: max-content; + display: inline-flex; touch-action: manipulation; user-select: none; @@ -32,7 +25,7 @@ const SplitButtonWrapper = styled.div<{ gap: 1px; & button { - cursor: pointer; + cursor: grab; appearance: none; background: none; border: none; @@ -47,8 +40,9 @@ const SplitButtonWrapper = styled.div<{ font-size: inherit; font-weight: 500; - padding-block: 1.25ch; - padding-inline: 2ch; + padding-block: 3px; + padding-inline: 5px; + line-height: 17px; color: var(${(props) => props.$ColorCSSVar}); outline-color: var(${(props) => props.$BGCSSVar}); @@ -62,7 +56,8 @@ const SplitButtonWrapper = styled.div<{ } & span { - inline-size: 3ch; + inline-size: 2.4ch; + block-size: 100%; cursor: pointer; display: inline-flex; align-items: center; @@ -85,10 +80,10 @@ const SplitButtonWrapper = styled.div<{ &:active { filter: brightness(0.6); } - } - & > svg { - stroke: var(${(props) => props.$ColorCSSVar}); + & > svg { + stroke: var(${(props) => props.$ColorCSSVar}); + } } & span:nth-of-type(${(props) => (props.$isLeftToggleDisabled ? 1 : 2)}) { @@ -100,36 +95,28 @@ const SplitButtonWrapper = styled.div<{ } `; -export function _SplitButton( - props: { - text: string; +export function SplitButton(props: { + text: string; + onClick: React.MouseEventHandler; + bGCSSVar: string; + colorCSSVar: string; + leftToggle: { + disable: boolean; onClick: React.MouseEventHandler; - bGCSSVar: string; - colorCSSVar: string; - leftToggle: { - disable: boolean; - onClick: React.MouseEventHandler; - title: string; - }; - rightToggle: { - disable: boolean; - onClick: React.MouseEventHandler; - title: string; - }; - onDragStart: React.DragEventHandler; - }, - ref: ForwardedRef, -) { + title: string; + }; + rightToggle: { + disable: boolean; + onClick: React.MouseEventHandler; + title: string; + }; +}) { return ( {!props.leftToggle.disable && ( ); } - -export const SplitButton = forwardRef(_SplitButton); diff --git a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/index.tsx b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/index.tsx index 564c0a0761..0c74007b44 100644 --- a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/index.tsx +++ b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/index.tsx @@ -17,6 +17,9 @@ import { AnvilWidgetNameComponent } from "./AnvilWidgetNameComponent"; import { getWidgetErrorCount, shouldSelectOrFocus } from "./selectors"; import type { NameComponentStates } from "./types"; import { generateDragStateForAnvilLayout } from "layoutSystems/anvil/utils/widgetUtils"; +import { SelectionRequestType } from "sagas/WidgetSelectUtils"; +import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; +import { isWidgetSelected } from "selectors/widgetSelectors"; export function AnvilWidgetName(props: { widgetId: string; @@ -40,23 +43,28 @@ export function AnvilWidgetName(props: { (state) => getWidgetErrorCount(state, widgetId) > 0, ); + const isParentSelected = useSelector(isWidgetSelected(parentId)); + const styleProps = getWidgetNameComponentStyleProps( widgetType, nameComponentState, showError, + isParentSelected, ); const { setDraggingState } = useWidgetDragResize(); + const { selectWidget } = useWidgetSelection(); const onDragStart = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - if (nameComponentState === "select") { - setDraggingState(generateDragState()); - } + // If we're dragging a focused widget, we need to select it before dragging + // Otherwise, the currently selected widget will instead be dragged. + selectWidget(SelectionRequestType.One, [widgetId]); + setDraggingState(generateDragState()); }, - [setDraggingState, nameComponentState], + [setDraggingState], ); /** Setup Floating UI logic */ diff --git a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/utils.ts b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/utils.ts index 1382033bc5..af5ab71cde 100644 --- a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/utils.ts +++ b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/utils.ts @@ -72,7 +72,7 @@ export function handleWidgetUpdate( middleware: [ flip(), shift(), - offset({ mainAxis: 8, crossAxis: -5 }), + offset({ mainAxis: 0, crossAxis: -5 }), getOverflowMiddleware(widgetsEditorElement as HTMLDivElement), hide({ strategy: "referenceHidden" }), hide({ strategy: "escaped" }), @@ -103,6 +103,7 @@ export function getWidgetNameComponentStyleProps( widgetType: string, nameComponentState: NameComponentStates, showError: boolean, + isParentSelected: boolean, ) { const config = WidgetFactory.getConfig(widgetType); const onCanvasUI = config?.onCanvasUI || { @@ -121,11 +122,6 @@ export function getWidgetNameComponentStyleProps( ? onCanvasUI.focusColorCSSVar : onCanvasUI.selectionColorCSSVar; - let disableParentToggle = onCanvasUI.disableParentSelection; - if (nameComponentState === "focus") { - disableParentToggle = true; - } - // If there is an error, show the widget name in error state // This includes background being the error color // and font color being white. @@ -134,7 +130,8 @@ export function getWidgetNameComponentStyleProps( colorCSSVar = "--on-canvas-ui-white"; } return { - disableParentToggle, + // disable parent toggle if the parent is already selected + disableParentToggle: isParentSelected || onCanvasUI.disableParentSelection, bGCSSVar, colorCSSVar, selectionBGCSSVar: onCanvasUI.selectionBGCSSVar, diff --git a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts index d861147a50..27878ea596 100644 --- a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts +++ b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts @@ -44,26 +44,18 @@ export const useAnvilWidgetHover = ( ], ); - // 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]); + }, [handleMouseOver]); }; diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index 6c10a02356..8ecadb837f 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -1,8 +1,8 @@ import log from "loglevel"; -import React from "react"; +import React, { useCallback } from "react"; import styled from "styled-components"; import * as Sentry from "@sentry/react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import type { CanvasWidgetStructure } from "WidgetProvider/constants"; import useWidgetFocus from "utils/hooks/useWidgetFocus"; import { combinedPreviewModeSelector } from "selectors/editorSelectors"; @@ -19,6 +19,7 @@ import type { WidgetProps } from "widgets/BaseWidget"; import { getAppThemeSettings } from "@appsmith/selectors/applicationSelectors"; import CodeModeTooltip from "pages/Editor/WidgetsEditor/components/CodeModeTooltip"; import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors"; +import { focusWidget } from "actions/widgetActions"; interface CanvasProps { widgetsStructure: CanvasWidgetStructure; @@ -64,6 +65,11 @@ const Canvas = (props: CanvasProps) => { // so that fixedLayout theme does not break because of calculations done in useTheme const { theme } = useTheme(isAnvilLayout ? wdsThemeProps : {}); + const dispatch = useDispatch(); + const unfocusAllWidgets = useCallback(() => { + dispatch(focusWidget()); + }, [dispatch]); + /** * background for canvas */ @@ -93,6 +99,7 @@ const Canvas = (props: CanvasProps) => { )}`} data-testid={"t--canvas-artboard"} id={CANVAS_ART_BOARD} + onMouseLeave={unfocusAllWidgets} ref={isAnvilLayout ? undefined : focusRef} width={canvasWidth} > diff --git a/app/client/src/selectors/widgetSelectors.ts b/app/client/src/selectors/widgetSelectors.ts index e5b04e814f..902bbf0a37 100644 --- a/app/client/src/selectors/widgetSelectors.ts +++ b/app/client/src/selectors/widgetSelectors.ts @@ -113,9 +113,9 @@ export const getParentToOpenSelector = (widgetId: string) => { }; // Check if widget is in the list of selected widgets -export const isWidgetSelected = (widgetId: string) => { +export const isWidgetSelected = (widgetId?: string) => { return createSelector(getSelectedWidgets, (widgets): boolean => - widgets.includes(widgetId), + widgetId ? widgets.includes(widgetId) : false, ); };