From ecb23c0216fdb316eda77ff98d070da7a38b7efa Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 14 Feb 2025 18:59:49 +0000 Subject: [PATCH] feat: Search for advanced section in style panel (#4862) https://github.com/webstudio-is/webstudio/issues/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 --- .../sections/advanced/advanced.tsx | 183 ++++++++++++------ .../src/components/search-field.tsx | 3 +- 2 files changed, 129 insertions(+), 57 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index ab24471461..f02ee698ec 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -1,3 +1,4 @@ +import { mergeRefs } from "@react-aria/utils"; import { lexer } from "css-tree"; import { colord } from "colord"; import { @@ -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"; @@ -26,6 +30,7 @@ import { InputField, Label, NestedInputButton, + SearchField, SectionTitle, SectionTitleButton, SectionTitleLabel, @@ -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({ property: "", label: "", @@ -309,11 +315,15 @@ const AddProperty = forwardRef< return (
- { + inputProps.onBlur(event); + onBlur(); + }} inputRef={forwardedRef} onKeyDown={handleKeyDown} placeholder="Add styles" @@ -414,12 +424,14 @@ const AdvancedPropertyValue = ({ autoFocus, property, onChangeComplete, + inputRef: inputRefProp, }: { autoFocus?: boolean; property: StyleProperty; onChangeComplete: ComponentProps< typeof CssValueInputContainer >["onChangeComplete"]; + inputRef?: RefObject; }) => { const styleDecl = useComputedStyleDecl(property); const inputRef = useRef(null); @@ -432,7 +444,7 @@ const AdvancedPropertyValue = ({ const isColor = colord(toValue(styleDecl.usedValue)).isValid(); return ( ["onChangeComplete"]; + valueInputRef?: RefObject; }) => { const visibilityChangeEventSupported = useClientSupports( () => "oncontentvisibilityautostatechange" in document.body @@ -636,6 +650,7 @@ const AdvancedProperty = memo( autoFocus={autoFocus} property={property} onChangeComplete={onChangeComplete} + inputRef={valueInputRef} /> @@ -650,75 +665,131 @@ export const Section = () => { const advancedProperties = useStore($advancedProperties); const [recentProperties, setRecentProperties] = useState([]); const addPropertyInputRef = useRef(null); + const recentValueInputRef = useRef(null); + const searchInputRef = useRef(null); + const [searchProperties, setSearchProperties] = + useState>(); + const containerRef = useRef(null); + const [minHeight, setMinHeight] = useState(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) => { + 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 ( - {recentProperties.map((property, index, properties) => ( - { - if (event.type === "enter") { - showAddProperty(); - } - }} - onReset={() => { - setRecentProperties((properties) => { - return properties.filter( - (recentProperty) => recentProperty !== property - ); - }); - }} - /> - ))} - - { - setIsAdding(false); - const styles = insertStyles(value); - const insertedProperties = styles.map(({ property }) => property); - addRecentProperties(insertedProperties); - }} - onClose={() => { - setIsAdding(false); - }} - onFocus={() => { - if (isAdding === false) { - showAddProperty(); - } - }} - ref={addPropertyInputRef} - /> - + - {recentProperties.length > 0 && } - {advancedProperties + {showRecentProperties && + recentProperties.map((property, index, properties) => { + const isLast = index === properties.length - 1; + return ( + { + if (event.type === "enter") { + handleShowAddStylesInput(); + } + }} + onReset={() => { + setRecentProperties((properties) => { + return properties.filter( + (recentProperty) => recentProperty !== property + ); + }); + }} + /> + ); + })} + {(showRecentProperties || isAdding) && ( + + { + if (isAdding === false) { + handleShowAddStylesInput(); + } + }} + onBlur={() => { + setIsAdding(false); + }} + ref={addPropertyInputRef} + /> + + )} + + {showRecentProperties && } + + {currentProperties .filter((property) => recentProperties.includes(property) === false) .map((property) => ( diff --git a/packages/design-system/src/components/search-field.tsx b/packages/design-system/src/components/search-field.tsx index ef6ad02129..63408b95cd 100644 --- a/packages/design-system/src/components/search-field.tsx +++ b/packages/design-system/src/components/search-field.tsx @@ -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 @@ -61,7 +62,7 @@ const SearchFieldBase: ForwardRefRenderFunction< // brings native reset button type="text" value={value} - inputRef={inputRef} + inputRef={mergeRefs(inputRef, rest.inputRef)} prefix={} suffix={