Skip to content

Commit

Permalink
feat: Auto scroll CSS Value inputs when value is too long just by hov…
Browse files Browse the repository at this point in the history
…ering with the mouse (#4502)

## Description

1. When not focused will scroll
2. Only active on fields with scroll potential

https://share.descript.com/view/x0E72z2aFlS

## Steps for reproduction

1. click button
3. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
  • Loading branch information
kof authored Dec 4, 2024
1 parent b2137c1 commit c57d308
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,43 @@ export const WithUnits = () => {
</Flex>
);
};

export const AutoScroll = () => {
const [value, setValue] = React.useState<StyleValue>({
type: "var",
value: "start-test-test-test-test-test-test-test-end",
});

const [intermediateValue, setIntermediateValue] = React.useState<
StyleValue | IntermediateStyleValue
>();

return (
<Flex css={{ width: 100 }}>
<CssValueInput
styleSource="preset"
property="alignItems"
value={value}
intermediateValue={intermediateValue}
onChange={(newValue) => {
setIntermediateValue(newValue);
}}
onHighlight={(value) => {
action("onHighlight")(value);
}}
onChangeComplete={({ value }) => {
// on blur, select, enter etc.
setValue(value);
setIntermediateValue(undefined);
action("onChangeComplete")(value);
}}
onAbort={() => {
action("onAbort")();
}}
onReset={() => {
action("onReset")();
}}
/>
</Flex>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
useEffect,
useRef,
useState,
useMemo,
type ComponentProps,
} from "react";
import { useUnitSelect } from "./unit-select";
Expand Down Expand Up @@ -294,6 +295,97 @@ const itemToString = (item: CssValueInputValue | null) => {
return toValue(item);
};

const scrollAhead = ({ target, clientX }: MouseEvent) => {
const element = target as HTMLInputElement;
// Get the scrollable width of the input element
const scrollWidth = element.scrollWidth;
const visibleWidth = element.clientWidth;

if (scrollWidth === visibleWidth) {
// 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) * (scrollWidth - visibleWidth);

// 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) {
// Nothing to scroll.
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] });

/**
Expand Down Expand Up @@ -724,6 +816,13 @@ export const CssValueInput = ({
handleMetaEnter
);

const { abort, ...autoScrollProps } = useMemo(() => {
return getAutoScrollProps();
}, []);
useEffect(() => {
return () => abort("unmount");
}, [abort]);

return (
<ComboboxRoot open={isOpen}>
<Box {...getComboboxProps()}>
Expand All @@ -734,6 +833,7 @@ export const CssValueInput = ({
aria-disabled={ariaDisabled}
fieldSizing={fieldSizing}
{...inputProps}
{...autoScrollProps}
onFocus={() => {
const isFocused = document.activeElement === inputRef.current;
if (isFocused) {
Expand Down

0 comments on commit c57d308

Please sign in to comment.