Skip to content

Commit 09f19b5

Browse files
authored
experimental: Animations. Entry animations support (#5102)
## Description 1. What is this PR about (link the issue and add a short description) ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## 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: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent e5369ef commit 09f19b5

File tree

5 files changed

+132
-4
lines changed

5 files changed

+132
-4
lines changed

apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ import { keywordValues } from "@webstudio-is/css-data";
1515
import { useIds } from "~/shared/form-utils";
1616

1717
import type {
18+
DurationUnitValue,
1819
RangeUnitValue,
1920
ScrollAnimation,
2021
ViewAnimation,
2122
} from "@webstudio-is/sdk";
2223
import {
24+
durationUnitValueSchema,
2325
RANGE_UNITS,
2426
rangeUnitValueSchema,
2527
scrollAnimationSchema,
@@ -37,6 +39,7 @@ import {
3739
import { Keyframes } from "./animation-keyframes";
3840
import { humanizeString } from "~/shared/string-utils";
3941
import { Link2Icon, Link2UnlinkedIcon } from "@webstudio-is/icons";
42+
import { $availableUnitVariables } from "~/builder/features/style-panel/shared/model";
4043

4144
const fillModeDescriptions: Record<
4245
NonNullable<ViewAnimation["timing"]["fill"]>,
@@ -95,9 +98,11 @@ const RangeValueInput = ({
9598
id,
9699
value,
97100
onChange,
101+
disabled,
98102
}: {
99103
id: string;
100104
value: RangeUnitValue;
105+
disabled?: boolean;
101106
onChange: ((value: undefined, isEphemeral: true) => void) &
102107
((value: RangeUnitValue, isEphemeral: boolean) => void);
103108
}) => {
@@ -108,6 +113,7 @@ const RangeValueInput = ({
108113
return (
109114
<CssValueInput
110115
id={id}
116+
disabled={disabled}
111117
styleSource="default"
112118
value={value}
113119
/* marginLeft to allow negative values */
@@ -127,12 +133,13 @@ const RangeValueInput = ({
127133

128134
onChange(undefined, true);
129135
}}
130-
getOptions={() => []}
136+
getOptions={() => $availableUnitVariables.get()}
131137
onHighlight={() => {
132138
/* Nothing to Highlight */
133139
}}
134140
onChangeComplete={(event) => {
135141
const parsedValue = rangeUnitValueSchema.safeParse(event.value);
142+
136143
if (parsedValue.success) {
137144
onChange(parsedValue.data, false);
138145
setIntermediateValue(undefined);
@@ -184,6 +191,7 @@ const EasingInput = ({
184191
type: "keyword" as const,
185192
value,
186193
})),
194+
...$availableUnitVariables.get(),
187195
]}
188196
property="animation-timing-function"
189197
intermediateValue={intermediateValue}
@@ -209,6 +217,61 @@ const EasingInput = ({
209217
);
210218
};
211219

220+
const DurationInput = ({
221+
id,
222+
value,
223+
onChange,
224+
}: {
225+
id: string;
226+
value: DurationUnitValue | undefined;
227+
onChange: (
228+
value: DurationUnitValue | undefined,
229+
isEphemeral: boolean
230+
) => void;
231+
}) => {
232+
const [intermediateValue, setIntermediateValue] = useState<
233+
StyleValue | IntermediateStyleValue
234+
>();
235+
236+
return (
237+
<CssValueInput
238+
id={id}
239+
styleSource="default"
240+
value={value}
241+
placeholder="auto"
242+
property="animation-duration"
243+
intermediateValue={intermediateValue}
244+
onChange={(styleValue) => {
245+
setIntermediateValue(styleValue);
246+
}}
247+
getOptions={() => $availableUnitVariables.get()}
248+
onHighlight={() => {}}
249+
onChangeComplete={(event) => {
250+
const value = durationUnitValueSchema.safeParse(event.value);
251+
onChange(undefined, true);
252+
if (value.success) {
253+
onChange(value.data, false);
254+
setIntermediateValue(undefined);
255+
return;
256+
}
257+
258+
setIntermediateValue({
259+
type: "invalid",
260+
value: toValue(event.value),
261+
});
262+
}}
263+
onAbort={() => {
264+
onChange(undefined, true);
265+
}}
266+
onReset={() => {
267+
setIntermediateValue(undefined);
268+
onChange(undefined, false);
269+
onChange(undefined, true);
270+
}}
271+
/>
272+
);
273+
};
274+
212275
type AnimationPanelContentProps = {
213276
type: "scroll" | "view";
214277
value: ScrollAnimation | ViewAnimation;
@@ -249,6 +312,7 @@ export const AnimationPanelContent = ({
249312
"fill",
250313
"easing",
251314
"name",
315+
"duration",
252316
] as const);
253317

254318
const timelineRangeDescriptions =
@@ -259,6 +323,8 @@ export const AnimationPanelContent = ({
259323
const animationSchema =
260324
type === "scroll" ? scrollAnimationSchema : viewAnimationSchema;
261325

326+
const isRangeEndEnabled = value.timing.duration === undefined;
327+
262328
const handleChange = (rawValue: unknown, isEphemeral: boolean) => {
263329
if (rawValue === undefined) {
264330
onChange(undefined, true);
@@ -414,7 +480,9 @@ export const AnimationPanelContent = ({
414480
>
415481
<Label htmlFor={fieldIds.rangeStartName}>Range Start</Label>
416482
<div />
417-
<Label htmlFor={fieldIds.rangeEndName}>Range End</Label>
483+
<Label disabled={!isRangeEndEnabled} htmlFor={fieldIds.rangeEndName}>
484+
Range End
485+
</Label>
418486

419487
<Select
420488
id={fieldIds.rangeStartName}
@@ -515,6 +583,7 @@ export const AnimationPanelContent = ({
515583
</Grid>
516584
<Select
517585
id={fieldIds.rangeEndName}
586+
disabled={!isRangeEndEnabled}
518587
options={timelineRangeNames}
519588
getLabel={humanizeString}
520589
value={value.timing.rangeEnd?.[0] ?? timelineRangeNames[0]!}
@@ -614,6 +683,7 @@ export const AnimationPanelContent = ({
614683
<div />
615684
<RangeValueInput
616685
id={fieldIds.rangeEndValue}
686+
disabled={!isRangeEndEnabled}
617687
value={
618688
value.timing.rangeEnd?.[1] ?? {
619689
type: "unit",
@@ -646,6 +716,39 @@ export const AnimationPanelContent = ({
646716
/>
647717
</Grid>
648718

719+
<Grid
720+
gap={1}
721+
align={"center"}
722+
css={{
723+
gridTemplateColumns: "1fr 16px 1fr",
724+
paddingInline: theme.panel.paddingInline,
725+
}}
726+
>
727+
<Label htmlFor={fieldIds.duration}>Duration</Label>
728+
<div />
729+
<DurationInput
730+
id={fieldIds.duration}
731+
value={value.timing.duration}
732+
onChange={(duration, isEphemeral) => {
733+
if (duration === undefined && isEphemeral) {
734+
handleChange(undefined, true);
735+
return;
736+
}
737+
738+
handleChange(
739+
{
740+
...value,
741+
timing: {
742+
...value.timing,
743+
duration,
744+
},
745+
},
746+
isEphemeral
747+
);
748+
}}
749+
/>
750+
</Grid>
751+
649752
<Keyframes
650753
value={value.keyframes}
651754
onChange={(keyframes, isEphemeral) => {

packages/sdk/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export type {
3939
ScrollAnimation,
4040
ViewAnimation,
4141
InsetUnitValue,
42+
DurationUnitValue,
43+
TimeUnit,
4244
} from "./schema/animation-schema";
4345

4446
export {
@@ -48,5 +50,6 @@ export {
4850
rangeUnitValueSchema,
4951
animationKeyframeSchema,
5052
insetUnitValueSchema,
53+
durationUnitValueSchema,
5154
RANGE_UNITS,
5255
} from "./schema/animation-schema";

packages/sdk/src/schema/animation-schema.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,25 @@ export const rangeUnitValueSchema = z.union([
6969
type: z.literal("unparsed"),
7070
value: z.string(),
7171
}),
72+
z.object({
73+
type: z.literal("var"),
74+
value: z.string(),
75+
}),
76+
]);
77+
78+
export const TIME_UNITS = ["ms", "s"] as const;
79+
const timeUnitSchema = literalUnion(TIME_UNITS);
80+
81+
export const durationUnitValueSchema = z.union([
82+
z.object({
83+
type: z.literal("unit"),
84+
value: z.number(),
85+
unit: timeUnitSchema,
86+
}),
87+
z.object({
88+
type: z.literal("var"),
89+
value: z.string(),
90+
}),
7291
]);
7392

7493
// view-timeline-inset
@@ -101,6 +120,7 @@ export const keyframeEffectOptionsSchema = z.object({
101120
z.literal("both"),
102121
])
103122
.optional(), // FillMode
123+
duration: durationUnitValueSchema.optional(),
104124
});
105125

106126
// Scroll Named Range
@@ -214,6 +234,8 @@ export const isRangeUnit = (
214234
// Type exports
215235
export type RangeUnit = z.infer<typeof rangeUnitSchema>;
216236
export type RangeUnitValue = z.infer<typeof rangeUnitValueSchema>;
237+
export type DurationUnitValue = z.infer<typeof durationUnitValueSchema>;
238+
export type TimeUnit = z.infer<typeof timeUnitSchema>;
217239
export type KeyframeStyles = z.infer<typeof keyframeStylesSchema>;
218240
export type AnimationKeyframe = z.infer<typeof animationKeyframeSchema>;
219241
export type ScrollNamedRange = z.infer<typeof scrollNamedRangeSchema>;

packages/tsconfig/base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"display": "Default",
44
"compilerOptions": {
55
"module": "ES2022",
6-
"target": "ES2022",
6+
"target": "ES2023",
77
"esModuleInterop": true,
88
"forceConsistentCasingInFileNames": true,
99
"inlineSources": false,

0 commit comments

Comments
 (0)