Skip to content

Commit 10a4c70

Browse files
authored
experimental: Show animation is running is Nav Tree (#5023)
## Description Show eye icon (in primary color) if Node is Animating because of Selection or if it's Pinned. <img width="757" alt="image" src="https://github.com/user-attachments/assets/41597955-6974-49c4-96a9-708480e4d644" /> ## 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 9eb8a03 commit 10a4c70

File tree

9 files changed

+100
-17
lines changed

9 files changed

+100
-17
lines changed

apps/builder/app/builder/features/navigator/navigator-tree.tsx

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { mergeRefs } from "@react-aria/utils";
55
import { useStore } from "@nanostores/react";
66
import {
77
Box,
8+
keyframes,
89
rawTheme,
910
ScrollArea,
1011
SmallIconButton,
@@ -19,7 +20,10 @@ import {
1920
TreeSortableItem,
2021
type TreeDropTarget,
2122
} from "@webstudio-is/design-system";
22-
import { showAttribute } from "@webstudio-is/react-sdk";
23+
import {
24+
animationCanPlayOnCanvasAttribute,
25+
showAttribute,
26+
} from "@webstudio-is/react-sdk";
2327
import {
2428
ROOT_INSTANCE_ID,
2529
collectionComponent,
@@ -49,6 +53,7 @@ import {
4953
$selectedInstanceSelector,
5054
getIndexedInstanceId,
5155
type ItemDropTarget,
56+
$propValuesByInstanceSelectorWithMemoryProps,
5257
} from "~/shared/nano-states";
5358
import type { InstanceSelector } from "~/shared/tree-utils";
5459
import { serverSyncStore } from "~/shared/sync";
@@ -282,12 +287,27 @@ const handleExpand = (item: TreeItem, isExpanded: boolean, all: boolean) => {
282287
$expandedItems.set(expandedItems);
283288
};
284289

290+
const pulse = keyframes({
291+
"0%": { fillOpacity: 0 },
292+
"100%": { fillOpacity: 1 },
293+
});
294+
295+
const AnimatedEyeOpenIcon = styled(EyeOpenIcon, {
296+
"& .ws-eye-open-pupil": {
297+
transformOrigin: "center",
298+
animation: `${pulse} 1.5s ease-in-out infinite alternate`,
299+
fill: "currentColor",
300+
},
301+
});
302+
285303
const ShowToggle = ({
286304
instance,
287305
value,
306+
isAnimating,
288307
}: {
289308
instance: Instance;
290309
value: boolean;
310+
isAnimating: boolean;
291311
}) => {
292312
// descendant component is not actually rendered
293313
// but affects styling of nested elements
@@ -315,18 +335,45 @@ const ShowToggle = ({
315335
}
316336
});
317337
};
338+
339+
const EyeIcon = isAnimating ? AnimatedEyeOpenIcon : EyeOpenIcon;
340+
318341
return (
319342
<Tooltip
320343
// If you are changing it, change the other one too
321-
content="Removes the instance from the DOM. Breakpoints have no effect on this setting."
344+
content={
345+
<Text>
346+
Removes the instance from the DOM. Breakpoints have no effect on this
347+
setting.
348+
{isAnimating && value && (
349+
<>
350+
<br />
351+
<Text css={{ color: theme.colors.foregroundPrimary }}>
352+
Animation is running on canvas.
353+
</Text>
354+
</>
355+
)}
356+
</Text>
357+
}
322358
disableHoverableContent
323359
variant="wrapped"
324360
>
325361
<SmallIconButton
362+
css={
363+
value && isAnimating
364+
? {
365+
color: theme.colors.foregroundPrimary,
366+
"&:hover": {
367+
color: theme.colors.foregroundPrimary,
368+
filter: "brightness(80%)",
369+
},
370+
}
371+
: undefined
372+
}
326373
tabIndex={-1}
327374
aria-label="Show"
328375
onClick={toggleShow}
329-
icon={value ? <EyeOpenIcon /> : <EyeClosedIcon />}
376+
icon={value ? <EyeIcon /> : <EyeClosedIcon />}
330377
/>
331378
</Tooltip>
332379
);
@@ -521,7 +568,11 @@ export const NavigatorTree = () => {
521568
const selectedKey = selectedInstanceSelector?.join();
522569
const hoveredInstanceSelector = useStore($hoveredInstanceSelector);
523570
const hoveredKey = hoveredInstanceSelector?.join();
524-
const propValuesByInstanceSelector = useStore($propValuesByInstanceSelector);
571+
const propValuesByInstanceSelectorWithMemoryProps = useStore(
572+
$propValuesByInstanceSelectorWithMemoryProps
573+
);
574+
const { propsByInstanceId } = useStore($propsIndex);
575+
525576
const metas = useStore($registeredComponentMetas);
526577
const editingItemSelector = useStore($editingItemSelector);
527578
const dragAndDropState = useStore($dragAndDropState);
@@ -608,14 +659,30 @@ export const NavigatorTree = () => {
608659
{flatTree.map((item) => {
609660
const level = item.visibleAncestors.length - 1;
610661
const key = item.selector.join();
611-
const propValues = propValuesByInstanceSelector.get(
662+
const propValues = propValuesByInstanceSelectorWithMemoryProps.get(
612663
getInstanceKey(item.selector)
613664
);
614665
const show = Boolean(propValues?.get(showAttribute) ?? true);
666+
667+
// Hook memory prop
668+
const isAnimationSelected =
669+
propValues?.get(animationCanPlayOnCanvasAttribute) === true;
670+
671+
const props = propsByInstanceId.get(item.instance.id);
672+
const actionProp = props?.find(
673+
(prop) => prop.type === "animationAction"
674+
);
675+
676+
const isAnimationPinned = actionProp?.value?.isPinned === true;
677+
678+
const isAnimating = isAnimationSelected || isAnimationPinned;
679+
615680
const meta = metas.get(item.instance.component);
681+
616682
if (meta === undefined) {
617683
return;
618684
}
685+
619686
return (
620687
<TreeSortableItem
621688
key={key}
@@ -669,6 +736,7 @@ export const NavigatorTree = () => {
669736
isSelected={selectedKey === key}
670737
isHighlighted={hoveredKey === key || dropTargetKey === key}
671738
isExpanded={item.isExpanded}
739+
isActionVisible={isAnimating}
672740
onExpand={(isExpanded, all) =>
673741
handleExpand(item, isExpanded, all)
674742
}
@@ -696,7 +764,13 @@ export const NavigatorTree = () => {
696764
}
697765
},
698766
}}
699-
action={<ShowToggle instance={item.instance} value={show} />}
767+
action={
768+
<ShowToggle
769+
instance={item.instance}
770+
value={show}
771+
isAnimating={isAnimating}
772+
/>
773+
}
700774
>
701775
<TreeNodeContent
702776
meta={meta}

packages/design-system/src/components/tree.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ export const TreeNode = ({
408408
isSelected,
409409
isHighlighted,
410410
isExpanded,
411+
isActionVisible,
411412
onExpand,
412413
nodeProps,
413414
buttonProps,
@@ -419,10 +420,11 @@ export const TreeNode = ({
419420
isSelected: boolean;
420421
isHighlighted?: boolean;
421422
isExpanded?: undefined | boolean;
423+
isActionVisible?: boolean;
422424
onExpand?: (expanded: boolean, all: boolean) => void;
423425
nodeProps?: ComponentPropsWithoutRef<"div">;
424426
buttonProps: ComponentPropsWithoutRef<"button">;
425-
action?: ReactNode;
427+
action: ReactNode;
426428
children: ReactNode;
427429
}) => {
428430
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -463,7 +465,10 @@ export const TreeNode = ({
463465
return (
464466
<NodeContainer
465467
{...nodeProps}
466-
css={{ [treeNodeLevel]: level }}
468+
css={{
469+
[treeNodeLevel]: level,
470+
...(isActionVisible && { [treeActionOpacity]: 1 }),
471+
}}
467472
onKeyDown={handleKeydown}
468473
>
469474
<DepthBars />
@@ -489,7 +494,7 @@ export const TreeNode = ({
489494
)}
490495
</ExpandButton>
491496
)}
492-
{action && <ActionContainer data-tree-action>{action}</ActionContainer>}
497+
<ActionContainer data-tree-action>{action}</ActionContainer>
493498
</NodeContainer>
494499
);
495500
};

packages/icons/icons/eye-open.svg

Lines changed: 1 addition & 0 deletions
Loading

packages/icons/src/__generated__/components.tsx

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/icons/src/__generated__/svg.ts

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-sdk/src/props.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export const showAttribute = "data-ws-show" as const;
129129
export const indexAttribute = "data-ws-index" as const;
130130
export const collapsedAttribute = "data-ws-collapsed" as const;
131131
export const textContentAttribute = "data-ws-text-content" as const;
132+
export const animationCanPlayOnCanvasAttribute =
133+
"data-ws-animation-can-play-on-canvas";
132134

133135
/**
134136
* Copyright (c) Meta Platforms, Inc. and affiliates.

packages/sdk-components-animation/src/animate-children.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import type { Hook } from "@webstudio-is/react-sdk";
1+
import {
2+
animationCanPlayOnCanvasAttribute,
3+
type Hook,
4+
} from "@webstudio-is/react-sdk";
25
import type { AnimationAction } from "@webstudio-is/sdk";
36
import { forwardRef, type ElementRef } from "react";
4-
import { animationCanPlayOnCanvasProperty } from "./shared/consts";
57

68
type ScrollProps = {
79
debug?: boolean;
@@ -28,7 +30,7 @@ export const hooksAnimateChildren: Hook = {
2830
) {
2931
context.setMemoryProp(
3032
event.instancePath[0],
31-
animationCanPlayOnCanvasProperty,
33+
animationCanPlayOnCanvasAttribute,
3234
undefined
3335
);
3436
}
@@ -40,7 +42,7 @@ export const hooksAnimateChildren: Hook = {
4042
) {
4143
context.setMemoryProp(
4244
event.instancePath[0],
43-
animationCanPlayOnCanvasProperty,
45+
animationCanPlayOnCanvasAttribute,
4446
true
4547
);
4648
}

packages/sdk-components-animation/src/shared/consts.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)