feat: Anvil: Interact with a focused widget's widget name component (#33646)

## 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"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!CAUTION]
> 🔴 🔴 🔴 Some tests have failed.
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/9201483569>
> Commit: 4373df84f255534a6eb839b1cad532ae327947ec
> Cypress dashboard: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9201483569&attempt=2&selectiontype=test&testsstatus=failed&specsstatus=fail"
target="_blank"> Click here!</a>
> The following are new failures, please fix them before merging the PR:
<ol>
> <li>cypress/e2e/Regression/ClientSide/BugTests/GitBugs_Spec.ts
>
<li>cypress/e2e/Regression/ClientSide/PartialImportExport/PartialImport_spec.ts
</ol>
> To know the list of identified flaky tests - <a
href="https://internal.appsmith.com/app/cypress-dashboard/identified-flaky-tests-65890b3c81d7400d08fa9ee3?branch=master"
target="_blank">Refer here</a>

<!-- end of auto-generated comment: Cypress test results  -->











## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No
This commit is contained in:
Abhinav Jha 2024-05-23 15:02:34 +05:30 committed by GitHub
parent cee654a19a
commit cc5a21c957
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 85 additions and 80 deletions

View File

@ -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 (
<SplitButton
bGCSSVar={props.bGCSSVar}
colorCSSVar={props.colorCSSVar}
leftToggle={leftToggle}
onClick={handleSelectWidget}
onDragStart={props.onDragStart}
ref={ref}
rightToggle={rightToggle}
text={props.name}
/>
<div draggable onDragStart={props.onDragStart} ref={ref} style={styles}>
<SplitButton
bGCSSVar={props.bGCSSVar}
colorCSSVar={props.colorCSSVar}
leftToggle={leftToggle}
onClick={handleSelectWidget}
rightToggle={rightToggle}
text={props.name}
/>
</div>
);
}

View File

@ -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<HTMLDivElement>,
) {
title: string;
};
rightToggle: {
disable: boolean;
onClick: React.MouseEventHandler;
title: string;
};
}) {
return (
<SplitButtonWrapper
$BGCSSVar={props.bGCSSVar}
$ColorCSSVar={props.colorCSSVar}
$isLeftToggleDisabled={props.leftToggle.disable}
$isRightToggleDisabled={props.rightToggle.disable}
draggable
onDragStart={props.onDragStart}
ref={ref}
style={styles}
>
{!props.leftToggle.disable && (
<span
@ -155,5 +142,3 @@ export function _SplitButton(
</SplitButtonWrapper>
);
}
export const SplitButton = forwardRef(_SplitButton);

View File

@ -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<HTMLDivElement>) => {
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 */

View File

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

View File

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

View File

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

View File

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