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:
Jaya Krishna Namburu authored and GitHub committed 2024-06-23 13:23:56 +02:00
1 parent 9a39c24502
commit ce2dad417d
11 files changed
+847 -117

No files matched your search

+11 -1
View File
@@ -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 };
};