From a65b7140141274e6ea87316349ea6b1fc2b69612 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Tue, 18 Mar 2025 16:04:38 +0000 Subject: [PATCH 1/7] limit and truncate the values --- .../sections/shared/value-text.tsx | 15 +++++++-- .../style-panel/sections/space/space.tsx | 31 +++++-------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx b/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx index 83a46cf30088..120b95f5784c 100644 --- a/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx @@ -9,12 +9,21 @@ const Container = styled("button", { // leave it here in case of tag change width: "fit-content", display: "flex", - flexWrap: "wrap", + flexWrap: "nowrap", alignItems: "baseline", - justifyContent: "center", + justifyContent: "start", border: "none", borderRadius: theme.borderRadius[3], padding: `${theme.spacing[2]}`, + overflow: "hidden", + whiteSpace: "nowrap", + // We want value to have `default` cursor to indicate that it's clickable, + // unlike the rest of the value area that has cursor that indicates scrubbing. + // Click and scrub works everywhere anyway, but we want cursors to be different. + // + // In order to have control over cursor we're setting pointerEvents to "all" here + // because SpaceLayout sets it to "none" for cells' content. + pointerEvents: "all", "&:focus-visible": { outline: "none", @@ -95,7 +104,7 @@ export const ValueText = ({ if (value.type === "var") { return ( - --{value.value} + {value.value} ); } diff --git a/apps/builder/app/builder/features/style-panel/sections/space/space.tsx b/apps/builder/app/builder/features/style-panel/sections/space/space.tsx index 00ebc38dc0dd..d336a8f2068c 100644 --- a/apps/builder/app/builder/features/style-panel/sections/space/space.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/space/space.tsx @@ -1,5 +1,4 @@ import { useState, useRef } from "react"; -import { theme } from "@webstudio-is/design-system"; import type { CssProperty } from "@webstudio-is/css-engine"; import { SpaceLayout } from "./layout"; import { ValueText } from "../shared/value-text"; @@ -10,7 +9,6 @@ import { type SpaceStyleProperty, } from "./properties"; import { InputPopover } from "../shared/input-popover"; -import { SpaceTooltip } from "./tooltip"; import { StyleSection } from "../../shared/style-section"; import { useKeyboardNavigation } from "../shared/keyboard"; import { useComputedStyleDecl, useComputedStyles } from "../../shared/model"; @@ -120,27 +118,14 @@ const Cell = ({ getActiveProperties={getActiveProperties} onClose={onPopoverClose} /> - - - onHover({ property, element: event.currentTarget }) - } - onMouseLeave={() => onHover(undefined)} - /> - + + onHover({ property, element: event.currentTarget }) + } + onMouseLeave={() => onHover(undefined)} + /> ); }; From a3a09a721d8717e0dd1915e2150d14a12ce540b3 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 19 Mar 2025 10:58:12 +0000 Subject: [PATCH 2/7] already added in ValueText --- .../style-panel/sections/position/inset-control.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/position/inset-control.tsx b/apps/builder/app/builder/features/style-panel/sections/position/inset-control.tsx index e7f5c6e124c3..cf4c7531cebc 100644 --- a/apps/builder/app/builder/features/style-panel/sections/position/inset-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/position/inset-control.tsx @@ -50,15 +50,6 @@ const Cell = ({ /> From 8f774303c6a7d0d45178f6055a418c0566ad73eb Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 19 Mar 2025 13:03:36 +0000 Subject: [PATCH 3/7] scroll effect --- .../sections/shared/value-text.tsx | 39 +++---- .../css-value-input/css-value-input.tsx | 90 +-------------- .../style-panel/shared/scroll-by-pointer.ts | 105 ++++++++++++++++++ 3 files changed, 122 insertions(+), 112 deletions(-) create mode 100644 apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts diff --git a/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx b/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx index 120b95f5784c..e330547719bd 100644 --- a/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx @@ -1,8 +1,9 @@ import { styled, Text } from "@webstudio-is/design-system"; import type { StyleValue } from "@webstudio-is/css-engine"; -import { useMemo, type ComponentProps } from "react"; +import { useEffect, useMemo, type ComponentProps } from "react"; import { theme } from "@webstudio-is/design-system"; import { toValue } from "@webstudio-is/css-engine"; +import { scrollByPointer } from "../../shared/scroll-by-pointer"; const Container = styled("button", { // fit-content is not needed for the "button" element, @@ -61,21 +62,13 @@ const Container = styled("button", { export const ValueText = ({ value, source, - truncate = false, ...rest -}: { value: StyleValue; truncate?: boolean } & Omit< - ComponentProps, - "value" ->) => { +}: { value: StyleValue } & Omit, "value">) => { const children = useMemo(() => { if (value.type === "unit") { // we want to show "0" rather than "0px" for default values for cleaner UI if (source === "default" && value.unit === "px" && value.value === 0) { - return ( - - {value.value} - - ); + return {value.value}; } /** @@ -85,7 +78,7 @@ export const ValueText = ({ return ( <> - + {value.value} - {value.value} - - ); + return {value.value}; } - return ( - - {toValue(value)} - - ); - }, [value, source, truncate]); + return {toValue(value)}; + }, [value, source]); + + const { abort, ...autoScrollProps } = useMemo(scrollByPointer, []); + + useEffect(() => { + return () => abort("unmount"); + }, [abort]); return ( - + {children} ); 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 682c53a32e8f..71ffe929a370 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 @@ -58,6 +58,7 @@ import { ValueEditorDialog, } from "./value-editor-dialog"; import { useEffectEvent } from "~/shared/hook-utils/effect-event"; +import { scrollByPointer } from "../scroll-by-pointer"; // We need to enable scrub on properties that can have numeric value. const canBeNumber = (property: CssProperty, value: CssValueInputValue) => { @@ -345,93 +346,6 @@ const itemToString = (item: CssValueInputValue | null) => { return toValue(item); }; -const scrollAhead = ({ target, clientX }: MouseEvent) => { - const element = target as HTMLInputElement; - - if (element.scrollWidth === element.clientWidth) { - // Nothing to scroll. - return false; - } - const inputRect = element.getBoundingClientRect(); - - // Calculate the relative x position of the mouse within the input element - const relativeMouseX = clientX - inputRect.x; - - // Calculate the percentage position (0% at the beginning, 100% at the end) - const inputWidth = inputRect.width; - const mousePercentageX = Math.ceil((relativeMouseX / inputWidth) * 100); - - // Apply acceleration based on the relative position of the mouse - // Closer to the beginning (-20%), closer to the end (+20%) - const accelerationFactor = (mousePercentageX - 50) / 50; - const adjustedMousePercentageX = Math.min( - Math.max(mousePercentageX + accelerationFactor * 20, 0), - 100 - ); - - // Calculate the scroll position corresponding to the adjusted percentage - const scrollPosition = - (adjustedMousePercentageX / 100) * - (element.scrollWidth - element.clientWidth); - - // Scroll the input element - element.scroll({ left: scrollPosition }); - return true; -}; - -const getAutoScrollProps = () => { - let abortController = new AbortController(); - - const abort = (reason: string) => { - abortController.abort(reason); - }; - - return { - abort, - onMouseOver(event: MouseEvent) { - if (event.target === document.activeElement) { - abort("focused"); - return; - } - if (scrollAhead(event) === false) { - return; - } - - abortController = new AbortController(); - event.target?.addEventListener( - "mousemove", - (event) => { - if (event.target === document.activeElement) { - abort("focused"); - return; - } - requestAnimationFrame(() => { - scrollAhead(event as MouseEvent); - }); - }, - { - signal: abortController.signal, - passive: true, - } - ); - }, - onMouseOut(event: MouseEvent) { - if (event.target === document.activeElement) { - abort("focused"); - return; - } - (event.target as HTMLInputElement).scroll({ - left: 0, - behavior: "smooth", - }); - abort("mouseout"); - }, - onFocus() { - abort("focus"); - }, - }; -}; - const Description = styled(Box, { width: theme.spacing[27] }); /** @@ -880,7 +794,7 @@ export const CssValueInput = ({ }; const { abort, ...autoScrollProps } = useMemo(() => { - return getAutoScrollProps(); + return scrollByPointer(); }, []); useEffect(() => { diff --git a/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts b/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts new file mode 100644 index 000000000000..9ad7cb61680c --- /dev/null +++ b/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts @@ -0,0 +1,105 @@ +const scrollAhead = (element: HTMLElement, clientX: number) => { + if (element.scrollWidth === element.clientWidth) { + // Nothing to scroll. + return false; + } + const inputRect = element.getBoundingClientRect(); + + // Calculate the relative x position of the mouse within the input element + const relativeMouseX = clientX - inputRect.x; + + // Calculate the percentage position (0% at the beginning, 100% at the end) + const inputWidth = inputRect.width; + const mousePercentageX = Math.ceil((relativeMouseX / inputWidth) * 100); + + // Apply acceleration based on the relative position of the mouse + // Closer to the beginning (-20%), closer to the end (+20%) + const accelerationFactor = (mousePercentageX - 50) / 50; + const adjustedMousePercentageX = Math.min( + Math.max(mousePercentageX + accelerationFactor * 20, 0), + 100 + ); + + // Calculate the scroll position corresponding to the adjusted percentage + const scrollPosition = + (adjustedMousePercentageX / 100) * + (element.scrollWidth - element.clientWidth); + + // Scroll the input element + element.scroll({ left: scrollPosition }); + return true; +}; + +// We don't want to scroll if the element is focused. +// E.g. this is important for inputs, where the user might be interacting with text. +const isFocused = (element: HTMLElement) => element === document.activeElement; + +export const scrollByPointer = () => { + let abortController = new AbortController(); + + const abort = (reason: string) => { + abortController.abort(reason); + }; + + const onMouseOver = (event: MouseEvent) => { + const element = event.currentTarget; + if (element instanceof HTMLElement === false) { + return; + } + if (isFocused(element)) { + abort("focused"); + return; + } + if (scrollAhead(element, event.clientX) === false) { + return; + } + + abortController = new AbortController(); + element?.addEventListener( + "mousemove", + (event: MouseEvent) => { + const element = event.currentTarget; + if (element instanceof HTMLElement === false) { + return; + } + + if (isFocused(element)) { + abort("focused"); + return; + } + requestAnimationFrame(() => { + scrollAhead(element, event.clientX); + }); + }, + { + signal: abortController.signal, + passive: true, + } + ); + }; + + const onMouseOut = (event: MouseEvent) => { + abort("mouseout"); + const element = event.currentTarget; + if (element instanceof HTMLElement === false) { + return; + } + if (isFocused(element)) { + abort("focused"); + return; + } + element.scroll({ + left: 0, + behavior: "smooth", + }); + }; + + return { + abort, + onMouseOver, + onMouseOut, + onFocus() { + abort("focus"); + }, + }; +}; From 9f3945e3286585573f52ac0ebfecdb5b737b5038 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 19 Mar 2025 15:11:30 +0000 Subject: [PATCH 4/7] optimize space --- .../features/style-panel/sections/shared/value-text.tsx | 3 ++- .../app/builder/features/style-panel/sections/space/layout.tsx | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx b/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx index e330547719bd..09fb9e2d7ebe 100644 --- a/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx @@ -15,7 +15,8 @@ const Container = styled("button", { justifyContent: "start", border: "none", borderRadius: theme.borderRadius[3], - padding: `${theme.spacing[2]}`, + paddingBlock: theme.spacing[2], + paddingInline: 0, overflow: "hidden", whiteSpace: "nowrap", // We want value to have `default` cursor to indicate that it's clickable, diff --git a/apps/builder/app/builder/features/style-panel/sections/space/layout.tsx b/apps/builder/app/builder/features/style-panel/sections/space/layout.tsx index 542b2257a6fe..16377b4afd8c 100644 --- a/apps/builder/app/builder/features/style-panel/sections/space/layout.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/space/layout.tsx @@ -173,7 +173,6 @@ const Cell = styled("div", { alignItems: "center", justifyContent: "center", maxWidth: "100%", - padding: theme.spacing[2], variants: { property: { "margin-top": { gridColumn: "2 / 5", gridRow: "1" }, From 60ccafa4816eb92d184de02fe899fdde3506dd3c Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 20 Mar 2025 22:54:43 +0000 Subject: [PATCH 5/7] autoscroll in inset --- .../features/style-panel/sections/position/inset-tooltip.tsx | 2 +- .../builder/features/style-panel/sections/shared/value-text.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/position/inset-tooltip.tsx b/apps/builder/app/builder/features/style-panel/sections/position/inset-tooltip.tsx index 611b7c354ab4..6590c295d195 100644 --- a/apps/builder/app/builder/features/style-panel/sections/position/inset-tooltip.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/position/inset-tooltip.tsx @@ -141,7 +141,7 @@ export const InsetTooltip = ({ } > {/* @todo show tooltip on focus */} -
{children}
+
{children}
); }; diff --git a/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx b/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx index 09fb9e2d7ebe..a3551f979e0a 100644 --- a/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx @@ -9,6 +9,7 @@ const Container = styled("button", { // fit-content is not needed for the "button" element, // leave it here in case of tag change width: "fit-content", + maxWidth: "100%", display: "flex", flexWrap: "nowrap", alignItems: "baseline", From 9840345b43907781801bdede0bcc23d6346acc52 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 20 Mar 2025 23:55:56 +0000 Subject: [PATCH 6/7] types --- .../builder/features/style-panel/shared/scroll-by-pointer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts b/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts index 9ad7cb61680c..16ef7e8543f9 100644 --- a/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts +++ b/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts @@ -41,7 +41,7 @@ export const scrollByPointer = () => { abortController.abort(reason); }; - const onMouseOver = (event: MouseEvent) => { + const onMouseOver = (event: React.MouseEvent) => { const element = event.currentTarget; if (element instanceof HTMLElement === false) { return; @@ -78,7 +78,7 @@ export const scrollByPointer = () => { ); }; - const onMouseOut = (event: MouseEvent) => { + const onMouseOut = (event: React.MouseEvent) => { abort("mouseout"); const element = event.currentTarget; if (element instanceof HTMLElement === false) { From 2c6412b91645bfafd13a650873ec719046cfa7f2 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 20 Mar 2025 23:58:19 +0000 Subject: [PATCH 7/7] add a comment --- .../builder/features/style-panel/shared/scroll-by-pointer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts b/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts index 16ef7e8543f9..52dec7cb6b4d 100644 --- a/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts +++ b/apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts @@ -34,6 +34,10 @@ const scrollAhead = (element: HTMLElement, clientX: number) => { // E.g. this is important for inputs, where the user might be interacting with text. const isFocused = (element: HTMLElement) => element === document.activeElement; +/** + * Scroll any element horizontally based on pointer position. + * Used in CSSValueInput and Spacing UI to show a string that is too long. + */ export const scrollByPointer = () => { let abortController = new AbortController();