Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ import {
Label,
Separator,
Tooltip,
ScrollArea,
theme,
Box,
} from "@webstudio-is/design-system";
import { MinusIcon, PlusIcon } from "@webstudio-is/icons";
import type { AnimationKeyframe } from "@webstudio-is/sdk";
import { Fragment, useMemo, useState } from "react";
import { Fragment, useMemo, useRef, useState } from "react";
import {
CssValueInput,
type IntermediateStyleValue,
} from "~/builder/features/style-panel/shared/css-value-input";
import { toKebabCase } from "~/builder/features/style-panel/shared/keyword-utils";
import { CodeEditor } from "~/builder/shared/code-editor";
import { useIds } from "~/shared/form-utils";
import { calcOffsets, findInsertionIndex, moveItem } from "./keyframe-helpers";

const unitOptions = [
{
Expand All @@ -30,10 +34,12 @@ const unitOptions = [
const OffsetInput = ({
id,
value,
placeholder,
onChange,
}: {
id: string;
value: number | undefined;
placeholder: number;
onChange: (value: number | undefined) => void;
}) => {
const [intermediateValue, setIntermediateValue] = useState<
Expand All @@ -43,7 +49,11 @@ const OffsetInput = ({
return (
<CssValueInput
id={id}
placeholder="auto"
placeholder={
value === undefined
? `auto (${Math.round(placeholder * 1000) / 10}%)`
: "auto"
}
getOptions={() => []}
unitOptions={unitOptions}
intermediateValue={intermediateValue}
Expand Down Expand Up @@ -107,9 +117,11 @@ const OffsetInput = ({

const Keyframe = ({
value,
offsetPlaceholder,
onChange,
}: {
value: AnimationKeyframe;
offsetPlaceholder: number;
onChange: (value: AnimationKeyframe | undefined) => void;
}) => {
const ids = useIds(["offset"]);
Expand All @@ -133,6 +145,7 @@ const Keyframe = ({
<OffsetInput
id={ids.offset}
value={value.offset}
placeholder={offsetPlaceholder}
onChange={(offset) => {
onChange({ ...value, offset });
}}
Expand Down Expand Up @@ -176,43 +189,97 @@ export const Keyframes = ({
}) => {
const ids = useIds(["addKeyframe"]);

// To preserve focus on children swap
const keyRefs = useRef(
Array.from({ length: keyframes.length }, (_, index) => index)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting one, I never thought one can make array like this vs Array.from(new Array(keyframes.length))

);

if (keyframes.length !== keyRefs.current.length) {
keyRefs.current = Array.from(
{ length: keyframes.length },
(_, index) => index
);
}

const offsets = calcOffsets(keyframes);

return (
<Grid gap={2}>
<Grid gap={1} align={"center"} css={{ gridTemplateColumns: "1fr auto" }}>
<Grid
css={{
minHeight: 0,
}}
gap={1}
>
<Grid
gap={1}
align={"center"}
css={{
paddingInline: theme.panel.paddingInline,
gridTemplateColumns: "1fr auto",
}}
>
<Label htmlFor={ids.addKeyframe}>
<Text variant={"titles"}>Keyframes</Text>
</Label>
<IconButton
id={ids.addKeyframe}
onClick={() =>
onChange([...keyframes, { offset: undefined, styles: {} }])
}
onClick={() => {
onChange([...keyframes, { offset: undefined, styles: {} }]);
keyRefs.current = [...keyRefs.current, keyframes.length];
}}
>
<PlusIcon />
</IconButton>
</Grid>
<Box
css={{
paddingInline: theme.panel.paddingInline,
}}
>
<Separator />
</Box>

<ScrollArea>
<Grid gap={2} css={{ padding: theme.panel.padding }}>
{keyframes.map((value, index) => (
<Fragment key={keyRefs.current[index]}>
{index > 0 && <Separator />}
<Keyframe
key={keyRefs.current[index]}
value={value}
offsetPlaceholder={offsets[index]}
onChange={(newValue) => {
if (newValue === undefined) {
const newValues = [...keyframes];
newValues.splice(index, 1);
onChange(newValues);
return;
}

let newValues = [...keyframes];
newValues[index] = newValue;

const { offset } = newValue;
if (offset === undefined) {
onChange(newValues);
return;
}

const insertionIndex = findInsertionIndex(newValues, index);
newValues = moveItem(newValues, index, insertionIndex);
keyRefs.current = moveItem(
keyRefs.current,
index,
insertionIndex
);

{keyframes.map((value, index) => (
<Fragment key={index}>
<Separator />
<Keyframe
key={index}
value={value}
onChange={(newValue) => {
if (newValue === undefined) {
const newValues = [...keyframes];
newValues.splice(index, 1);
onChange(newValues);
return;
}

const newValues = [...keyframes];
newValues[index] = newValue;
onChange(newValues);
}}
/>
</Fragment>
))}
onChange(newValues);
}}
/>
</Fragment>
))}
</Grid>
</ScrollArea>
</Grid>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Box,
Flex,
Grid,
Label,
Select,
Expand Down Expand Up @@ -40,12 +41,12 @@ const fillModeDescriptions: Record<
NonNullable<ViewAnimation["timing"]["fill"]>,
string
> = {
none: "No animation is applied before or after the active period",
forwards:
"The animation state is applied after the active period. Prefered for Out Animations",
both: "The animation state is applied before and after the active period. Set if unsure whether it's In or Out.",
backwards:
"The animation state is applied before the active period. Prefered for In Animations",
both: "The animation state is applied before and after the active period",
forwards:
"The animation state is applied after the active period. Prefered for Out Animations",
none: "No animation is applied before or after the active period.",
};

const fillModeNames = Object.keys(fillModeDescriptions) as NonNullable<
Expand Down Expand Up @@ -223,9 +224,22 @@ export const AnimationPanelContent = ({ onChange, value, type }: Props) => {
toast.error("Animation schema is incompatible, try fix");
};

// Flex is used to allow the Keyframes to overflow without setting
// gridTemplateRows: auto auto 1fr
return (
<Grid gap="2" css={{ padding: theme.panel.padding }}>
<Grid gap={1} align={"center"} css={{ gridTemplateColumns: "1fr 1fr" }}>
<Flex
gap="2"
direction="column"
css={{
maxHeight: "60dvh",
paddingBottom: theme.panel.paddingBlock,
}}
>
<Grid
gap={1}
align={"center"}
css={{ gridTemplateColumns: "1fr 1fr", padding: theme.panel.padding }}
>
<Label htmlFor={fieldIds.fill}>Fill Mode</Label>
<Label htmlFor={fieldIds.easing}>Easing</Label>
<Select
Expand Down Expand Up @@ -270,7 +284,11 @@ export const AnimationPanelContent = ({ onChange, value, type }: Props) => {
}}
/>
</Grid>
<Grid gap={1} align={"center"} css={{ gridTemplateColumns: "2fr 1fr" }}>
<Grid
gap={1}
align={"center"}
css={{ gridTemplateColumns: "2fr 1fr", padding: theme.panel.padding }}
>
<Label htmlFor={fieldIds.rangeStartName}>Range Start</Label>
<Label htmlFor={fieldIds.rangeStartValue}>Value</Label>

Expand Down Expand Up @@ -405,6 +423,6 @@ export const AnimationPanelContent = ({ onChange, value, type }: Props) => {
value={value.keyframes}
onChange={(keyframes) => handleChange({ ...value, keyframes })}
/>
</Grid>
</Flex>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const animationTypes: AnimationAction["type"][] = Object.keys(
) as AnimationAction["type"][];

const defaultActionValue: AnimationAction = {
type: "scroll",
type: "view",
animations: [],
};

Expand Down
Loading