Skip to content

Commit 1e86e76

Browse files
authored
feat: Autocomplete values in advanced panel (#4854)
#4816 ## Description - When adding a new property, you sometimes don't know the property name, e.g. you know you want to "center" it but you don't remember if its align-items, align-content or any other and you need to see the list. - Currently you can create a css variable "bla" and it will bypass validation, now it will validate - also try "color" esc autocomplete, enter - or "color:red" esc autocomplete enter Try typing "center" ## Steps for reproduction 1. click button 2. 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 d60d3c5 commit 1e86e76

File tree

1 file changed

+96
-40
lines changed
  • apps/builder/app/builder/features/style-panel/sections/advanced

1 file changed

+96
-40
lines changed

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

+96-40
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ import {
3838
import {
3939
parseCss,
4040
properties as propertiesData,
41+
keywordValues,
4142
propertyDescriptions,
43+
parseCssValue,
4244
} from "@webstudio-is/css-data";
4345
import {
46+
cssWideKeywords,
4447
hyphenateProperty,
4548
toValue,
4649
type StyleProperty,
@@ -117,7 +120,43 @@ const AdvancedStyleSection = (props: {
117120
);
118121
};
119122

120-
type SearchItem = { value: string; label: string };
123+
type SearchItem = { property: string; label: string; value?: string };
124+
125+
const autoCompleteItems: Array<SearchItem> = [];
126+
127+
const getAutocompleteItems = () => {
128+
if (autoCompleteItems.length > 0) {
129+
return autoCompleteItems;
130+
}
131+
for (const property in propertiesData) {
132+
autoCompleteItems.push({
133+
property,
134+
label: hyphenateProperty(property),
135+
});
136+
}
137+
138+
const ignoreValues = new Set([...cssWideKeywords, ...keywordValues.color]);
139+
140+
for (const property in keywordValues) {
141+
const values = keywordValues[property as keyof typeof keywordValues];
142+
for (const value of values) {
143+
if (ignoreValues.has(value)) {
144+
continue;
145+
}
146+
autoCompleteItems.push({
147+
property,
148+
value,
149+
label: `${hyphenateProperty(property)}: ${value}`,
150+
});
151+
}
152+
}
153+
154+
autoCompleteItems.sort((a, b) =>
155+
Intl.Collator().compare(a.property, b.property)
156+
);
157+
158+
return autoCompleteItems;
159+
};
121160

122161
const matchOrSuggestToCreate = (
123162
search: string,
@@ -127,36 +166,49 @@ const matchOrSuggestToCreate = (
127166
const matched = matchSorter(items, search, {
128167
keys: [itemToString],
129168
});
130-
const propertyName = search.trim();
169+
170+
// Limit the array to 100 elements
171+
matched.length = Math.min(matched.length, 100);
172+
173+
const property = search.trim();
131174
if (
132-
propertyName.startsWith("--") &&
133-
lexer.match("<custom-ident>", propertyName).matched
175+
property.startsWith("--") &&
176+
lexer.match("<custom-ident>", property).matched
134177
) {
135178
matched.unshift({
136-
value: propertyName,
137-
label: `Create "${propertyName}"`,
179+
property,
180+
label: `Create "${property}"`,
138181
});
139182
}
140183
// When there is no match we suggest to create a custom property.
141-
if (matched.length === 0) {
184+
if (
185+
matched.length === 0 &&
186+
lexer.match("<custom-ident>", `--${property}`).matched
187+
) {
142188
matched.unshift({
143-
value: `--${propertyName}`,
144-
label: `--${propertyName}`,
189+
property: `--${property}`,
190+
label: `--${property}: unset;`,
145191
});
146192
}
193+
147194
return matched;
148195
};
149196

150197
const getNewPropertyDescription = (item: null | SearchItem) => {
151198
let description: string | undefined = `Create CSS variable.`;
152-
if (item && item.value in propertyDescriptions) {
153-
description = propertyDescriptions[item.value];
199+
if (item && item.property in propertyDescriptions) {
200+
description = propertyDescriptions[item.property];
154201
}
155202
return <Box css={{ width: theme.spacing[28] }}>{description}</Box>;
156203
};
157204

158205
const insertStyles = (text: string) => {
159-
const parsedStyles = parseCss(`selector{${text}}`);
206+
let parsedStyles = parseCss(`selector{${text}}`);
207+
if (parsedStyles.length === 0) {
208+
// Try a single property without a value.
209+
parsedStyles = parseCss(`selector{${text}: unset}`);
210+
}
211+
160212
if (parsedStyles.length === 0) {
161213
return [];
162214
}
@@ -168,13 +220,6 @@ const insertStyles = (text: string) => {
168220
return parsedStyles;
169221
};
170222

171-
const sortedProperties = Object.keys(propertiesData)
172-
.sort(Intl.Collator().compare)
173-
.map((property) => ({
174-
value: property,
175-
label: hyphenateProperty(property),
176-
}));
177-
178223
/**
179224
*
180225
* Advanced search control supports following interactions
@@ -188,28 +233,46 @@ const sortedProperties = Object.keys(propertiesData)
188233
const AddProperty = forwardRef<
189234
HTMLInputElement,
190235
{
191-
onSelect: (value: StyleProperty) => void;
192236
onClose: () => void;
193-
onSubmit: (value: string) => void;
237+
onSubmit: (css: string) => void;
194238
onFocus: () => void;
195239
}
196-
>(({ onSelect, onClose, onSubmit, onFocus }, forwardedRef) => {
240+
>(({ onClose, onSubmit, onFocus }, forwardedRef) => {
197241
const [item, setItem] = useState<SearchItem>({
198-
value: "",
242+
property: "",
199243
label: "",
200244
});
245+
const highlightedItemRef = useRef<SearchItem>();
201246

202247
const combobox = useCombobox<SearchItem>({
203-
getItems: () => sortedProperties,
248+
getItems: getAutocompleteItems,
204249
itemToString: (item) => item?.label ?? "",
205250
value: item,
206251
defaultHighlightedIndex: 0,
207252
getItemProps: () => ({ text: "sentence" }),
208253
match: matchOrSuggestToCreate,
209-
onChange: (value) => setItem({ value: value ?? "", label: value ?? "" }),
254+
onChange: (value) => setItem({ property: value ?? "", label: value ?? "" }),
210255
onItemSelect: (item) => {
211256
clear();
212-
onSelect(item.value as StyleProperty);
257+
onSubmit(`${item.property}: ${item.value ?? "unset"}`);
258+
},
259+
onItemHighlight: (item) => {
260+
const previousHighlightedItem = highlightedItemRef.current;
261+
if (item?.value === undefined && previousHighlightedItem) {
262+
deleteProperty(previousHighlightedItem.property as StyleProperty, {
263+
isEphemeral: true,
264+
});
265+
highlightedItemRef.current = undefined;
266+
return;
267+
}
268+
269+
if (item?.value) {
270+
const value = parseCssValue(item.property as StyleProperty, item.value);
271+
setProperty(item.property as StyleProperty)(value, {
272+
isEphemeral: true,
273+
});
274+
highlightedItemRef.current = item;
275+
}
213276
},
214277
});
215278

@@ -219,7 +282,7 @@ const AddProperty = forwardRef<
219282
const inputProps = combobox.getInputProps();
220283

221284
const clear = () => {
222-
setItem({ value: "", label: "" });
285+
setItem({ property: "", label: "" });
223286
};
224287

225288
const handleKeys = (event: KeyboardEvent) => {
@@ -229,7 +292,7 @@ const AddProperty = forwardRef<
229292
}
230293
if (event.key === "Enter") {
231294
clear();
232-
onSubmit(item.value);
295+
onSubmit(item.property);
233296
return;
234297
}
235298
if (event.key === "Escape") {
@@ -363,6 +426,7 @@ const AdvancedPropertyValue = ({
363426
useEffect(() => {
364427
if (autoFocus) {
365428
inputRef.current?.focus();
429+
inputRef.current?.select();
366430
}
367431
}, [autoFocus]);
368432
const isColor = colord(toValue(styleDecl.usedValue)).isValid();
@@ -560,7 +624,10 @@ const AdvancedProperty = memo(
560624
<Text
561625
variant="mono"
562626
// Improves the visual separation of value from the property.
563-
css={{ textIndent: "-0.5ch", fontWeight: "bold" }}
627+
css={{
628+
textIndent: "-0.5ch",
629+
fontWeight: "bold",
630+
}}
564631
>
565632
:
566633
</Text>
@@ -631,17 +698,6 @@ export const Section = () => {
631698
}
632699
>
633700
<AddProperty
634-
onSelect={(property) => {
635-
setIsAdding(false);
636-
const isNew = advancedProperties.includes(property) === false;
637-
if (isNew) {
638-
setProperty(property)(
639-
{ type: "guaranteedInvalid" },
640-
{ listed: true }
641-
);
642-
}
643-
addRecentProperties([property]);
644-
}}
645701
onSubmit={(value) => {
646702
setIsAdding(false);
647703
const styles = insertStyles(value);

0 commit comments

Comments
 (0)