mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 14:03:33 +09:00
refactor: use longhand properties to save transition values in style-panel (#3584)
## Description fixes #3567 Use long-hand properties to save `transition` values in the style-panel. ## Steps for reproduction - Add some layers in transition panel in UI. - Delete some layers - Swap some layers ## TODO - [ ] Allow `show/hide` for each layer. - [ ] Add tests for all the layer handlers for transition. ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview @TrySound i made some refactor to use individual properties as long-hands instead of short-hand property. I am still yet to add tests for the new handlers, since they need to be handled in a different way now for transitions. But you can still play around with the current setup. ## 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: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests
This commit is contained in:
1 parent
9a39c24502
commit
ce2dad417d
11 files changed
+847
-117
No files matched your search
@@ -10,7 +10,13 @@ import {
|
||||
} from "@webstudio-is/css-engine";
|
||||
import { keywordValues } from "./__generated__/keyword-values";
|
||||
import { units } from "./__generated__/units";
|
||||
import { parseFilter, parseShadow, parseTransition } from "./property-parsers";
|
||||
import {
|
||||
isTransitionLongHandProperty,
|
||||
parseFilter,
|
||||
parseShadow,
|
||||
parseTransition,
|
||||
parseTransitionLonghands,
|
||||
} from "./property-parsers";
|
||||
import { camelCase } from "change-case";
|
||||
|
||||
export const cssTryParseValue = (input: string) => {
|
||||
@@ -92,6 +98,10 @@ export const parseCssValue = (
|
||||
return parseTransition(input);
|
||||
}
|
||||
|
||||
if (isTransitionLongHandProperty(property)) {
|
||||
return parseTransitionLonghands(property, input);
|
||||
}
|
||||
|
||||
if (
|
||||
ast != null &&
|
||||
ast.type === "Value" &&
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type {
|
||||
KeywordValue,
|
||||
StyleValue,
|
||||
TupleValue,
|
||||
UnitValue,
|
||||
} from "@webstudio-is/css-engine";
|
||||
@@ -12,19 +11,17 @@ export const isTimingFunction = (timing: string) => {
|
||||
};
|
||||
|
||||
export type ExtractedTransitionProperties = {
|
||||
property?: KeywordValue | null;
|
||||
timing?: KeywordValue | null;
|
||||
delay?: StyleValue | null;
|
||||
duration?: StyleValue | null;
|
||||
property?: KeywordValue;
|
||||
timing?: KeywordValue;
|
||||
delay?: UnitValue;
|
||||
duration?: UnitValue;
|
||||
};
|
||||
|
||||
export const extractTransitionProperties = (
|
||||
transition: TupleValue
|
||||
): ExtractedTransitionProperties => {
|
||||
let property: KeywordValue | null = null;
|
||||
let timing: KeywordValue | null = null;
|
||||
let duration: UnitValue | null = null;
|
||||
let delay: UnitValue | null = null;
|
||||
let property: KeywordValue | undefined;
|
||||
let timing: KeywordValue | undefined;
|
||||
|
||||
const keywordValues: KeywordValue[] = [];
|
||||
const unitValues: UnitValue[] = [];
|
||||
@@ -43,11 +40,11 @@ export const extractTransitionProperties = (
|
||||
timing = keywordValues[0];
|
||||
} else {
|
||||
property = keywordValues[0];
|
||||
timing = keywordValues[1] ?? null;
|
||||
timing = keywordValues[1] ?? undefined;
|
||||
}
|
||||
|
||||
duration = unitValues[0] ?? null;
|
||||
delay = unitValues[1] ?? null;
|
||||
const duration: UnitValue | undefined = unitValues[0] ?? undefined;
|
||||
const delay: UnitValue | undefined = unitValues[1] ?? undefined;
|
||||
|
||||
return {
|
||||
property,
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { describe, expect, test } from "@jest/globals";
|
||||
import { expect, test } from "@jest/globals";
|
||||
|
||||
import { parseTransition } from "./transition";
|
||||
|
||||
describe("parseTransition", () => {
|
||||
test("parses value and returns invalid when used a invalid color", () => {
|
||||
expect(parseTransition(`10px 10px 5px foo`)).toMatchInlineSnapshot(`
|
||||
test("parses value and returns invalid when used a invalid color", () => {
|
||||
expect(parseTransition(`10px 10px 5px foo`)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "invalid",
|
||||
"value": "10px 10px 5px foo",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test("parse value and returns multiple valid layers", () => {
|
||||
expect(parseTransition("color .2s, text-shadow .2s"))
|
||||
.toMatchInlineSnapshot(`
|
||||
test("parse value and returns multiple valid layers", () => {
|
||||
expect(parseTransition("color .2s, text-shadow .2s")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "layers",
|
||||
"value": [
|
||||
@@ -49,10 +47,10 @@ describe("parseTransition", () => {
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test("parse value and returns valid layer", () => {
|
||||
expect(parseTransition("opacity 200ms ease 0s")).toMatchInlineSnapshot(`
|
||||
test("parse value and returns valid layer", () => {
|
||||
expect(parseTransition("opacity 200ms ease 0s")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "layers",
|
||||
"value": [
|
||||
@@ -82,27 +80,24 @@ describe("parseTransition", () => {
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error if any custom transition proeprty is used", () => {
|
||||
const transition = parseTransition(
|
||||
"all 200ms ease 0s, --foo 200ms ease 0s"
|
||||
);
|
||||
expect(transition).toMatchInlineSnapshot(`
|
||||
test("throws error if any custom transition proeprty is used", () => {
|
||||
const transition = parseTransition("all 200ms ease 0s, --foo 200ms ease 0s");
|
||||
expect(transition).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "invalid",
|
||||
"value": "all 200ms ease 0s, --foo 200ms ease 0s",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error if any custom transition timing function is used", () => {
|
||||
const transition = parseTransition("all 200ms custom-function 0s");
|
||||
expect(transition).toMatchInlineSnapshot(`
|
||||
test("throws error if any custom transition timing function is used", () => {
|
||||
const transition = parseTransition("all 200ms custom-function 0s");
|
||||
expect(transition).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "invalid",
|
||||
"value": "all 200ms custom-function 0s",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,63 @@
|
||||
import * as csstree from "css-tree";
|
||||
import type {
|
||||
InvalidValue,
|
||||
KeywordValue,
|
||||
LayerValueItem,
|
||||
LayersValue,
|
||||
TupleValue,
|
||||
TupleValueItem,
|
||||
Unit,
|
||||
UnparsedValue,
|
||||
UnitValue,
|
||||
} from "@webstudio-is/css-engine";
|
||||
import { animatableProperties } from "../";
|
||||
import { isTimingFunction } from "./transition-property-extractor";
|
||||
import { cssTryParseValue } from "../parse-css-value";
|
||||
import { kebabCase } from "change-case";
|
||||
|
||||
type AnimatableProperty = (typeof animatableProperties)[number];
|
||||
|
||||
export const transitionLongHandProperties = [
|
||||
"transitionProperty",
|
||||
"transitionTimingFunction",
|
||||
"transitionDelay",
|
||||
"transitionDuration",
|
||||
] as const;
|
||||
|
||||
export const commonTransitionProperties = [
|
||||
"all",
|
||||
"opacity",
|
||||
"margin",
|
||||
"padding",
|
||||
"border",
|
||||
"transform",
|
||||
"filter",
|
||||
"flex",
|
||||
"background-color",
|
||||
];
|
||||
|
||||
export const isAnimatableProperty = (
|
||||
property: string
|
||||
): property is AnimatableProperty => {
|
||||
return animatableProperties.some((item) => item === property);
|
||||
if (property === "all") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [...commonTransitionProperties, ...animatableProperties].some(
|
||||
(item) => item === property
|
||||
);
|
||||
};
|
||||
|
||||
export const isTransitionLongHandProperty = (
|
||||
property: string
|
||||
): property is (typeof transitionLongHandProperties)[number] => {
|
||||
return transitionLongHandProperties.some((item) => item === property);
|
||||
};
|
||||
|
||||
export const isValidTransitionValue = (
|
||||
value: LayerValueItem
|
||||
): value is KeywordValue | UnitValue => {
|
||||
return value.type === "keyword" || value.type === "unit";
|
||||
};
|
||||
|
||||
export const parseTransition = (
|
||||
@@ -34,7 +77,6 @@ export const parseTransition = (
|
||||
}
|
||||
|
||||
const cssAst = cssTryParseValue(tokenStream);
|
||||
|
||||
if (cssAst === undefined) {
|
||||
return {
|
||||
type: "invalid",
|
||||
@@ -119,3 +161,46 @@ export const parseTransition = (
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const parseTransitionLonghands = (
|
||||
property: (typeof transitionLongHandProperties)[number],
|
||||
input: string
|
||||
): LayersValue | InvalidValue | UnparsedValue => {
|
||||
const cssAst = cssTryParseValue(input);
|
||||
if (cssAst === undefined) {
|
||||
return {
|
||||
type: "invalid",
|
||||
value: input,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = csstree.lexer.matchProperty(kebabCase(property), cssAst);
|
||||
if (parsed.error) {
|
||||
return {
|
||||
type: "invalid",
|
||||
value: input,
|
||||
};
|
||||
}
|
||||
|
||||
const layers: LayersValue = { type: "layers", value: [] };
|
||||
csstree.walk(cssAst, (node) => {
|
||||
if (node.type === "Identifier") {
|
||||
if (
|
||||
isAnimatableProperty(node.name) === true ||
|
||||
isTimingFunction(node.name) === true
|
||||
) {
|
||||
layers.value.push({ type: "keyword", value: node.name });
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === "Dimension") {
|
||||
layers.value.push({
|
||||
type: "unit",
|
||||
value: Number(node.value),
|
||||
unit: node.unit as Unit,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return layers.value.length > 0 ? layers : { type: "invalid", value: input };
|
||||
};
|
||||
Reference in new issue
Block a user