diff --git a/apps/builder/app/builder/features/help/remote-dialog.tsx b/apps/builder/app/builder/features/help/remote-dialog.tsx index 407ef91ebd..4661982bfe 100644 --- a/apps/builder/app/builder/features/help/remote-dialog.tsx +++ b/apps/builder/app/builder/features/help/remote-dialog.tsx @@ -13,7 +13,6 @@ import { ExternalLinkIcon } from "@webstudio-is/icons"; export const RemoteDialog = () => { const options = useStore($remoteDialog); - if (options === undefined) { return; } @@ -25,11 +24,16 @@ export const RemoteDialog = () => { $remoteDialog.set(undefined); }} modal={false} + draggable + resize="auto" > - + {/* Title is at the end intentionally, * to make the close button last in the tab order diff --git a/apps/builder/app/builder/features/project-settings/project-settings.tsx b/apps/builder/app/builder/features/project-settings/project-settings.tsx index 78105832e2..833fd2869a 100644 --- a/apps/builder/app/builder/features/project-settings/project-settings.tsx +++ b/apps/builder/app/builder/features/project-settings/project-settings.tsx @@ -41,9 +41,12 @@ export const ProjectSettingsView = ({ const isDesignMode = useStore($isDesignMode); return ( - + {itemLabel}} diff --git a/apps/builder/app/builder/features/workspace/canvas-tools/resize-handles.tsx b/apps/builder/app/builder/features/workspace/canvas-tools/resize-handles.tsx index a822f02c5d..3eb9b67c12 100644 --- a/apps/builder/app/builder/features/workspace/canvas-tools/resize-handles.tsx +++ b/apps/builder/app/builder/features/workspace/canvas-tools/resize-handles.tsx @@ -142,22 +142,12 @@ const useScrub = ({ side }: { side: "right" | "left" }) => { return ref; }; -const useResize = () => { +export const ResizeHandles = () => { const isResizing = useStore($isResizingCanvas); const leftRef = useScrub({ side: "left" }); const rightRef = useScrub({ side: "right" }); const state = isResizing ? "resizing" : "idle"; - return { - state, - leftRef, - rightRef, - }; -}; - -export const ResizeHandles = () => { - const { state, leftRef, rightRef } = useResize(); - return ( <>
} hidden={props.hidden} - draggable={true} + draggable state={props.state} focused={props.focused} active={props.active} diff --git a/packages/design-system/src/components/dialog.tsx b/packages/design-system/src/components/dialog.tsx index b7d6bd37bb..42f71c01c1 100644 --- a/packages/design-system/src/components/dialog.tsx +++ b/packages/design-system/src/components/dialog.tsx @@ -13,12 +13,13 @@ import * as Primitive from "@radix-ui/react-dialog"; import { css, theme, type CSS } from "../stitches.config"; import { PanelTitle } from "./panel-title"; import { Flex } from "./flex"; -import { useDisableCanvasPointerEvents } from "../utilities"; +import { useDisableCanvasPointerEvents, useResize } from "../utilities"; import type { CSSProperties } from "@stitches/react"; import { mergeRefs } from "@react-aria/utils"; import { Button } from "./button"; import { XIcon, MaximizeIcon, MinimizeIcon } from "@webstudio-is/icons"; import { Separator } from "./separator"; +import { Text } from "./text"; export const DialogTrigger = Primitive.Trigger; @@ -51,15 +52,28 @@ const panelStyle = css({ const DialogContext = createContext<{ isMaximized: boolean; setIsMaximized: (isMaximized: boolean) => void; + resize?: "auto" | "none"; + draggable?: boolean; }>({ isMaximized: false, setIsMaximized: () => {}, + draggable: true, + resize: "none", }); -export const Dialog = (props: ComponentProps) => { +export const Dialog = ({ + resize, + draggable, + ...props +}: ComponentProps & { + resize?: "auto" | "none"; + draggable?: boolean; +}) => { const [isMaximized, setIsMaximized] = useState(false); return ( - + ); @@ -128,16 +142,13 @@ const useDraggable = ({ rect: Rect; } >(undefined); - const { enableCanvasPointerEvents, disableCanvasPointerEvents } = - useDisableCanvasPointerEvents(); - const draggableRef = useRef(null); + const ref = useRef(null); const handleDragStart: DragEventHandler = (event) => { - const target = draggableRef.current; + const target = ref.current; if (target === null) { return; } - disableCanvasPointerEvents(); if (placeholderImage) { event.dataTransfer.setDragImage(placeholderImage, 0, 0); } @@ -153,7 +164,7 @@ const useDraggable = ({ }; const handleDrag: DragEventHandler = (event) => { - const target = draggableRef.current; + const target = ref.current; if ( event.pageX <= 0 || @@ -206,9 +217,45 @@ const useDraggable = ({ return { onDragStart: handleDragStart, onDrag: handleDrag, - onDragEnd: enableCanvasPointerEvents, style, - draggableRef, + ref, + }; +}; + +// This is needed to prevent pointer events on the iframe from interfering with dragging and resizing. +const useSetPointerEvents = () => { + const { enableCanvasPointerEvents, disableCanvasPointerEvents } = + useDisableCanvasPointerEvents(); + + const setPointerEvents = (value: string) => { + return () => { + value === "none" + ? disableCanvasPointerEvents() + : enableCanvasPointerEvents(); + // RAF is needed otherwise dragstart event won't fire because of pointer-events: none + requestAnimationFrame(() => { + if (element) { + element.style.pointerEvents = value; + } + }); + }; + }; + + const [element, ref] = useResize({ + onResizeStart: setPointerEvents("none"), + onResizeEnd: setPointerEvents("auto"), + }); + + const { resize, draggable } = useContext(DialogContext); + + if (resize === "none" && draggable !== true) { + return {}; + } + + return { + ref, + onDragStartCapture: setPointerEvents("none"), + onDragEndCapture: setPointerEvents("auto"), }; }; @@ -218,7 +265,6 @@ export const DialogContent = forwardRef( children, className, css, - resize = "none", width, height, x, @@ -229,11 +275,11 @@ export const DialogContent = forwardRef( }: ComponentProps & UseDraggableProps & { css?: CSS; - resize?: "auto" | "none"; }, forwardedRef: Ref ) => { - const { draggableRef, ...draggableProps } = useDraggable({ + const { resize } = useContext(DialogContext); + const { ref: draggableRef, ...draggableProps } = useDraggable({ width, height, x, @@ -242,14 +288,18 @@ export const DialogContent = forwardRef( minHeight, }); + const { ref: pointerEventsRef, ...pointerEventsProps } = + useSetPointerEvents(); + return ( {children} @@ -265,6 +315,13 @@ const titleSlotStyle = css({ order: -1, }); +const titleStyle = css({ + display: "flex", + flexGrow: 1, + height: "100%", + alignItems: "center", +}); + export const DialogTitle = ({ children, suffix, @@ -272,27 +329,30 @@ export const DialogTitle = ({ }: ComponentProps & { suffix?: ReactNode; closeLabel?: string; -}) => ( -
- }> - {children} - - -
-); +}) => { + const { draggable } = useContext(DialogContext); + + return ( +
+ }> + + + {children} + + + + +
+ ); +}; export const DialogTitleActions = ({ children }: { children: ReactNode }) => { - return ( - { - // Prevent dragging dialog - event.preventDefault(); - }} - > - {children} - - ); + return {children}; }; export const DialogActions = ({ children }: { children: ReactNode }) => { @@ -345,8 +405,3 @@ const contentStyle = css(panelStyle, { }, }, }); - -const titleStyle = css({ - // Resetting H2 styles (Primitive.Title is H2) - all: "unset", -}); diff --git a/packages/design-system/src/components/floating-panel.tsx b/packages/design-system/src/components/floating-panel.tsx index b2b5474237..c8a9edbe4c 100644 --- a/packages/design-system/src/components/floating-panel.tsx +++ b/packages/design-system/src/components/floating-panel.tsx @@ -52,7 +52,7 @@ type FloatingPanelProps = { content: ReactNode; children: ReactNode; maximizable?: boolean; - resize?: ComponentProps["resize"]; + resize?: ComponentProps["resize"]; width?: number; height?: number; // - bottom - below the trigger button @@ -134,13 +134,17 @@ export const FloatingPanel = ({ useLayoutEffect(calcPosition, [calcPosition]); return ( - + {children} & { suffix?: ReactNode; css?: CSS; }; -const Container = styled("div", textVariants.titles, { +const Container = styled("div", { display: "flex", alignItems: "center", flexShrink: 0, @@ -27,7 +25,6 @@ const Container = styled("div", textVariants.titles, { height: theme.spacing[15], paddingInline: theme.panel.paddingInline, paddingRight: theme.spacing[5], - color: theme.colors.foregroundMain, }); const SuffixSlot = styled("div", { @@ -48,7 +45,13 @@ export const PanelTitle = forwardRef( ref: Ref ) => ( - {children} + {typeof children === "string" ? ( + + {children} + + ) : ( + children + )} {suffix && {suffix}} ) diff --git a/packages/design-system/src/utilities.ts b/packages/design-system/src/utilities.ts index c59d243d1c..963680e020 100644 --- a/packages/design-system/src/utilities.ts +++ b/packages/design-system/src/utilities.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import warnOnce from "warn-once"; import type { CSS } from "./stitches.config"; @@ -85,6 +85,63 @@ export const useDisableCanvasPointerEvents = () => { return enableDisable; }; +/** + * Uses ResizeObserver to notify about resize events, with start, end and timeout. + */ +export const useResize = ({ + onResizeStart, + onResize, + onResizeEnd, + timeout = 300, +}: { + onResizeStart?: () => void; + onResize?: () => void; + onResizeEnd?: () => void; + timeout?: number; +}) => { + const [element, ref] = useState(null); + const timeoutRef = useRef>(); + const onResizeStartRef = useRef(onResizeStart); + onResizeStartRef.current = onResizeStart; + const onResizeRef = useRef(onResize); + onResizeRef.current = onResize; + const onResizeEndRef = useRef(onResizeEnd); + onResizeEndRef.current = onResizeEnd; + const isResizingRef = useRef(); + + useEffect(() => { + if (element === null) { + return; + } + // Mark resizing as on a new observer instance, we will use this to skip first resize event. + isResizingRef.current = undefined; + const observer = new ResizeObserver(() => { + // Resize observer called first time is not a start of resize + if (isResizingRef.current === undefined) { + isResizingRef.current = false; + return; + } + if (isResizingRef.current === false) { + isResizingRef.current = true; + onResizeStartRef.current?.(); + } + onResizeRef.current?.(); + clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + onResizeEndRef.current?.(); + isResizingRef.current = false; + }, timeout); + }); + observer.observe(element); + return () => { + clearTimeout(timeoutRef.current); + observer.disconnect(); + }; + }, [element, onResizeStartRef, onResizeRef, onResizeEndRef, timeout]); + + return [element, ref] as const; +}; + export const truncate = (): CSS => ({ whiteSpace: "nowrap", textOverflow: "ellipsis",