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={