Skip to content

Commit 931d12c

Browse files
authored
Add animations on widget buttons and on action buttons (#15631)
Animated: - Grip - Trash can - Action buttons https://github.com/user-attachments/assets/1cac7a5e-2036-4308-9622-3b4809ae90a3
1 parent 137aba0 commit 931d12c

File tree

8 files changed

+152
-69
lines changed

8 files changed

+152
-69
lines changed
Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,46 @@
11
import { ActionComponent } from '@/action-menu/actions/display/components/ActionComponent';
22
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
3-
3+
import { useTheme } from '@emotion/react';
4+
import styled from '@emotion/styled';
5+
import { motion } from 'framer-motion';
46
import { useContext } from 'react';
7+
8+
const StyledActionContainer = styled(motion.div)`
9+
display: flex;
10+
align-items: center;
11+
justify-content: center;
12+
`;
13+
514
export const PageHeaderActionMenuButtons = () => {
615
const { actions } = useContext(ActionMenuContext);
16+
const theme = useTheme();
717

818
const pinnedActions = actions.filter((entry) => entry.isPinned);
919

10-
return pinnedActions.map((action) => (
11-
<ActionComponent key={action.key} action={action} />
12-
));
20+
const actionsWithPositionForAnimation = pinnedActions.map(
21+
(action, index) => ({
22+
action,
23+
position: pinnedActions.length - index - 1,
24+
}),
25+
);
26+
27+
return (
28+
<>
29+
{actionsWithPositionForAnimation.map(({ action, position }) => (
30+
<StyledActionContainer
31+
key={position}
32+
layout
33+
initial={{ width: 0, opacity: 0 }}
34+
animate={{ width: 'unset', opacity: 1 }}
35+
exit={{ width: 0, opacity: 0 }}
36+
transition={{
37+
duration: theme.animation.duration.instant,
38+
ease: 'easeInOut',
39+
}}
40+
>
41+
<ActionComponent action={action} />
42+
</StyledActionContainer>
43+
))}
44+
</>
45+
);
1346
};

packages/twenty-front/src/modules/page-layout/widgets/components/WidgetPlaceholder.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const WidgetPlaceholder = () => {
5151
isDragging={false}
5252
>
5353
<WidgetCardHeader
54+
isWidgetCardHovered={false}
5455
isInEditMode={isPageLayoutInEditMode}
5556
title={t`Add Widget`}
5657
isEmpty

packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { WidgetCardContent } from '@/page-layout/widgets/widget-card/components/
1212
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
1313
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
1414
import { useTheme } from '@emotion/react';
15-
import { type MouseEvent } from 'react';
15+
import { useState, type MouseEvent } from 'react';
1616
import { IconLock } from 'twenty-ui/display';
1717
import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql';
1818

@@ -61,16 +61,29 @@ export const WidgetRenderer = ({
6161
deletePageLayoutWidget(widget.id);
6262
};
6363

64+
const [isHovered, setIsHovered] = useState(false);
65+
66+
const handleMouseEnter = () => {
67+
setIsHovered(true);
68+
};
69+
70+
const handleMouseLeave = () => {
71+
setIsHovered(false);
72+
};
73+
6474
return (
6575
<WidgetCard
6676
onClick={isPageLayoutInEditMode ? handleClick : undefined}
6777
isDragging={isDragging}
6878
pageLayoutType={pageLayoutType}
6979
layoutMode={layoutMode}
7080
isEditing={isEditing}
81+
onMouseEnter={handleMouseEnter}
82+
onMouseLeave={handleMouseLeave}
7183
>
7284
{layoutMode !== 'canvas' && (
7385
<WidgetCardHeader
86+
isWidgetCardHovered={isHovered}
7487
isInEditMode={isPageLayoutInEditMode}
7588
title={widget.title}
7689
onRemove={handleRemove}

packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCard.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export type WidgetCardProps = {
1515
isEditing: boolean;
1616
isDragging: boolean;
1717
className?: string;
18+
onMouseEnter?: () => void;
19+
onMouseLeave?: () => void;
1820
};
1921

2022
const StyledWidgetCard = styled.div<{
@@ -61,12 +63,8 @@ const StyledWidgetCard = styled.div<{
6163
!isEditing &&
6264
css`
6365
&:hover {
64-
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
6566
border: 1px solid ${theme.border.color.strong};
66-
67-
.widget-card-remove-button {
68-
display: flex !important;
69-
}
67+
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
7068
}
7169
`}
7270
@@ -101,12 +99,8 @@ const StyledWidgetCard = styled.div<{
10199
!isEditing &&
102100
css`
103101
&:hover {
104-
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
105102
border: 1px solid ${theme.border.color.strong};
106-
107-
.widget-card-remove-button {
108-
display: flex !important;
109-
}
103+
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
110104
}
111105
`}
112106
@@ -142,6 +136,8 @@ export const WidgetCard = ({
142136
isEditing,
143137
isDragging,
144138
className,
139+
onMouseEnter,
140+
onMouseLeave,
145141
}: WidgetCardProps) => {
146142
const isPageLayoutInEditMode = useRecoilComponentValue(
147143
isPageLayoutInEditModeComponentState,
@@ -156,6 +152,8 @@ export const WidgetCard = ({
156152
isEditing={isEditing}
157153
isDragging={isDragging}
158154
className={className}
155+
onMouseEnter={onMouseEnter}
156+
onMouseLeave={onMouseLeave}
159157
>
160158
{children}
161159
</StyledWidgetCard>

packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import { useTheme } from '@emotion/react';
12
import styled from '@emotion/styled';
23
import { t } from '@lingui/core/macro';
34
import { type ReactNode } from 'react';
45
import { IconTrash, OverflowingTextWithTooltip } from 'twenty-ui/display';
56
import { IconButton } from 'twenty-ui/input';
67

78
import { WidgetGrip } from '@/page-layout/widgets/widget-card/components/WidgetGrip';
9+
import { AnimatePresence, motion } from 'framer-motion';
810
import { isDefined } from 'twenty-shared/utils';
911

1012
export type WidgetCardHeaderProps = {
13+
isWidgetCardHovered: boolean;
1114
isInEditMode: boolean;
1215
isEmpty?: boolean;
1316
title: string;
@@ -39,40 +42,58 @@ const StyledRightContainer = styled.div`
3942
gap: ${({ theme }) => theme.spacing(0.5)};
4043
`;
4144

42-
const StyledIconButton = styled(IconButton)`
43-
display: none;
45+
const StyledIconButtonContainer = styled(motion.div)`
46+
display: flex;
47+
align-items: center;
48+
justify-content: center;
4449
`;
4550

4651
export const WidgetCardHeader = ({
52+
isWidgetCardHovered = false,
4753
isEmpty = false,
4854
isInEditMode = false,
4955
title,
5056
onRemove,
5157
forbiddenDisplay,
5258
className,
5359
}: WidgetCardHeaderProps) => {
60+
const theme = useTheme();
61+
5462
return (
5563
<StyledWidgetCardHeader className={className}>
56-
{!isEmpty && isInEditMode && (
57-
<WidgetGrip
58-
className="drag-handle"
59-
onClick={(e) => e.stopPropagation()}
60-
/>
61-
)}
64+
<AnimatePresence>
65+
{!isEmpty && isInEditMode && (
66+
<WidgetGrip
67+
className="drag-handle"
68+
onClick={(e) => e.stopPropagation()}
69+
/>
70+
)}
71+
</AnimatePresence>
6272
<StyledTitleContainer>
6373
<OverflowingTextWithTooltip text={isEmpty ? t`Add Widget` : title} />
6474
</StyledTitleContainer>
6575
<StyledRightContainer>
6676
{isDefined(forbiddenDisplay) && forbiddenDisplay}
67-
{!isEmpty && isInEditMode && onRemove && (
68-
<StyledIconButton
69-
onClick={onRemove}
70-
Icon={IconTrash}
71-
variant="tertiary"
72-
size="small"
73-
className="widget-card-remove-button"
74-
/>
75-
)}
77+
<AnimatePresence>
78+
{!isEmpty && isInEditMode && onRemove && isWidgetCardHovered && (
79+
<StyledIconButtonContainer
80+
initial={{ width: 0, opacity: 0 }}
81+
animate={{ width: 'auto', opacity: 1 }}
82+
exit={{ width: 0, opacity: 0 }}
83+
transition={{
84+
duration: theme.animation.duration.fast,
85+
ease: 'easeInOut',
86+
}}
87+
>
88+
<IconButton
89+
onClick={onRemove}
90+
Icon={IconTrash}
91+
variant="tertiary"
92+
size="small"
93+
/>
94+
</StyledIconButtonContainer>
95+
)}
96+
</AnimatePresence>
7697
</StyledRightContainer>
7798
</StyledWidgetCardHeader>
7899
);

packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetGrip.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useTheme } from '@emotion/react';
22
import styled from '@emotion/styled';
3+
import { motion } from 'framer-motion';
34
import { IconGripVertical } from 'twenty-ui/display';
45

5-
const StyledGripContainer = styled.div`
6+
const StyledGripContainer = styled(motion.div)`
67
width: 20px;
78
height: 20px;
89
display: flex;
@@ -32,7 +33,18 @@ export const WidgetGrip = ({ className, onClick }: WidgetGripProps) => {
3233
const theme = useTheme();
3334

3435
return (
35-
<StyledGripContainer className={className} onClick={onClick}>
36+
<StyledGripContainer
37+
layout
38+
className={className}
39+
onClick={onClick}
40+
initial={{ width: 0, opacity: 0 }}
41+
animate={{ width: 20, opacity: 1 }}
42+
exit={{ width: 0, opacity: 0 }}
43+
transition={{
44+
duration: theme.animation.duration.fast,
45+
ease: 'easeInOut',
46+
}}
47+
>
3648
<IconGripVertical
3749
size={theme.icon.size.sm}
3850
color={theme.font.color.extraLight}

packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/__stories__/WidgetCard.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const Default: Story = {
8484
isDragging={args.isDragging}
8585
>
8686
<WidgetCardHeader
87+
isWidgetCardHovered={false}
8788
isInEditMode={true}
8889
onRemove={() => {}}
8990
title="Widget name"
@@ -189,6 +190,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetCard> = {
189190
isInEditMode={!isReadMode}
190191
onRemove={!isReadMode ? () => {} : undefined}
191192
title="Widget name"
193+
isWidgetCardHovered={args.state === 'Hover'}
192194
/>
193195
<WidgetCardContent
194196
pageLayoutType={pageLayoutType}

packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID } from '@/ui/layout/page/constan
99
import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight';
1010
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
1111
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
12+
import { AnimatePresence } from 'framer-motion';
1213
import {
1314
type IconComponent,
1415
IconX,
@@ -100,43 +101,45 @@ export const PageHeader = ({
100101
);
101102

102103
return (
103-
<StyledTopBarContainer className={className} isMobile={isMobile}>
104-
<StyledLeftContainer>
105-
{!isMobile && !isNavigationDrawerExpanded && (
106-
<NavigationDrawerCollapseButton direction="right" />
107-
)}
108-
{hasClosePageButton && (
109-
<LightIconButton
110-
Icon={IconX}
111-
size="small"
112-
accent="tertiary"
113-
onClick={() => onClosePage?.()}
114-
/>
115-
)}
116-
117-
<StyledTopBarIconStyledTitleContainer>
118-
{Icon && (
119-
<StyledIconContainer>
120-
<Icon size={theme.icon.size.md} />
121-
</StyledIconContainer>
104+
<AnimatePresence initial={false}>
105+
<StyledTopBarContainer className={className} isMobile={isMobile}>
106+
<StyledLeftContainer>
107+
{!isMobile && !isNavigationDrawerExpanded && (
108+
<NavigationDrawerCollapseButton direction="right" />
122109
)}
123-
{title && (
124-
<StyledTitleContainer data-testid="top-bar-title">
125-
{typeof title === 'string' ? (
126-
<OverflowingTextWithTooltip text={title} />
127-
) : (
128-
title
129-
)}
130-
</StyledTitleContainer>
110+
{hasClosePageButton && (
111+
<LightIconButton
112+
Icon={IconX}
113+
size="small"
114+
accent="tertiary"
115+
onClick={() => onClosePage?.()}
116+
/>
131117
)}
132-
</StyledTopBarIconStyledTitleContainer>
133-
</StyledLeftContainer>
134118

135-
<StyledPageActionContainer
136-
data-click-outside-id={PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID}
137-
>
138-
{children}
139-
</StyledPageActionContainer>
140-
</StyledTopBarContainer>
119+
<StyledTopBarIconStyledTitleContainer>
120+
{Icon && (
121+
<StyledIconContainer>
122+
<Icon size={theme.icon.size.md} />
123+
</StyledIconContainer>
124+
)}
125+
{title && (
126+
<StyledTitleContainer data-testid="top-bar-title">
127+
{typeof title === 'string' ? (
128+
<OverflowingTextWithTooltip text={title} />
129+
) : (
130+
title
131+
)}
132+
</StyledTitleContainer>
133+
)}
134+
</StyledTopBarIconStyledTitleContainer>
135+
</StyledLeftContainer>
136+
137+
<StyledPageActionContainer
138+
data-click-outside-id={PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID}
139+
>
140+
{children}
141+
</StyledPageActionContainer>
142+
</StyledTopBarContainer>
143+
</AnimatePresence>
141144
);
142145
};

0 commit comments

Comments
 (0)