mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
experimental: support selecting style source and state (#3972)
Ref https://github.com/webstudio-is/webstudio/issues/3399 https://github.com/webstudio-is/webstudio/issues/1536 Here added support for selecting specific style source and state when computing styles. Matching states are now treated as less specific.
This commit is contained in:
1 parent
f60b9bddb1
commit
03aa922425
2 files changed
+238
-35
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { describe, expect, test } from "@jest/globals";
|
||||
import type { HtmlTags } from "html-tags";
|
||||
import {
|
||||
type Breakpoint,
|
||||
@@ -561,15 +561,12 @@ test("cascade value from matching style sources", () => {
|
||||
).toEqual({ type: "unit", unit: "px", value: 20 });
|
||||
});
|
||||
|
||||
test("cascade values with states as more specific", () => {
|
||||
test("cascade values with matching states", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
bodyLocal:hover {
|
||||
width: 20px;
|
||||
}
|
||||
bodyLocal {
|
||||
width: 10px;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="bodyLocal"></$.Body>,
|
||||
matchingStates: new Set([":hover"]),
|
||||
@@ -582,7 +579,7 @@ test("cascade values with states as more specific", () => {
|
||||
).toEqual({ type: "unit", unit: "px", value: 20 });
|
||||
});
|
||||
|
||||
test("ignore values from not matching states", () => {
|
||||
test("prefer stateless values over matching states", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
bodyLocal {
|
||||
@@ -591,6 +588,24 @@ test("ignore values from not matching states", () => {
|
||||
bodyLocal:hover {
|
||||
width: 20px;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="bodyLocal"></$.Body>,
|
||||
matchingStates: new Set([":hover"]),
|
||||
});
|
||||
const instanceSelector = ["body"];
|
||||
// value with state wins
|
||||
expect(
|
||||
getComputedStyleDecl({ model, instanceSelector, property: "width" })
|
||||
.usedValue
|
||||
).toEqual({ type: "unit", unit: "px", value: 10 });
|
||||
});
|
||||
|
||||
test("ignore values from not matching states", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
bodyLocal:hover {
|
||||
width: 20px;
|
||||
}
|
||||
bodyLocal:focus {
|
||||
width: 30px;
|
||||
}
|
||||
@@ -710,17 +725,17 @@ test("breakpoints are more specific than style sources", () => {
|
||||
).toEqual({ type: "unit", unit: "px", value: 20 });
|
||||
});
|
||||
|
||||
test("states are more specific than breakpoints", () => {
|
||||
test("breakpoints are more specific than matching states", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
bodyLocal:hover {
|
||||
width: 20px;
|
||||
}
|
||||
@media small {
|
||||
bodyLocal {
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
bodyLocal:hover {
|
||||
width: 20px;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="bodyLocal"></$.Body>,
|
||||
matchingBreakpoints: ["base", "small"],
|
||||
@@ -733,7 +748,7 @@ test("states are more specific than breakpoints", () => {
|
||||
instanceSelector: ["body"],
|
||||
property: "width",
|
||||
}).usedValue
|
||||
).toEqual({ type: "unit", unit: "px", value: 20 });
|
||||
).toEqual({ type: "unit", unit: "px", value: 10 });
|
||||
});
|
||||
|
||||
test("user styles are more specific than preset styles", () => {
|
||||
@@ -787,3 +802,186 @@ test("preset styles are more specific than browser styles", () => {
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "flex" });
|
||||
});
|
||||
|
||||
describe("selected style", () => {
|
||||
test("access selected style source value within cascade", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
token {
|
||||
color: red;
|
||||
}
|
||||
local {
|
||||
color: blue;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="token local"></$.Body>,
|
||||
});
|
||||
expect(
|
||||
getComputedStyleDecl({
|
||||
model,
|
||||
instanceSelector: ["body"],
|
||||
styleSourceId: "token",
|
||||
property: "color",
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "red" });
|
||||
});
|
||||
|
||||
test("fallback to previous style source", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
token {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="token local"></$.Body>,
|
||||
});
|
||||
expect(
|
||||
getComputedStyleDecl({
|
||||
model,
|
||||
instanceSelector: ["body"],
|
||||
styleSourceId: "local",
|
||||
property: "color",
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "red" });
|
||||
});
|
||||
|
||||
test("fallback to final style source", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
first {
|
||||
color: red;
|
||||
}
|
||||
local {
|
||||
color: blue;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="first second local"></$.Body>,
|
||||
});
|
||||
expect(
|
||||
getComputedStyleDecl({
|
||||
model,
|
||||
instanceSelector: ["body"],
|
||||
styleSourceId: "second",
|
||||
property: "color",
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "blue" });
|
||||
});
|
||||
|
||||
test("access selected state value", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
local {
|
||||
color: red;
|
||||
}
|
||||
local:hover {
|
||||
color: green;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="local"></$.Body>,
|
||||
});
|
||||
expect(
|
||||
getComputedStyleDecl({
|
||||
model,
|
||||
instanceSelector: ["body"],
|
||||
property: "color",
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "red" });
|
||||
expect(
|
||||
getComputedStyleDecl({
|
||||
model,
|
||||
instanceSelector: ["body"],
|
||||
state: ":hover",
|
||||
property: "color",
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "green" });
|
||||
});
|
||||
|
||||
test("fallback to stateless value", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
local {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="local"></$.Body>,
|
||||
});
|
||||
expect(
|
||||
getComputedStyleDecl({
|
||||
model,
|
||||
instanceSelector: ["body"],
|
||||
state: ":hover",
|
||||
property: "color",
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "red" });
|
||||
});
|
||||
|
||||
test("prefer stateless when states are matched but not selected", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
local {
|
||||
color: red;
|
||||
}
|
||||
local:hover {
|
||||
color: blue;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="local"></$.Body>,
|
||||
matchingStates: new Set([":hover"]),
|
||||
});
|
||||
expect(
|
||||
getComputedStyleDecl({
|
||||
model,
|
||||
instanceSelector: ["body"],
|
||||
property: "color",
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "red" });
|
||||
});
|
||||
|
||||
test("prefer selected state over matched one", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
local:hover {
|
||||
color: green;
|
||||
}
|
||||
local:focus {
|
||||
color: blue;
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="local"></$.Body>,
|
||||
matchingStates: new Set([":hover", ":focus"]),
|
||||
});
|
||||
expect(
|
||||
getComputedStyleDecl({
|
||||
model,
|
||||
instanceSelector: ["body"],
|
||||
state: ":hover",
|
||||
property: "color",
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "green" });
|
||||
});
|
||||
|
||||
test("prefer selected state over breakpoint", () => {
|
||||
const model = createModel({
|
||||
css: `
|
||||
local:hover {
|
||||
color: red;
|
||||
}
|
||||
@media small {
|
||||
local {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`,
|
||||
jsx: <$.Body ws:id="body" class="local"></$.Body>,
|
||||
matchingBreakpoints: ["base", "small"],
|
||||
});
|
||||
expect(
|
||||
getComputedStyleDecl({
|
||||
model,
|
||||
instanceSelector: ["body"],
|
||||
state: ":hover",
|
||||
property: "color",
|
||||
}).usedValue
|
||||
).toEqual({ type: "keyword", value: "red" });
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import type { StyleValue, StyleProperty } from "@webstudio-is/css-engine";
|
||||
import {
|
||||
type Breakpoint,
|
||||
type Instance,
|
||||
type StyleDecl,
|
||||
type StyleSourceSelections,
|
||||
type Styles,
|
||||
getStyleDeclKey,
|
||||
@@ -96,10 +97,14 @@ export const getPresetStyleDeclKey = ({
|
||||
const getCascadedValue = ({
|
||||
model,
|
||||
instanceId,
|
||||
styleSourceId: selectedStyleSourceId,
|
||||
state: selectedState,
|
||||
property,
|
||||
}: {
|
||||
model: StyleObjectModel;
|
||||
instanceId: Instance["id"];
|
||||
styleSourceId?: StyleDecl["styleSourceId"];
|
||||
state?: StyleDecl["state"];
|
||||
property: Property;
|
||||
}) => {
|
||||
const {
|
||||
@@ -127,6 +132,15 @@ const getCascadedValue = ({
|
||||
}
|
||||
}
|
||||
|
||||
// sort first matching states
|
||||
// then statesless
|
||||
// and selected state
|
||||
const states = new Set<undefined | string>(matchingStates);
|
||||
states.add(undefined);
|
||||
// move selected state in the end if already present in matching states
|
||||
states.delete(selectedState);
|
||||
states.add(selectedState);
|
||||
|
||||
// preset component styles
|
||||
if (component && tag) {
|
||||
// stateless
|
||||
@@ -136,7 +150,7 @@ const getCascadedValue = ({
|
||||
declaredValues.push({ value: styleValue });
|
||||
}
|
||||
// stateful
|
||||
for (const state of matchingStates) {
|
||||
for (const state of states) {
|
||||
const key = getPresetStyleDeclKey({ component, tag, state, property });
|
||||
const styleValue = presetStyles.get(key);
|
||||
if (styleValue) {
|
||||
@@ -146,25 +160,15 @@ const getCascadedValue = ({
|
||||
}
|
||||
|
||||
// user styles
|
||||
|
||||
// stateless
|
||||
const styleSourceIds = styleSourceSelections.get(instanceId)?.values ?? [];
|
||||
for (const breakpointId of matchingBreakpoints) {
|
||||
for (const styleSourceId of styleSourceIds) {
|
||||
const key = getStyleDeclKey({
|
||||
styleSourceId,
|
||||
breakpointId,
|
||||
property: property as StyleProperty,
|
||||
});
|
||||
const styleDecl = styles.get(key);
|
||||
if (styleDecl) {
|
||||
declaredValues.push({ value: styleDecl.value });
|
||||
}
|
||||
}
|
||||
const styleSourceIds = new Set(
|
||||
styleSourceSelections.get(instanceId)?.values ?? []
|
||||
);
|
||||
if (selectedStyleSourceId) {
|
||||
// move selected style source in the end
|
||||
styleSourceIds.delete(selectedStyleSourceId);
|
||||
styleSourceIds.add(selectedStyleSourceId);
|
||||
}
|
||||
|
||||
// stateful
|
||||
for (const state of matchingStates) {
|
||||
for (const state of states) {
|
||||
for (const breakpointId of matchingBreakpoints) {
|
||||
for (const styleSourceId of styleSourceIds) {
|
||||
const key = getStyleDeclKey({
|
||||
@@ -203,20 +207,19 @@ const customPropertyData = {
|
||||
/**
|
||||
* follow value processing specification
|
||||
* https://drafts.csswg.org/css-cascade-5/#value-stages
|
||||
*
|
||||
* @todo
|
||||
* - selected style source
|
||||
* - selected state
|
||||
*
|
||||
*/
|
||||
export const getComputedStyleDecl = ({
|
||||
model,
|
||||
instanceSelector,
|
||||
styleSourceId,
|
||||
state,
|
||||
property,
|
||||
customPropertiesGraph = new Map(),
|
||||
}: {
|
||||
model: StyleObjectModel;
|
||||
instanceSelector: InstanceSelector;
|
||||
styleSourceId?: StyleDecl["styleSourceId"];
|
||||
state?: StyleDecl["state"];
|
||||
property: Property;
|
||||
/**
|
||||
* for internal use only
|
||||
@@ -250,6 +253,8 @@ export const getComputedStyleDecl = ({
|
||||
const { cascadedValue } = getCascadedValue({
|
||||
model,
|
||||
instanceId,
|
||||
styleSourceId,
|
||||
state,
|
||||
property,
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user