Skip to content

Commit

Permalink
feat: Search for advanced section in style panel (#4862)
Browse files Browse the repository at this point in the history
#4805

## Description

1. start searching
2. abort with esc
3. try in default as well as in advanced mode
4. try with recent changes and without

## Steps for reproduction

1. click button
5. 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 Feb 14, 2025
1 parent c3ac4d6 commit ecb23c0
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mergeRefs } from "@react-aria/utils";
import { lexer } from "css-tree";
import { colord } from "colord";
import {
Expand All @@ -6,8 +7,11 @@ import {
useEffect,
useRef,
useState,
type ChangeEvent,
type ComponentProps,
type KeyboardEvent,
type ReactNode,
type RefObject,
} from "react";
import { useStore } from "@nanostores/react";
import { computed } from "nanostores";
Expand All @@ -26,6 +30,7 @@ import {
InputField,
Label,
NestedInputButton,
SearchField,
SectionTitle,
SectionTitleButton,
SectionTitleLabel,
Expand Down Expand Up @@ -236,8 +241,9 @@ const AddProperty = forwardRef<
onClose: () => void;
onSubmit: (css: string) => void;
onFocus: () => void;
onBlur: () => void;
}
>(({ onClose, onSubmit, onFocus }, forwardedRef) => {
>(({ onClose, onSubmit, onFocus, onBlur }, forwardedRef) => {
const [item, setItem] = useState<SearchItem>({
property: "",
label: "",
Expand Down Expand Up @@ -309,11 +315,15 @@ const AddProperty = forwardRef<
return (
<ComboboxRoot open={combobox.isOpen}>
<div {...combobox.getComboboxProps()}>
<input type="submit" hidden />
<ComboboxAnchor>
<InputField
{...inputProps}
autoFocus
onFocus={onFocus}
onBlur={(event) => {
inputProps.onBlur(event);
onBlur();
}}
inputRef={forwardedRef}
onKeyDown={handleKeyDown}
placeholder="Add styles"
Expand Down Expand Up @@ -414,12 +424,14 @@ const AdvancedPropertyValue = ({
autoFocus,
property,
onChangeComplete,
inputRef: inputRefProp,
}: {
autoFocus?: boolean;
property: StyleProperty;
onChangeComplete: ComponentProps<
typeof CssValueInputContainer
>["onChangeComplete"];
inputRef?: RefObject<HTMLInputElement>;
}) => {
const styleDecl = useComputedStyleDecl(property);
const inputRef = useRef<HTMLInputElement>(null);
Expand All @@ -432,7 +444,7 @@ const AdvancedPropertyValue = ({
const isColor = colord(toValue(styleDecl.usedValue)).isValid();
return (
<CssValueInputContainer
inputRef={inputRef}
inputRef={mergeRefs(inputRef, inputRefProp)}
variant="chromeless"
text="mono"
fieldSizing="content"
Expand Down Expand Up @@ -559,13 +571,15 @@ const AdvancedProperty = memo(
autoFocus,
onChangeComplete,
onReset,
valueInputRef,
}: {
property: StyleProperty;
autoFocus?: boolean;
onReset?: () => void;
onChangeComplete?: ComponentProps<
typeof CssValueInputContainer
>["onChangeComplete"];
valueInputRef?: RefObject<HTMLInputElement>;
}) => {
const visibilityChangeEventSupported = useClientSupports(
() => "oncontentvisibilityautostatechange" in document.body
Expand Down Expand Up @@ -636,6 +650,7 @@ const AdvancedProperty = memo(
autoFocus={autoFocus}
property={property}
onChangeComplete={onChangeComplete}
inputRef={valueInputRef}
/>
</Box>
</>
Expand All @@ -650,75 +665,131 @@ export const Section = () => {
const advancedProperties = useStore($advancedProperties);
const [recentProperties, setRecentProperties] = useState<StyleProperty[]>([]);
const addPropertyInputRef = useRef<HTMLInputElement>(null);
const recentValueInputRef = useRef<HTMLInputElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const [searchProperties, setSearchProperties] =
useState<Array<StyleProperty>>();
const containerRef = useRef<HTMLDivElement>(null);
const [minHeight, setMinHeight] = useState<number>(0);

const addRecentProperties = (properties: StyleProperty[]) => {
setRecentProperties(
Array.from(new Set([...recentProperties, ...properties]))
);
const currentProperties = searchProperties ?? advancedProperties;

const showRecentProperties =
recentProperties.length > 0 && searchProperties === undefined;

const memorizeMinHeight = () => {
setMinHeight(containerRef.current?.getBoundingClientRect().height ?? 0);
};

const showAddProperty = () => {
const handleShowAddStylesInput = () => {
setIsAdding(true);
// User can click twice on the add button, so we need to focus the input on the second click after autoFocus isn't working.
addPropertyInputRef.current?.focus();
};

const handleAbortSearch = () => {
setMinHeight(0);
setSearchProperties(undefined);
};

const handleSubmitStyles = (cssText: string) => {
setIsAdding(false);
const styles = insertStyles(cssText);
const insertedProperties = styles.map(({ property }) => property);
setRecentProperties(
Array.from(new Set([...recentProperties, ...insertedProperties]))
);
};

const handleSearch = (event: ChangeEvent<HTMLInputElement>) => {
const search = event.target.value.trim();
if (search === "") {
return handleAbortSearch();
}
memorizeMinHeight();
const matched = matchSorter(advancedProperties, search);
setSearchProperties(matched);
};

const handleAbortAddStyles = () => {
setIsAdding(false);
requestAnimationFrame(() => {
// We are either focusing the last value input from the recent list if available or the search input.
const element = recentValueInputRef.current ?? searchInputRef.current;
element?.focus();
});
};

return (
<AdvancedStyleSection
label="Advanced"
properties={advancedProperties}
onAdd={showAddProperty}
onAdd={handleShowAddStylesInput}
>
<Box css={{ paddingInline: theme.panel.paddingInline }}>
{recentProperties.map((property, index, properties) => (
<AdvancedProperty
key={property}
property={property}
autoFocus={index === properties.length - 1}
onChangeComplete={(event) => {
if (event.type === "enter") {
showAddProperty();
}
}}
onReset={() => {
setRecentProperties((properties) => {
return properties.filter(
(recentProperty) => recentProperty !== property
);
});
}}
/>
))}
<Box
css={
isAdding
? { paddingTop: theme.spacing[3] }
: // We hide it visually so you can tab into it to get shown.
{ overflow: "hidden", height: 0 }
}
>
<AddProperty
onSubmit={(value) => {
setIsAdding(false);
const styles = insertStyles(value);
const insertedProperties = styles.map(({ property }) => property);
addRecentProperties(insertedProperties);
}}
onClose={() => {
setIsAdding(false);
}}
onFocus={() => {
if (isAdding === false) {
showAddProperty();
}
}}
ref={addPropertyInputRef}
/>
</Box>
<SearchField
inputRef={searchInputRef}
onChange={handleSearch}
onAbort={handleAbortSearch}
/>
</Box>
{recentProperties.length > 0 && <Separator />}
<Box css={{ paddingInline: theme.panel.paddingInline }}>
{advancedProperties
{showRecentProperties &&
recentProperties.map((property, index, properties) => {
const isLast = index === properties.length - 1;
return (
<AdvancedProperty
valueInputRef={isLast ? recentValueInputRef : undefined}
key={property}
property={property}
autoFocus={isLast}
onChangeComplete={(event) => {
if (event.type === "enter") {
handleShowAddStylesInput();
}
}}
onReset={() => {
setRecentProperties((properties) => {
return properties.filter(
(recentProperty) => recentProperty !== property
);
});
}}
/>
);
})}
{(showRecentProperties || isAdding) && (
<Box
style={
isAdding
? { paddingTop: theme.spacing[3] }
: // We hide it visually so you can tab into it to get shown.
{ overflow: "hidden", height: 0 }
}
>
<AddProperty
onSubmit={handleSubmitStyles}
onClose={handleAbortAddStyles}
onFocus={() => {
if (isAdding === false) {
handleShowAddStylesInput();
}
}}
onBlur={() => {
setIsAdding(false);
}}
ref={addPropertyInputRef}
/>
</Box>
)}
</Box>
{showRecentProperties && <Separator />}
<Box
css={{ paddingInline: theme.panel.paddingInline }}
style={{ minHeight }}
ref={containerRef}
>
{currentProperties
.filter((property) => recentProperties.includes(property) === false)
.map((property) => (
<AdvancedProperty key={property} property={property} />
Expand Down
3 changes: 2 additions & 1 deletion packages/design-system/src/components/search-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { theme } from "../stitches.config";
import { InputField } from "./input-field";
import { SmallIconButton } from "./small-icon-button";
import { Flex } from "./flex";
import { mergeRefs } from "@react-aria/utils";

const SearchIconStyled = styled(SearchIcon, {
// need to center icon vertically
Expand Down Expand Up @@ -61,7 +62,7 @@ const SearchFieldBase: ForwardRefRenderFunction<
// brings native reset button
type="text"
value={value}
inputRef={inputRef}
inputRef={mergeRefs(inputRef, rest.inputRef)}
prefix={<SearchIconStyled />}
suffix={
<Flex align="center" css={{ padding: theme.spacing[2] }}>
Expand Down

0 comments on commit ecb23c0

Please sign in to comment.