Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Anvil widget name component rendering #33672

Merged
merged 7 commits into from
May 23, 2024
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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.
Expand Down Expand Up @@ -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>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,14 +13,19 @@ 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;
-webkit-tap-highlight-color: transparent;
gap: 1px;

& button {
cursor: pointer;
cursor: grab;
appearance: none;
background: none;
border: none;
Expand All @@ -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});
Expand All @@ -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;
Expand All @@ -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)}) {
Expand All @@ -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;
title: string;
};
rightToggle: {
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;
};
}) {
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
Expand All @@ -155,5 +142,3 @@ export function _SplitButton(
</SplitButtonWrapper>
);
}

export const SplitButton = forwardRef(_SplitButton);
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 */
Expand All @@ -77,7 +85,13 @@ export function AnvilWidgetName(props: {

let cleanup = () => {};
useEffect(() => {
if (widgetElement && widgetNameComponent && widgetsEditorElement) {
if (
widgetElement &&
widgetNameComponent &&
widgetsEditorElement &&
// Makes sure we add listeners only if the widget is selected or focused
nameComponentState !== "none"
) {
cleanup = handleWidgetUpdate(
widgetElement,
widgetNameComponent,
Expand All @@ -100,6 +114,8 @@ export function AnvilWidgetName(props: {
return null;
// Don't show widget name component if the widget DOM element isn't found
if (!widgetElement) return null;
// Don't render any DOM nodes if the widget is not selected or focused
if (nameComponentState === "none") return null;

return (
<FloatingPortal>
Expand Down
69 changes: 35 additions & 34 deletions app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,44 +65,49 @@ export function handleWidgetUpdate(
widgetsEditorElement: HTMLDivElement,
nameComponentState: NameComponentStates,
) {
return autoUpdate(widgetElement, widgetNameComponent, () => {
computePosition(widgetElement as HTMLDivElement, widgetNameComponent, {
placement: "top-start",
strategy: "fixed",
middleware: [
flip(),
shift(),
offset({ mainAxis: 8, crossAxis: -5 }),
getOverflowMiddleware(widgetsEditorElement as HTMLDivElement),
hide({ strategy: "referenceHidden" }),
hide({ strategy: "escaped" }),
],
}).then(({ middlewareData, x, y }) => {
let shiftOffset = 0;
if (middlewareData.containWithinCanvas.overflowAmount > 0) {
shiftOffset = middlewareData.containWithinCanvas.overflowAmount + 5;
}
return autoUpdate(
widgetElement,
widgetNameComponent,
() => {
computePosition(widgetElement as HTMLDivElement, widgetNameComponent, {
placement: "top-start",
strategy: "fixed",
middleware: [
flip(),
shift(),
offset({ mainAxis: 0, crossAxis: -5 }),
getOverflowMiddleware(widgetsEditorElement as HTMLDivElement),
hide({ strategy: "referenceHidden", padding: 40 }),
hide({ strategy: "escaped" }),
],
}).then(({ middlewareData, x, y }) => {
let shiftOffset = 0;
if (middlewareData.containWithinCanvas.overflowAmount > 0) {
shiftOffset = middlewareData.containWithinCanvas.overflowAmount + 5;
}

Object.assign(widgetNameComponent.style, {
left: `${x - shiftOffset}px`,
top: `${y}px`,
visibility:
nameComponentState === "none" || middlewareData.hide?.referenceHidden
Object.assign(widgetNameComponent.style, {
left: `${x - shiftOffset}px`,
top: `${y}px`,
visibility: middlewareData.hide?.referenceHidden
? "hidden"
: "visible",
zIndex:
nameComponentState === "focus"
? "calc(var(--on-canvas-ui-zindex) + 1)"
: "var(--on-canvas-ui-zindex)",
zIndex:
nameComponentState === "focus"
? "calc(var(--on-canvas-ui-zindex) + 1)"
: "var(--on-canvas-ui-zindex)",
});
});
});
});
},
{ animationFrame: true },
);
}

export function getWidgetNameComponentStyleProps(
widgetType: string,
nameComponentState: NameComponentStates,
showError: boolean,
isParentSelected: boolean,
) {
const config = WidgetFactory.getConfig(widgetType);
const onCanvasUI = config?.onCanvasUI || {
Expand All @@ -121,11 +126,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.
Expand All @@ -134,7 +134,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
};
Loading
Loading