Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions apps/builder/app/builder/features/style-panel/shared/model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { propertiesData } from "@webstudio-is/css-data";
import {
compareMedia,
hyphenateProperty,
matchMedia,
toVarFallback,
type CssProperty,
type StyleValue,
Expand Down Expand Up @@ -41,6 +42,7 @@ import {
type InstancePath,
} from "~/shared/awareness";
import type { InstanceSelector } from "~/shared/tree-utils";
import { $canvasWidth } from "~/builder/shared/nano-states";

const $presetStyles = computed($registeredComponentMetas, (metas) => {
const presetStyles = new Map<string, StyleValue>();
Expand Down Expand Up @@ -111,16 +113,28 @@ const $instanceComponents = computed(
);

export const $matchingBreakpoints = computed(
[$breakpoints, $selectedBreakpoint],
(breakpoints, selectedBreakpoint) => {
const sortedBreakpoints = Array.from(breakpoints.values()).sort(
compareMedia
);
[$breakpoints, $selectedBreakpoint, $canvasWidth],
(breakpoints, selectedBreakpoint, canvasWidth) => {
// zero is not correct, need to use current width for base breakpoint
// add always add base
const selectedWidth =
selectedBreakpoint?.minWidth ??
selectedBreakpoint?.maxWidth ??
canvasWidth ??
0;
const sortedBreakpoints = Array.from(breakpoints.values())
.sort(compareMedia)
.sort((left, right) => {
// put selected breakpoint always in the end
// to make style from matching breakpoints remote
const leftScore = left.id === selectedBreakpoint?.id ? 1 : 0;
const rightScore = right.id === selectedBreakpoint?.id ? 1 : 0;
return leftScore - rightScore;
});
const matchingBreakpoints: Breakpoint["id"][] = [];
for (const breakpoint of sortedBreakpoints) {
matchingBreakpoints.push(breakpoint.id);
if (breakpoint.id === selectedBreakpoint?.id) {
break;
if (matchMedia(breakpoint, selectedWidth)) {
matchingBreakpoints.push(breakpoint.id);
}
}
return matchingBreakpoints;
Expand Down Expand Up @@ -240,6 +254,8 @@ const $model = computed(
}
);

export { $model as _$model };

export const $computedStyleDeclarations = computed(
[
$model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3003,6 +3003,11 @@ describe("Styles", () => {
background-color: rgba(0, 208, 255, 1)
}
}
@media all and (min-width: 1280px) {
Div Block 2 {
background-color: rgba(0, 255, 128, 1)
}
}
@media all and (max-width: 991px) {
Div Block 2 {
background-color: rgba(68, 0, 255, 1)
Expand All @@ -3017,11 +3022,6 @@ describe("Styles", () => {
Div Block 2 {
background-color: rgba(255, 0, 4, 1)
}
}
@media all and (min-width: 1280px) {
Div Block 2 {
background-color: rgba(0, 255, 128, 1)
}
}"
`);
});
Expand Down
159 changes: 159 additions & 0 deletions apps/builder/app/shared/style-object-model.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ import {
getComputedStyleDecl,
getPresetStyleDeclKey,
} from "./style-object-model";
import {
$breakpoints,
$selectedBreakpointId,
$styles,
$styleSourceSelections,
} from "./nano-states";
import { _$model } from "~/builder/features/style-panel/shared/model";

/**
* Create model fixture with a few features
Expand Down Expand Up @@ -1608,3 +1615,155 @@ describe("style value source", () => {
});
});
});

describe("compute matching breakpoints", () => {
test("min-width only breakpoints", () => {
const model = createModel({
css: `
@media mobile {
bodyLocal {
color: red;
}
}
`,
jsx: <$.Body ws:id="body" ws:tag="body" class="bodyLocal"></$.Body>,
});
$styles.set(model.styles);
$styleSourceSelections.set(model.styleSourceSelections);
$breakpoints.set(
new Map([
["desktop", { id: "desktop", label: "", minWidth: 991 }],
["mobile", { id: "mobile", label: "", minWidth: 479 }],
["base", { id: "base", label: "" }],
])
);
$selectedBreakpointId.set("desktop");
expect(
getComputedStyleDecl({
model: _$model.get(),
instanceSelector: ["body"],
property: "color",
}).computedValue
).toEqual({
type: "keyword",
value: "red",
});
});

test("max-width only breakpoints", () => {
const model = createModel({
css: `
@media mobile {
bodyLocal {
color: red;
}
}
`,
jsx: <$.Body ws:id="body" ws:tag="body" class="bodyLocal"></$.Body>,
});
$styles.set(model.styles);
$styleSourceSelections.set(model.styleSourceSelections);
$breakpoints.set(
new Map([
["base", { id: "base", label: "" }],
["mobile", { id: "mobile", label: "", maxWidth: 479 }],
["desktop", { id: "desktop", label: "", maxWidth: 991 }],
])
);
$selectedBreakpointId.set("desktop");
expect(
getComputedStyleDecl({
model: _$model.get(),
instanceSelector: ["body"],
property: "color",
}).computedValue
).toEqual({
type: "keyword",
value: "black",
});
});

test("mixed min-width and max-width with selected min-width", () => {
const model = createModel({
css: `
@media mobile {
bodyLocal {
color: red;
}
}
`,
jsx: <$.Body ws:id="body" ws:tag="body" class="bodyLocal"></$.Body>,
});
$styles.set(model.styles);
$styleSourceSelections.set(model.styleSourceSelections);
$breakpoints.set(
new Map([
["desktop", { id: "desktop", label: "", minWidth: 991 }],
["base", { id: "base", label: "" }],
["mobile", { id: "mobile", label: "", maxWidth: 479 }],
])
);
$selectedBreakpointId.set("desktop");
expect(
getComputedStyleDecl({
model: _$model.get(),
instanceSelector: ["body"],
property: "color",
}).computedValue
).toEqual({
type: "keyword",
value: "black",
});
$selectedBreakpointId.set("base");
expect(
getComputedStyleDecl({
model: _$model.get(),
instanceSelector: ["body"],
property: "color",
}).source
).toEqual(
expect.objectContaining({ name: "remote", breakpointId: "mobile" })
);
});

test("mixed min-width and max-width with selected max-width", () => {
const model = createModel({
css: `
@media desktop {
bodyLocal {
color: red;
}
}
`,
jsx: <$.Body ws:id="body" ws:tag="body" class="bodyLocal"></$.Body>,
});
$styles.set(model.styles);
$styleSourceSelections.set(model.styleSourceSelections);
$breakpoints.set(
new Map([
["desktop", { id: "desktop", label: "", minWidth: 991 }],
["base", { id: "base", label: "" }],
["mobile", { id: "mobile", label: "", maxWidth: 479 }],
])
);
$selectedBreakpointId.set("mobile");
expect(
getComputedStyleDecl({
model: _$model.get(),
instanceSelector: ["body"],
property: "color",
}).computedValue
).toEqual({
type: "keyword",
value: "black",
});
$selectedBreakpointId.set("base");
expect(
getComputedStyleDecl({
model: _$model.get(),
instanceSelector: ["body"],
property: "color",
}).source
).toEqual(expect.objectContaining({ name: "default" }));
});
});
34 changes: 20 additions & 14 deletions packages/css-engine/src/core/compare-media.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,31 @@ describe("Compare media", () => {
});

test("mixed max and min", () => {
const initial = [
expect(
[
{},
{ maxWidth: 991 },
{ maxWidth: 479 },
{ maxWidth: 767 },
{ minWidth: 1440 },
{ minWidth: 1280 },
{ minWidth: 1920 },
].toSorted(compareMedia)
).toStrictEqual([
{},
{ maxWidth: 991 },
{ maxWidth: 479 },
{ maxWidth: 767 },
{ minWidth: 1440 },
{ minWidth: 1280 },
{ minWidth: 1440 },
{ minWidth: 1920 },
];
const expected = [
{},
{ maxWidth: 991 },
{ maxWidth: 767 },
{ maxWidth: 479 },
{ minWidth: 1280 },
{ minWidth: 1440 },
{ minWidth: 1920 },
];
const sorted = initial.sort(compareMedia);
expect(sorted).toStrictEqual(expected);
]);
// test both directions of sorting
expect(
[{ maxWidth: 479 }, { minWidth: 991 }, {}].toSorted(compareMedia)
).toStrictEqual([{}, { minWidth: 991 }, { maxWidth: 479 }]);
expect(
[{ minWidth: 991 }, {}, { maxWidth: 479 }].toSorted(compareMedia)
).toStrictEqual([{}, { minWidth: 991 }, { maxWidth: 479 }]);
});
});
7 changes: 7 additions & 0 deletions packages/css-engine/src/core/compare-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export const compareMedia = (
return optionB.maxWidth - optionA.maxWidth;
}

if (optionA.minWidth !== undefined && optionB.maxWidth !== undefined) {
return optionB.maxWidth - optionA.minWidth;
}
if (optionA.maxWidth !== undefined && optionB.minWidth !== undefined) {
return optionB.minWidth - optionA.maxWidth;
}

// Media with maxWith should render before minWith just to have the same sorting visually in the UI as in CSSOM.
return "minWidth" in optionA ? 1 : -1;
};
2 changes: 1 addition & 1 deletion packages/tsconfig/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"display": "Default",
"compilerOptions": {
"module": "ES2022",
"target": "ES2022",
"target": "ES2023",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
Expand Down