Skip to content

Commit ecb23c0

Browse files
authored
feat: Search for advanced section in style panel (#4862)
#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
1 parent c3ac4d6 commit ecb23c0

File tree

2 files changed

+129
-57
lines changed

2 files changed

+129
-57
lines changed

apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx

Lines changed: 127 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { mergeRefs } from "@react-aria/utils";
12
import { lexer } from "css-tree";
23
import { colord } from "colord";
34
import {
@@ -6,8 +7,11 @@ import {
67
useEffect,
78
useRef,
89
useState,
10+
type ChangeEvent,
911
type ComponentProps,
12+
type KeyboardEvent,
1013
type ReactNode,
14+
type RefObject,
1115
} from "react";
1216
import { useStore } from "@nanostores/react";
1317
import { computed } from "nanostores";
@@ -26,6 +30,7 @@ import {
2630
InputField,
2731
Label,
2832
NestedInputButton,
33+
SearchField,
2934
SectionTitle,
3035
SectionTitleButton,
3136
SectionTitleLabel,
@@ -236,8 +241,9 @@ const AddProperty = forwardRef<
236241
onClose: () => void;
237242
onSubmit: (css: string) => void;
238243
onFocus: () => void;
244+
onBlur: () => void;
239245
}
240-
>(({ onClose, onSubmit, onFocus }, forwardedRef) => {
246+
>(({ onClose, onSubmit, onFocus, onBlur }, forwardedRef) => {
241247
const [item, setItem] = useState<SearchItem>({
242248
property: "",
243249
label: "",
@@ -309,11 +315,15 @@ const AddProperty = forwardRef<
309315
return (
310316
<ComboboxRoot open={combobox.isOpen}>
311317
<div {...combobox.getComboboxProps()}>
312-
<input type="submit" hidden />
313318
<ComboboxAnchor>
314319
<InputField
315320
{...inputProps}
321+
autoFocus
316322
onFocus={onFocus}
323+
onBlur={(event) => {
324+
inputProps.onBlur(event);
325+
onBlur();
326+
}}
317327
inputRef={forwardedRef}
318328
onKeyDown={handleKeyDown}
319329
placeholder="Add styles"
@@ -414,12 +424,14 @@ const AdvancedPropertyValue = ({
414424
autoFocus,
415425
property,
416426
onChangeComplete,
427+
inputRef: inputRefProp,
417428
}: {
418429
autoFocus?: boolean;
419430
property: StyleProperty;
420431
onChangeComplete: ComponentProps<
421432
typeof CssValueInputContainer
422433
>["onChangeComplete"];
434+
inputRef?: RefObject<HTMLInputElement>;
423435
}) => {
424436
const styleDecl = useComputedStyleDecl(property);
425437
const inputRef = useRef<HTMLInputElement>(null);
@@ -432,7 +444,7 @@ const AdvancedPropertyValue = ({
432444
const isColor = colord(toValue(styleDecl.usedValue)).isValid();
433445
return (
434446
<CssValueInputContainer
435-
inputRef={inputRef}
447+
inputRef={mergeRefs(inputRef, inputRefProp)}
436448
variant="chromeless"
437449
text="mono"
438450
fieldSizing="content"
@@ -559,13 +571,15 @@ const AdvancedProperty = memo(
559571
autoFocus,
560572
onChangeComplete,
561573
onReset,
574+
valueInputRef,
562575
}: {
563576
property: StyleProperty;
564577
autoFocus?: boolean;
565578
onReset?: () => void;
566579
onChangeComplete?: ComponentProps<
567580
typeof CssValueInputContainer
568581
>["onChangeComplete"];
582+
valueInputRef?: RefObject<HTMLInputElement>;
569583
}) => {
570584
const visibilityChangeEventSupported = useClientSupports(
571585
() => "oncontentvisibilityautostatechange" in document.body
@@ -636,6 +650,7 @@ const AdvancedProperty = memo(
636650
autoFocus={autoFocus}
637651
property={property}
638652
onChangeComplete={onChangeComplete}
653+
inputRef={valueInputRef}
639654
/>
640655
</Box>
641656
</>
@@ -650,75 +665,131 @@ export const Section = () => {
650665
const advancedProperties = useStore($advancedProperties);
651666
const [recentProperties, setRecentProperties] = useState<StyleProperty[]>([]);
652667
const addPropertyInputRef = useRef<HTMLInputElement>(null);
668+
const recentValueInputRef = useRef<HTMLInputElement>(null);
669+
const searchInputRef = useRef<HTMLInputElement>(null);
670+
const [searchProperties, setSearchProperties] =
671+
useState<Array<StyleProperty>>();
672+
const containerRef = useRef<HTMLDivElement>(null);
673+
const [minHeight, setMinHeight] = useState<number>(0);
653674

654-
const addRecentProperties = (properties: StyleProperty[]) => {
655-
setRecentProperties(
656-
Array.from(new Set([...recentProperties, ...properties]))
657-
);
675+
const currentProperties = searchProperties ?? advancedProperties;
676+
677+
const showRecentProperties =
678+
recentProperties.length > 0 && searchProperties === undefined;
679+
680+
const memorizeMinHeight = () => {
681+
setMinHeight(containerRef.current?.getBoundingClientRect().height ?? 0);
658682
};
659683

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

690+
const handleAbortSearch = () => {
691+
setMinHeight(0);
692+
setSearchProperties(undefined);
693+
};
694+
695+
const handleSubmitStyles = (cssText: string) => {
696+
setIsAdding(false);
697+
const styles = insertStyles(cssText);
698+
const insertedProperties = styles.map(({ property }) => property);
699+
setRecentProperties(
700+
Array.from(new Set([...recentProperties, ...insertedProperties]))
701+
);
702+
};
703+
704+
const handleSearch = (event: ChangeEvent<HTMLInputElement>) => {
705+
const search = event.target.value.trim();
706+
if (search === "") {
707+
return handleAbortSearch();
708+
}
709+
memorizeMinHeight();
710+
const matched = matchSorter(advancedProperties, search);
711+
setSearchProperties(matched);
712+
};
713+
714+
const handleAbortAddStyles = () => {
715+
setIsAdding(false);
716+
requestAnimationFrame(() => {
717+
// We are either focusing the last value input from the recent list if available or the search input.
718+
const element = recentValueInputRef.current ?? searchInputRef.current;
719+
element?.focus();
720+
});
721+
};
722+
666723
return (
667724
<AdvancedStyleSection
668725
label="Advanced"
669726
properties={advancedProperties}
670-
onAdd={showAddProperty}
727+
onAdd={handleShowAddStylesInput}
671728
>
672729
<Box css={{ paddingInline: theme.panel.paddingInline }}>
673-
{recentProperties.map((property, index, properties) => (
674-
<AdvancedProperty
675-
key={property}
676-
property={property}
677-
autoFocus={index === properties.length - 1}
678-
onChangeComplete={(event) => {
679-
if (event.type === "enter") {
680-
showAddProperty();
681-
}
682-
}}
683-
onReset={() => {
684-
setRecentProperties((properties) => {
685-
return properties.filter(
686-
(recentProperty) => recentProperty !== property
687-
);
688-
});
689-
}}
690-
/>
691-
))}
692-
<Box
693-
css={
694-
isAdding
695-
? { paddingTop: theme.spacing[3] }
696-
: // We hide it visually so you can tab into it to get shown.
697-
{ overflow: "hidden", height: 0 }
698-
}
699-
>
700-
<AddProperty
701-
onSubmit={(value) => {
702-
setIsAdding(false);
703-
const styles = insertStyles(value);
704-
const insertedProperties = styles.map(({ property }) => property);
705-
addRecentProperties(insertedProperties);
706-
}}
707-
onClose={() => {
708-
setIsAdding(false);
709-
}}
710-
onFocus={() => {
711-
if (isAdding === false) {
712-
showAddProperty();
713-
}
714-
}}
715-
ref={addPropertyInputRef}
716-
/>
717-
</Box>
730+
<SearchField
731+
inputRef={searchInputRef}
732+
onChange={handleSearch}
733+
onAbort={handleAbortSearch}
734+
/>
718735
</Box>
719-
{recentProperties.length > 0 && <Separator />}
720736
<Box css={{ paddingInline: theme.panel.paddingInline }}>
721-
{advancedProperties
737+
{showRecentProperties &&
738+
recentProperties.map((property, index, properties) => {
739+
const isLast = index === properties.length - 1;
740+
return (
741+
<AdvancedProperty
742+
valueInputRef={isLast ? recentValueInputRef : undefined}
743+
key={property}
744+
property={property}
745+
autoFocus={isLast}
746+
onChangeComplete={(event) => {
747+
if (event.type === "enter") {
748+
handleShowAddStylesInput();
749+
}
750+
}}
751+
onReset={() => {
752+
setRecentProperties((properties) => {
753+
return properties.filter(
754+
(recentProperty) => recentProperty !== property
755+
);
756+
});
757+
}}
758+
/>
759+
);
760+
})}
761+
{(showRecentProperties || isAdding) && (
762+
<Box
763+
style={
764+
isAdding
765+
? { paddingTop: theme.spacing[3] }
766+
: // We hide it visually so you can tab into it to get shown.
767+
{ overflow: "hidden", height: 0 }
768+
}
769+
>
770+
<AddProperty
771+
onSubmit={handleSubmitStyles}
772+
onClose={handleAbortAddStyles}
773+
onFocus={() => {
774+
if (isAdding === false) {
775+
handleShowAddStylesInput();
776+
}
777+
}}
778+
onBlur={() => {
779+
setIsAdding(false);
780+
}}
781+
ref={addPropertyInputRef}
782+
/>
783+
</Box>
784+
)}
785+
</Box>
786+
{showRecentProperties && <Separator />}
787+
<Box
788+
css={{ paddingInline: theme.panel.paddingInline }}
789+
style={{ minHeight }}
790+
ref={containerRef}
791+
>
792+
{currentProperties
722793
.filter((property) => recentProperties.includes(property) === false)
723794
.map((property) => (
724795
<AdvancedProperty key={property} property={property} />

packages/design-system/src/components/search-field.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { theme } from "../stitches.config";
1515
import { InputField } from "./input-field";
1616
import { SmallIconButton } from "./small-icon-button";
1717
import { Flex } from "./flex";
18+
import { mergeRefs } from "@react-aria/utils";
1819

1920
const SearchIconStyled = styled(SearchIcon, {
2021
// need to center icon vertically
@@ -61,7 +62,7 @@ const SearchFieldBase: ForwardRefRenderFunction<
6162
// brings native reset button
6263
type="text"
6364
value={value}
64-
inputRef={inputRef}
65+
inputRef={mergeRefs(inputRef, rest.inputRef)}
6566
prefix={<SearchIconStyled />}
6667
suffix={
6768
<Flex align="center" css={{ padding: theme.spacing[2] }}>

0 commit comments

Comments
 (0)