Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const getCssText = (
return;
}
result.push(`/* ${comment} */`);
result.push(generateStyleMap({ style: mergeStyles(style) }));
result.push(generateStyleMap(mergeStyles(style)));
};

add("Style Sources", sourceStyles);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { lexer } from "css-tree";
import { forwardRef, useRef, useState, type KeyboardEvent } from "react";
import { matchSorter } from "match-sorter";
import {
Expand All @@ -24,11 +23,14 @@ import {
} from "@webstudio-is/css-data";
import {
cssWideKeywords,
generateStyleMap,
hyphenateProperty,
toValue,
type StyleProperty,
} from "@webstudio-is/css-engine";
import { deleteProperty, setProperty } from "../../shared/use-style-data";
import { composeEventHandlers } from "~/shared/event-utils";
import { parseStyleInput } from "./parse-style-input";

type SearchItem = { property: string; label: string; value?: string };

Expand Down Expand Up @@ -88,25 +90,24 @@ const matchOrSuggestToCreate = (
// Limit the array to 100 elements
matched.length = Math.min(matched.length, 100);

const property = search.trim();
if (
property.startsWith("--") &&
lexer.match("<custom-ident>", property).matched
) {
matched.unshift({
property,
label: `Create "${property}"`,
});
}
// When there is no match we suggest to create a custom property.
if (
matched.length === 0 &&
lexer.match("<custom-ident>", `--${property}`).matched
) {
matched.unshift({
property: `--${property}`,
label: `--${property}: unset;`,
});
if (matched.length === 0) {
const parsedStyles = parseStyleInput(search);
// When parsedStyles is more than one, user entered a shorthand.
// We will suggest to insert their shorthand first.
if (parsedStyles.length > 1) {
matched.push({
property: search,
label: `Create "${search}"`,
});
}
// Now we will suggest to insert each longhand separately.
for (const style of parsedStyles) {
matched.push({
property: style.property,
value: toValue(style.value),
label: `Create "${generateStyleMap(new Map([[style.property, style.value]]))}"`,
});
}
}

return matched;
Expand All @@ -122,7 +123,7 @@ const matchOrSuggestToCreate = (
* paste css declarations
*
*/
export const AddStylesInput = forwardRef<
export const AddStyleInput = forwardRef<
HTMLInputElement,
{
onClose: () => void;
Expand All @@ -147,7 +148,14 @@ export const AddStylesInput = forwardRef<
onChange: (value) => setItem({ property: value ?? "", label: value ?? "" }),
onItemSelect: (item) => {
clear();
onSubmit(`${item.property}: ${item.value ?? "unset"}`);
// When there is no value, property can be:
// - property without value: gap
// - declaration with value: gap: 10px
// - block: gap: 10px; margin: 20px;
if (item.value === undefined) {
return onSubmit(item.property);
}
onSubmit(`${item.property}: ${item.value}`);
},
onItemHighlight: (item) => {
const previousHighlightedItem = highlightedItemRef.current;
Expand Down Expand Up @@ -207,6 +215,17 @@ export const AddStylesInput = forwardRef<
handleDelete,
]);

const handleBlur = composeEventHandlers([
inputProps.onBlur,
() => {
// When user clicks on a combobox item, input will receive blur event,
// but we don't want that to be handled upstream because input may get hidden without click getting handled.
if (combobox.isOpen === false) {
onBlur();
}
},
]);

return (
<ComboboxRoot open={combobox.isOpen}>
<div {...combobox.getComboboxProps()}>
Expand All @@ -215,10 +234,7 @@ export const AddStylesInput = forwardRef<
{...inputProps}
autoFocus
onFocus={onFocus}
onBlur={(event) => {
inputProps.onBlur(event);
onBlur();
}}
onBlur={handleBlur}
inputRef={forwardedRef}
onKeyDown={handleKeyDown}
placeholder="Add styles"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
theme,
Tooltip,
} from "@webstudio-is/design-system";
import { parseCss, propertyDescriptions } from "@webstudio-is/css-data";
import { propertyDescriptions } from "@webstudio-is/css-data";
import {
hyphenateProperty,
toValue,
Expand Down Expand Up @@ -55,7 +55,8 @@ import { useClientSupports } from "~/shared/client-supports";
import { CopyPasteMenu, propertyContainerAttribute } from "./copy-paste-menu";
import { $advancedStyles } from "./stores";
import { $settings } from "~/builder/shared/client-settings";
import { AddStylesInput } from "./add-styles-input";
import { AddStyleInput } from "./add-style-input";
import { parseStyleInput } from "./parse-style-input";

// Only here to keep the same section module interface
export const properties = [];
Expand Down Expand Up @@ -97,13 +98,8 @@ const AdvancedStyleSection = (props: {
);
};

const insertStyles = (text: string) => {
let parsedStyles = parseCss(`selector{${text}}`);
if (parsedStyles.length === 0) {
// Try a single property without a value.
parsedStyles = parseCss(`selector{${text}: unset}`);
}

const insertStyles = (css: string) => {
const parsedStyles = parseStyleInput(css);
if (parsedStyles.length === 0) {
return [];
}
Expand Down Expand Up @@ -386,9 +382,10 @@ export const Section = () => {
setRecentProperties(
Array.from(new Set([...recentProperties, ...insertedProperties]))
);
return styles;
};

const handleShowAddStylesInput = () => {
const handleShowAddStyleInput = () => {
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();
Expand Down Expand Up @@ -434,7 +431,7 @@ export const Section = () => {
<AdvancedStyleSection
label="Advanced"
properties={advancedProperties}
onAdd={handleShowAddStylesInput}
onAdd={handleShowAddStyleInput}
>
<Box css={{ paddingInline: theme.panel.paddingInline }}>
<SearchField
Expand All @@ -460,7 +457,7 @@ export const Section = () => {
autoFocus={isLast}
onChangeComplete={(event) => {
if (event.type === "enter") {
handleShowAddStylesInput();
handleShowAddStyleInput();
}
}}
onReset={() => {
Expand All @@ -482,15 +479,17 @@ export const Section = () => {
{ overflow: "hidden", height: 0 }
}
>
<AddStylesInput
<AddStyleInput
onSubmit={(cssText: string) => {
setIsAdding(false);
handleInsertStyles(cssText);
const styles = handleInsertStyles(cssText);
if (styles.length > 0) {
setIsAdding(false);
}
}}
onClose={handleAbortAddStyles}
onFocus={() => {
if (isAdding === false) {
handleShowAddStylesInput();
handleShowAddStyleInput();
}
}}
onBlur={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const CopyPasteMenu = ({
}
}

const css = generateStyleMap({ style: mergeStyles(currentStyleMap) });
const css = generateStyleMap(mergeStyles(currentStyleMap));
navigator.clipboard.writeText(css);
};

Expand All @@ -59,7 +59,7 @@ export const CopyPasteMenu = ({
return;
}
const style = new Map([[property, value]]);
const css = generateStyleMap({ style });
const css = generateStyleMap(style);
navigator.clipboard.writeText(css);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, test, expect } from "vitest";
import { parseStyleInput } from "./parse-style-input";

describe("parseStyleInput", () => {
test("parses custom property", () => {
const result = parseStyleInput("--custom-color");
expect(result).toEqual([
{
selector: "selector",
property: "--custom-color",
value: { type: "unset", value: "" },
},
]);
});

test("parses regular property", () => {
const result = parseStyleInput("color");
expect(result).toEqual([
{
selector: "selector",
property: "color",
value: { type: "unset", value: "" },
},
]);
});

test("trims whitespace", () => {
const result = parseStyleInput(" color ");
expect(result).toEqual([
{
selector: "selector",
property: "color",
value: { type: "unset", value: "" },
},
]);
});

test("handles unparsable regular property", () => {
const result = parseStyleInput("notapro perty");
expect(result).toEqual([]);
});

test("converts unknown property to custom property assuming user forgot to add --", () => {
const result = parseStyleInput("notaproperty");
expect(result).toEqual([
{
selector: "selector",
property: "--notaproperty",
value: { type: "unset", value: "" },
},
]);
});

test("parses single property-value pair", () => {
const result = parseStyleInput("color: red");
expect(result).toEqual([
{
selector: "selector",
property: "color",
value: { type: "keyword", value: "red" },
},
]);
});

test("parses multiple property-value pairs", () => {
const result = parseStyleInput("color: red; display: block");
expect(result).toEqual([
{
selector: "selector",
property: "color",
value: { type: "keyword", value: "red" },
},
{
selector: "selector",
property: "display",
value: { type: "keyword", value: "block" },
},
]);
});

test("parses multiple property-value pairs, one is invalid", () => {
const result = parseStyleInput("color: red; somethinginvalid: block");
expect(result).toEqual([
{
selector: "selector",
property: "color",
value: { type: "keyword", value: "red" },
},
{
selector: "selector",
property: "--somethinginvalid",
value: { type: "unparsed", value: "block" },
},
]);
});

test("parses custom property with value", () => {
const result = parseStyleInput("--custom-color: red");
expect(result).toEqual([
{
selector: "selector",
property: "--custom-color",
value: { type: "unparsed", value: "red" },
},
]);
});

test("handles malformed style block", () => {
const result = parseStyleInput("color: red; invalid;");
expect(result).toEqual([
{
selector: "selector",
property: "color",
value: { type: "keyword", value: "red" },
},
]);
});
});
Loading
Loading