diff --git a/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx b/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx index c93070891e3a..4a4fa7f3eac2 100644 --- a/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx +++ b/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx @@ -7,10 +7,13 @@ 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, @@ -18,6 +21,7 @@ import { 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 = [ { @@ -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< @@ -43,7 +49,11 @@ const OffsetInput = ({ return ( []} unitOptions={unitOptions} intermediateValue={intermediateValue} @@ -107,9 +117,11 @@ const OffsetInput = ({ const Keyframe = ({ value, + offsetPlaceholder, onChange, }: { value: AnimationKeyframe; + offsetPlaceholder: number; onChange: (value: AnimationKeyframe | undefined) => void; }) => { const ids = useIds(["offset"]); @@ -133,6 +145,7 @@ const Keyframe = ({ { onChange({ ...value, offset }); }} @@ -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) + ); + + if (keyframes.length !== keyRefs.current.length) { + keyRefs.current = Array.from( + { length: keyframes.length }, + (_, index) => index + ); + } + + const offsets = calcOffsets(keyframes); + return ( - - + + - onChange([...keyframes, { offset: undefined, styles: {} }]) - } + onClick={() => { + onChange([...keyframes, { offset: undefined, styles: {} }]); + keyRefs.current = [...keyRefs.current, keyframes.length]; + }} > + + + + + + + {keyframes.map((value, index) => ( + + {index > 0 && } + { + 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) => ( - - - { - if (newValue === undefined) { - const newValues = [...keyframes]; - newValues.splice(index, 1); - onChange(newValues); - return; - } - - const newValues = [...keyframes]; - newValues[index] = newValue; - onChange(newValues); - }} - /> - - ))} + onChange(newValues); + }} + /> + + ))} + + ); }; diff --git a/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx b/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx index 96239dc4ce95..60f1fb18978e 100644 --- a/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx +++ b/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx @@ -1,5 +1,6 @@ import { Box, + Flex, Grid, Label, Select, @@ -40,12 +41,12 @@ const fillModeDescriptions: Record< NonNullable, 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< @@ -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 ( - - + +