diff --git a/apps/builder/app/builder/features/breakpoints/trigger-button.tsx b/apps/builder/app/builder/features/breakpoints/trigger-button.tsx index e1ebe4ccea..86d2edbe08 100644 --- a/apps/builder/app/builder/features/breakpoints/trigger-button.tsx +++ b/apps/builder/app/builder/features/breakpoints/trigger-button.tsx @@ -35,7 +35,7 @@ export const TriggerButton = (props: TriggerButtonProps) => { const scale = useStore(scaleStore); const breakpoint = useStore(selectedBreakpointStore); const [canvasWidth] = useCanvasWidth(); - if (breakpoint === undefined) { + if (breakpoint === undefined || canvasWidth === undefined) { return null; } return ( @@ -46,7 +46,7 @@ export const TriggerButton = (props: TriggerButtonProps) => { })} > - {canvasWidth} + {Math.round(canvasWidth)} {scale !== 100 && ( diff --git a/apps/builder/app/builder/features/style-panel/sections/space/scrub.tsx b/apps/builder/app/builder/features/style-panel/sections/space/scrub.tsx index 2de3f22827..aa561f195d 100644 --- a/apps/builder/app/builder/features/style-panel/sections/space/scrub.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/space/scrub.tsx @@ -77,12 +77,12 @@ export const useScrub = (props: { setValues(isEphemeral ? nextValues : {}); }; - const scrub = numericScrubControl(finalTarget.element, { + return numericScrubControl(finalTarget.element, { direction: property.endsWith("Left") || property.endsWith("Right") ? "horizontal" : "vertical", - getValue() { + getInitialValue() { const { value } = nonDependencies.current.props; if (value?.type === "unit") { unitRef.current = value.unit; @@ -103,8 +103,6 @@ export const useScrub = (props: { ); }, }); - - return scrub.disconnectedCallback; }, [finalTarget]); return { diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index 6bf8652050..810689d483 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -85,7 +85,7 @@ const useScrub = ({ return; } - const scrub = numericScrubControl(scrubRefCurrent, { + return numericScrubControl(scrubRefCurrent, { // @todo: after this https://github.com/webstudio-is/webstudio-builder/issues/564 // we can switch back on using just initial value // @@ -95,7 +95,7 @@ const useScrub = ({ // Until we have decision do we use key properties for this or not, // on of the solution to get value inside scrub is to use ref and lazy getter. // Getter to avoid recreating scrub on every value change - getValue: () => { + getInitialValue: () => { if (valueRef.current.type === "unit") { return valueRef.current.value; } @@ -130,8 +130,6 @@ const useScrub = ({ }, shouldHandleEvent: shouldHandleEvent, }); - - return scrub.disconnectedCallback; }, [type, unit, shouldHandleEvent]); return [scrubRef, inputRef]; diff --git a/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx b/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx index e355e18637..eed3e5f07a 100644 --- a/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx +++ b/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx @@ -60,16 +60,13 @@ export const CanvasTools = ({ publish }: CanvasToolsProps) => { ) : null; } - if (isPreviewMode) { - return null; - } return ( - - - + {isPreviewMode === false && } + {isPreviewMode === false && } + {isPreviewMode === false && } ); }; 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 668afc3923..b8b11c5b71 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 @@ -1,9 +1,8 @@ import { useStore } from "@nanostores/react"; import { findApplicableMedia } from "@webstudio-is/css-engine"; -import { css, theme, useDrag } from "@webstudio-is/design-system"; -import { useState } from "react"; +import { css, numericScrubControl, theme } from "@webstudio-is/design-system"; +import { useEffect, useRef, useState } from "react"; import { - canvasRectStore, canvasWidthStore, isCanvasPointerEventsEnabledStore, } from "~/builder/shared/nano-states"; @@ -78,59 +77,64 @@ const handleIcon = ( ); -const baseDragProps = { - startDistanceThreashold: 0, - elementToData: () => true, - onStart() { - isCanvasPointerEventsEnabledStore.set(false); - isResizingCanvasStore.set(true); - }, - onEnd() { - isCanvasPointerEventsEnabledStore.set(true); - isResizingCanvasStore.set(false); - }, +const updateBreakpoint = (width: number) => { + const applicableBreakpoint = findApplicableMedia( + Array.from(breakpointsStore.get().values()), + width + ); + if (applicableBreakpoint) { + selectedBreakpointIdStore.set(applicableBreakpoint.id); + } +}; + +const useScrub = ({ side }: { side: "right" | "left" }) => { + const ref = useRef(null); + + useEffect(() => { + if (ref.current === null) { + return; + } + return numericScrubControl(ref.current, { + getInitialValue() { + return canvasWidthStore.get(); + }, + getValue(state, movement) { + const value = + side === "left" + ? // * 2 is a compensation for the fact that canvas is centered, so when we scrub, width has to change twice faster, + // otherwise cursor will be faster than the edge movement + state.value - movement * 2 + : state.value + movement * 2; + return Math.max(value, minCanvasWidth); + }, + onStatusChange(status) { + if (status === "scrubbing") { + isCanvasPointerEventsEnabledStore.set(false); + isResizingCanvasStore.set(true); + return; + } + isCanvasPointerEventsEnabledStore.set(true); + isResizingCanvasStore.set(false); + }, + onValueInput(event) { + canvasWidthStore.set(event.value); + updateBreakpoint(event.value); + }, + }); + }, [side]); + + return ref; }; const useResize = () => { const isDragging = useStore(isCanvasPointerEventsEnabledStore) === false; const [isHovering, setIsHovering] = useState(false); - - const updateBreakpoint = (width: number) => { - const applicableBreakpoint = findApplicableMedia( - Array.from(breakpointsStore.get().values()), - width - ); - if (applicableBreakpoint) { - selectedBreakpointIdStore.set(applicableBreakpoint.id); - } - }; - - const { rootRef: leftRef } = useDrag({ - ...baseDragProps, - onMove({ x }) { - const rect = canvasRectStore.get(); - if (rect) { - const width = Math.round(Math.max(rect.right - x, minCanvasWidth)); - canvasWidthStore.set(width); - updateBreakpoint(width); - } - }, - }); - const { rootRef: rightRef } = useDrag({ - ...baseDragProps, - onMove({ x }) { - const rect = canvasRectStore.get(); - if (rect) { - const width = Math.max(Math.round(x - rect.left), minCanvasWidth); - canvasWidthStore.set(width); - updateBreakpoint(width); - } - }, - }); + const leftRef = useScrub({ side: "left" }); + const rightRef = useScrub({ side: "right" }); const handleMouseEnter = () => setIsHovering(true); - const handleMouseOut = () => setIsHovering(false); + const handleMouseLeave = () => setIsHovering(false); const state = isDragging ? "dragging" : isHovering ? "hovering" : "idle"; @@ -139,12 +143,12 @@ const useResize = () => { leftRef, rightRef, handleMouseEnter, - handleMouseOut, + handleMouseLeave, }; }; export const ResizeHandles = () => { - const { state, leftRef, rightRef, handleMouseEnter, handleMouseOut } = + const { state, leftRef, rightRef, handleMouseEnter, handleMouseLeave } = useResize(); return ( @@ -152,7 +156,7 @@ export const ResizeHandles = () => {
{
disconnectedCallback(); + return cleanup; }, [ref, read, write, callback]); }; diff --git a/packages/design-system/src/components/primitives/create-content-controller.ts b/packages/design-system/src/components/primitives/create-content-controller.ts index 4d1ea083ac..4b3cdaf425 100644 --- a/packages/design-system/src/components/primitives/create-content-controller.ts +++ b/packages/design-system/src/components/primitives/create-content-controller.ts @@ -2,11 +2,11 @@ * @description * - detects whether the pointer/keyboard is on any given token as specified by the {contents: []} contract. * - pointermove/keyup in turn dispatch onMouseMove/onCaretMove with the current state if and when within a configured content token. - * - returns two methods {update, disconnectedCallback} the former of which dispatches a content update(while preserving the caret position if any) + * - returns two methods {update, cleanup} the former of which dispatches a content update(while preserving the caret position if any) * - read/write methods are called when reading from the passed element or when updating/writing to the same element. * @example * const target = document.querySelector('input'); - * const {update, disconnectedCallback} = createContentController(target, { + * const {update, cleanup} = createContentController(target, { * contents: [ * { name: 'unit', match: (value) => value === 'px' }, * { name: 'number', match: (value) => Number.isNaN(parseFloat(value)) === false }, @@ -89,7 +89,7 @@ export const createContentController = ( } ): { update: (value: string) => void; - disconnectedCallback: () => void; + cleanup: () => void; } => { const eventNames = ["keyup", "pointermove"] as const; const handleEvent = (event: KeyboardEvent | PointerEvent): void => { @@ -136,7 +136,7 @@ export const createContentController = ( write(targetNode, value); targetNode.setSelectionRange(selectionStart, selectionEnd); }, - disconnectedCallback: () => { + cleanup: () => { eventNames.forEach((eventName) => targetNode.removeEventListener(eventName, handleEvent, false) ); diff --git a/packages/design-system/src/components/primitives/numeric-gesture-control.stories.tsx b/packages/design-system/src/components/primitives/numeric-gesture-control.stories.tsx index e748f9a5ba..51b49525f2 100644 --- a/packages/design-system/src/components/primitives/numeric-gesture-control.stories.tsx +++ b/packages/design-system/src/components/primitives/numeric-gesture-control.stories.tsx @@ -19,8 +19,8 @@ const useNumericScrubControl = ({ return; } ref.current.value = String(value); - const { disconnectedCallback } = numericScrubControl(ref.current, { - getValue: () => value, + return numericScrubControl(ref.current, { + getInitialValue: () => value, direction: direction, onValueChange: (event) => { event.preventDefault(); @@ -28,7 +28,6 @@ const useNumericScrubControl = ({ (event.target as HTMLInputElement).select(); }, }); - return () => disconnectedCallback(); }, [direction, value, ref]); }; diff --git a/packages/design-system/src/components/primitives/numeric-gesture-control.ts b/packages/design-system/src/components/primitives/numeric-gesture-control.ts index f1d32770ce..5c174f35df 100644 --- a/packages/design-system/src/components/primitives/numeric-gesture-control.ts +++ b/packages/design-system/src/components/primitives/numeric-gesture-control.ts @@ -25,9 +25,15 @@ export type NumericScrubCallback = (event: { }) => void; export type NumericScrubOptions = { + inverse?: boolean; minValue?: NumericScrubValue; maxValue?: NumericScrubValue; - getValue: () => number | undefined; + getInitialValue: () => number | undefined; + getValue?: ( + state: NumericScrubState, + movement: number, + options: NumericScrubOptions + ) => number; direction?: NumericScrubDirection; onValueInput?: NumericScrubCallback; onValueChange?: NumericScrubCallback; @@ -42,19 +48,37 @@ type NumericScrubState = { timerId?: ReturnType; }; -export const numericScrubControl = ( - targetNode: HTMLElement | SVGElement, +const getValueDefault = ( + state: NumericScrubState, + movement: number, { minValue = Number.MIN_SAFE_INTEGER, maxValue = Number.MAX_SAFE_INTEGER, - getValue, + }: NumericScrubOptions +) => { + const value = state.value + movement; + if (value < minValue) { + return minValue; + } + if (state.value > maxValue) { + return maxValue; + } + return value; +}; + +export const numericScrubControl = ( + targetNode: HTMLElement | SVGElement, + options: NumericScrubOptions +) => { + const { + getInitialValue, + getValue = getValueDefault, direction = "horizontal", onValueInput, onValueChange, onStatusChange, shouldHandleEvent, - }: NumericScrubOptions -) => { + } = options; const eventNames = ["pointerup", "pointerdown"] as const; const state: NumericScrubState = { // We will read value lazyly in a moment it will be used to avoid having outdated value @@ -66,6 +90,25 @@ export const numericScrubControl = ( let exitPointerLock: (() => void) | undefined = undefined; + let originalUserSelect = ""; + + const cleanup = () => { + targetNode.removeEventListener("pointermove", handleEvent); + onStatusChange?.("idle"); + clearTimeout(state.timerId); + + exitPointerLock?.(); + exitPointerLock = undefined; + if (originalUserSelect) { + targetNode.ownerDocument.documentElement.style.userSelect = + originalUserSelect; + } else { + targetNode.ownerDocument.documentElement.style.removeProperty( + "user-select" + ); + } + }; + // Cannot define `event:` as PointerEvent, // because (HTMLElement | SVGElement).addEventListener("pointermove", ...) // takes (Event => void) as a callback @@ -81,13 +124,7 @@ export const numericScrubControl = ( switch (type) { case "pointerup": { const shouldComponentUpdate = Boolean(state.cursor); - targetNode.removeEventListener("pointermove", handleEvent); - onStatusChange?.("idle"); - clearTimeout(state.timerId); - - exitPointerLock?.(); - exitPointerLock = undefined; - + cleanup(); if (shouldComponentUpdate) { onValueChange?.({ target: targetNode, @@ -108,11 +145,11 @@ export const numericScrubControl = ( if (event.pressure === 0 || event.button !== 0) { break; } - const value = getValue(); + const value = getInitialValue(); // We don't support scrub on non unit values // Its highly unlikely that the value here will be undefined, as useScrub tries to not create scrub on non unit values - // but having that we use lazy getValue() and vanilla js events it's possible. + // but having that we use lazy getInitialValue() and vanilla js events it's possible. if (value === undefined) { return; } @@ -127,17 +164,17 @@ export const numericScrubControl = ( onStatusChange?.("scrubbing"); targetNode.addEventListener("pointermove", handleEvent); + originalUserSelect = + targetNode.ownerDocument.documentElement.style.userSelect; + targetNode.ownerDocument.documentElement.style.userSelect = "none"; break; } case "pointermove": { - state.value += movement; - - if (state.value < minValue) { - state.value = minValue; - } else if (state.value > maxValue) { - state.value = maxValue; + const nextValue = getValue(state, movement, options); + if (nextValue === state.value) { + break; } - + state.value = nextValue; onValueInput?.({ target: targetNode, value: state.value, @@ -172,19 +209,11 @@ export const numericScrubControl = ( targetNode.addEventListener(eventName, handleEvent) ); - return { - disconnectedCallback: () => { - eventNames.forEach((eventName) => - targetNode.removeEventListener(eventName, handleEvent) - ); - - clearTimeout(state.timerId); - targetNode.removeEventListener("pointermove", handleEvent); - onStatusChange?.("idle"); - - exitPointerLock?.(); - exitPointerLock = undefined; - }, + return () => { + eventNames.forEach((eventName) => + targetNode.removeEventListener(eventName, handleEvent) + ); + cleanup(); }; }; diff --git a/packages/design-system/src/components/primitives/use-scrub.ts b/packages/design-system/src/components/primitives/use-scrub.ts index ca2bc52a29..355a4d623a 100644 --- a/packages/design-system/src/components/primitives/use-scrub.ts +++ b/packages/design-system/src/components/primitives/use-scrub.ts @@ -40,8 +40,8 @@ export const useScrub = ({ return; } - const scrub = numericScrubControl(scrubRefCurrent, { - getValue: () => { + return numericScrubControl(scrubRefCurrent, { + getInitialValue: () => { return valueRef.current; }, onValueInput(event) { @@ -66,8 +66,6 @@ export const useScrub = ({ }, shouldHandleEvent: shouldHandleEventRef.current, }); - - return scrub.disconnectedCallback; }, []); return { scrubRef, inputRef };