diff --git a/apps/designer/app/canvas/canvas.tsx b/apps/designer/app/canvas/canvas.tsx index 81914d2ae5..f6d551514f 100644 --- a/apps/designer/app/canvas/canvas.tsx +++ b/apps/designer/app/canvas/canvas.tsx @@ -14,14 +14,13 @@ import { useDeleteInstance, useInsertInstance, usePopulateRootInstance, - usePublishHoveredInstanceData, - usePublishHoveredInstanceRect, usePublishRootInstance, usePublishSelectedInstanceData, usePublishSelectedInstanceDataRect, usePublishTextEditingInstanceId, useReparentInstance, useSetHoveredInstance, + usePublishHoveredInstanceData, useUnselectInstance, useUpdateSelectedInstance, } from "./shared/instance"; @@ -41,7 +40,6 @@ import { useSubscribeScrollState, } from "~/shared/nano-states"; import { registerContainers } from "./shared/immerhin"; -import { useTrackHoveredElement } from "./shared/use-track-hovered-element"; import { usePublishScrollState } from "./shared/use-publish-scroll-state"; import { useDragAndDrop } from "./shared/use-drag-drop"; import { setInstanceChildrenMutable } from "~/shared/tree-utils"; @@ -90,19 +88,17 @@ const DesignMode = ({ treeId, buildId }: DesignModeProps) => { useUpdateStyle(); useManageProps(); usePublishSelectedInstanceData(treeId); - usePublishHoveredInstanceData(); useHandleBreakpoints(); useInsertInstance(); useReparentInstance(); useDeleteInstance(); usePublishRootInstance(); useTrackSelectedElement(); - useTrackHoveredElement(); useSetHoveredInstance(); + usePublishHoveredInstanceData(); useSync({ buildId, treeId }); useUpdateSelectedInstance(); usePublishSelectedInstanceDataRect(); - usePublishHoveredInstanceRect(); useUnselectInstance(); usePublishScrollState(); useSubscribeScrollState(); diff --git a/apps/designer/app/canvas/shared/instance.ts b/apps/designer/app/canvas/shared/instance.ts index 5060b5e8c2..5de7f1cb8d 100644 --- a/apps/designer/app/canvas/shared/instance.ts +++ b/apps/designer/app/canvas/shared/instance.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ObjectId from "bson-objectid"; import { type Instance, @@ -29,7 +29,6 @@ import { import { useSelectedInstance, useSelectedElement, - useHoveredElement, useHoveredInstance, } from "./nano-states"; import { @@ -38,8 +37,12 @@ import { useTextEditingInstanceId, } from "~/shared/nano-states"; import { useMeasure } from "~/shared/dom-hooks"; -import { findInstanceByElement } from "~/shared/dom-utils"; +import { + findInstanceByElement, + getInstanceElementById, +} from "~/shared/dom-utils"; import { publish } from "~/shared/pubsub"; +import { useTrackHoveredElement } from "./use-track-hovered-element"; declare module "~/shared/pubsub" { export interface PubsubMap { @@ -263,23 +266,33 @@ export const usePublishSelectedInstanceDataRect = () => { }, [rect]); }; -export const usePublishHoveredInstanceRect = () => { - const [element] = useHoveredElement(); - const publishRect = useCallback(() => { - if (element === undefined) return; - publish({ - type: "hoveredInstanceRect", - payload: element.getBoundingClientRect(), - }); - }, [element]); - useEffect(publishRect, [publishRect]); -}; - export const useSetHoveredInstance = () => { const [rootInstance] = useRootInstance(); - const [hoveredElement] = useHoveredElement(); const [, setHoveredInstance] = useHoveredInstance(); + const [hoveredElement, setHoveredElement] = useState( + undefined + ); + + useTrackHoveredElement(setHoveredElement); + + useSubscribe( + "navigatorHoveredInstance", + useCallback((instance) => { + setHoveredElement( + instance && (getInstanceElementById(instance.id) || undefined) + ); + }, []) + ); + + useEffect(() => { + if (hoveredElement === undefined) return; + publish({ + type: "hoveredInstanceRect", + payload: hoveredElement.getBoundingClientRect(), + }); + }, [hoveredElement]); + useEffect(() => { let instance; if (rootInstance !== undefined && hoveredElement) { diff --git a/apps/designer/app/canvas/shared/nano-states.ts b/apps/designer/app/canvas/shared/nano-states.ts index b5b3a8fbcd..542c883bcb 100644 --- a/apps/designer/app/canvas/shared/nano-states.ts +++ b/apps/designer/app/canvas/shared/nano-states.ts @@ -15,6 +15,3 @@ const selectedElementContainer = createValueContainer< HTMLElement | undefined >(); export const useSelectedElement = () => useValue(selectedElementContainer); - -const hoveredElementContainer = createValueContainer(); -export const useHoveredElement = () => useValue(hoveredElementContainer); diff --git a/apps/designer/app/canvas/shared/use-track-hovered-element.ts b/apps/designer/app/canvas/shared/use-track-hovered-element.ts index 5de85a2ab6..1d9c2c785a 100644 --- a/apps/designer/app/canvas/shared/use-track-hovered-element.ts +++ b/apps/designer/app/canvas/shared/use-track-hovered-element.ts @@ -1,14 +1,15 @@ import { useEffect } from "react"; -import { useSelectedElement, useHoveredElement } from "./nano-states"; +import { useSelectedElement } from "./nano-states"; import { useRootInstance } from "~/shared/nano-states"; const eventOptions = { passive: true, }; -export const useTrackHoveredElement = () => { +export const useTrackHoveredElement = ( + onChange: (element: HTMLElement | undefined) => void +) => { const [rootInstance] = useRootInstance(); - const [, setHoveredElement] = useHoveredElement(); const [selectedElement] = useSelectedElement(); useEffect(() => { @@ -21,12 +22,12 @@ export const useTrackHoveredElement = () => { ) { return; } - setHoveredElement(element); + onChange(element); }; const handleMouseOut = () => { if (rootInstance === undefined) return; - setHoveredElement(undefined); + onChange(undefined); }; window.addEventListener("mouseover", handleMouseOver, eventOptions); @@ -36,5 +37,5 @@ export const useTrackHoveredElement = () => { window.removeEventListener("mouseover", handleMouseOver); window.removeEventListener("mouseout", handleMouseOut); }; - }, [rootInstance, selectedElement, setHoveredElement]); + }, [rootInstance, selectedElement, onChange]); }; diff --git a/apps/designer/app/designer/features/sidebar-left/lib/navigator/navigator.tsx b/apps/designer/app/designer/features/sidebar-left/lib/navigator/navigator.tsx index c4431c40e9..b5fd3977c7 100644 --- a/apps/designer/app/designer/features/sidebar-left/lib/navigator/navigator.tsx +++ b/apps/designer/app/designer/features/sidebar-left/lib/navigator/navigator.tsx @@ -1,5 +1,5 @@ import { Flex } from "@webstudio-is/design-system"; -import type { Instance } from "@webstudio-is/react-sdk"; +import { type Instance } from "@webstudio-is/react-sdk"; import { type Publish } from "~/shared/pubsub"; import { useCallback } from "react"; import { useSelectedInstanceData } from "~/designer/shared/nano-states"; @@ -15,6 +15,7 @@ declare module "~/shared/pubsub" { dropTarget: { instanceId: Instance["id"]; position: number | "end" }; }; deleteInstance: { id: Instance["id"] }; + navigatorHoveredInstance: { id: Instance["id"] } | undefined; } } @@ -67,6 +68,16 @@ export const Navigator = ({ publish, isClosable, onClose }: NavigatorProps) => { [publish] ); + const handleHover = useCallback( + (instance: Instance | undefined) => { + publish({ + type: "navigatorHoveredInstance", + payload: instance && { id: instance.id }, + }); + }, + [publish] + ); + if (rootInstance === undefined) return null; return ( @@ -76,6 +87,7 @@ export const Navigator = ({ publish, isClosable, onClose }: NavigatorProps) => { root={rootInstance} selectedItemId={selectedInstanceData?.id} onSelect={handleSelect} + onHover={handleHover} onDragEnd={handleDragEnd} onDelete={handleDelete} /> diff --git a/packages/design-system/src/components/tree/tree-node.tsx b/packages/design-system/src/components/tree/tree-node.tsx index 298b8dd204..8c24045244 100644 --- a/packages/design-system/src/components/tree/tree-node.tsx +++ b/packages/design-system/src/components/tree/tree-node.tsx @@ -143,6 +143,8 @@ export type TreeNodeProps = { selectedItemId?: string; parentIsSelected?: boolean; onSelect?: (itemId: string) => void; + onMouseEnter?: (item: Data) => void; + onMouseLeave?: (item: Data) => void; level?: number; animate?: boolean; @@ -161,6 +163,8 @@ export const TreeNode = ({ setIsExpanded, selectedItemId, onSelect, + onMouseEnter, + onMouseLeave, onExpandTransitionEnd, forceHoverStateAtItem, renderItem, @@ -213,6 +217,8 @@ export const TreeNode = ({ data-drop-target-id={itemData.id} > onMouseEnter?.(itemData)} + onMouseLeave={() => onMouseLeave?.(itemData)} isSelected={isSelected} parentIsSelected={parentIsSelected} enableHoverState={forceHoverStateAtItem === undefined} diff --git a/packages/design-system/src/components/tree/tree.tsx b/packages/design-system/src/components/tree/tree.tsx index bdb6e47256..ceefee0207 100644 --- a/packages/design-system/src/components/tree/tree.tsx +++ b/packages/design-system/src/components/tree/tree.tsx @@ -30,6 +30,7 @@ export type TreeProps = { selectedItemId?: string; onSelect?: (itemId: string) => void; + onHover?: (item: Data | undefined) => void; animate?: boolean; onDragEnd: (event: { itemId: string; @@ -49,6 +50,7 @@ export const Tree = ({ renderItem, selectedItemId, onSelect, + onHover, animate, onDragEnd, onDelete, @@ -238,6 +240,13 @@ export const Tree = ({ onEsc: dragHandlers.cancelCurrentDrag, }); + const [onMouseEnter, onMouseLeave] = useMemo(() => { + if (onHover === undefined) { + return [undefined, undefined]; + } + return [(item: Data) => onHover(item), () => onHover(undefined)] as const; + }, [onHover]); + return ( ({ getItemChildren={getItemChildren} animate={animate} onSelect={onSelect} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} selectedItemId={selectedItemId} itemData={root} level={0}