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
This commit is contained in:
+37
-4
@@ -1,13 +1,46 @@
|
||||
import { ActionComponent } from '@/action-menu/actions/display/components/ActionComponent';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useContext } from 'react';
|
||||
|
||||
const StyledActionContainer = styled(motion.div)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const PageHeaderActionMenuButtons = () => {
|
||||
const { actions } = useContext(ActionMenuContext);
|
||||
const theme = useTheme();
|
||||
|
||||
const pinnedActions = actions.filter((entry) => entry.isPinned);
|
||||
|
||||
return pinnedActions.map((action) => (
|
||||
<ActionComponent key={action.key} action={action} />
|
||||
));
|
||||
const actionsWithPositionForAnimation = pinnedActions.map(
|
||||
(action, index) => ({
|
||||
action,
|
||||
position: pinnedActions.length - index - 1,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{actionsWithPositionForAnimation.map(({ action, position }) => (
|
||||
<StyledActionContainer
|
||||
key={position}
|
||||
layout
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'unset', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
<ActionComponent action={action} />
|
||||
</StyledActionContainer>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ export const WidgetPlaceholder = () => {
|
||||
isDragging={false}
|
||||
>
|
||||
<WidgetCardHeader
|
||||
isWidgetCardHovered={false}
|
||||
isInEditMode={isPageLayoutInEditMode}
|
||||
title={t`Add Widget`}
|
||||
isEmpty
|
||||
|
||||
+14
-1
@@ -12,7 +12,7 @@ import { WidgetCardContent } from '@/page-layout/widgets/widget-card/components/
|
||||
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { type MouseEvent } from 'react';
|
||||
import { useState, type MouseEvent } from 'react';
|
||||
import { IconLock } from 'twenty-ui/display';
|
||||
import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
@@ -61,6 +61,16 @@ export const WidgetRenderer = ({
|
||||
deletePageLayoutWidget(widget.id);
|
||||
};
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<WidgetCard
|
||||
onClick={isPageLayoutInEditMode ? handleClick : undefined}
|
||||
@@ -68,9 +78,12 @@ export const WidgetRenderer = ({
|
||||
pageLayoutType={pageLayoutType}
|
||||
layoutMode={layoutMode}
|
||||
isEditing={isEditing}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{layoutMode !== 'canvas' && (
|
||||
<WidgetCardHeader
|
||||
isWidgetCardHovered={isHovered}
|
||||
isInEditMode={isPageLayoutInEditMode}
|
||||
title={widget.title}
|
||||
onRemove={handleRemove}
|
||||
|
||||
+8
-10
@@ -15,6 +15,8 @@ export type WidgetCardProps = {
|
||||
isEditing: boolean;
|
||||
isDragging: boolean;
|
||||
className?: string;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
};
|
||||
|
||||
const StyledWidgetCard = styled.div<{
|
||||
@@ -61,12 +63,8 @@ const StyledWidgetCard = styled.div<{
|
||||
!isEditing &&
|
||||
css`
|
||||
&:hover {
|
||||
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
|
||||
border: 1px solid ${theme.border.color.strong};
|
||||
|
||||
.widget-card-remove-button {
|
||||
display: flex !important;
|
||||
}
|
||||
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -101,12 +99,8 @@ const StyledWidgetCard = styled.div<{
|
||||
!isEditing &&
|
||||
css`
|
||||
&:hover {
|
||||
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
|
||||
border: 1px solid ${theme.border.color.strong};
|
||||
|
||||
.widget-card-remove-button {
|
||||
display: flex !important;
|
||||
}
|
||||
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -142,6 +136,8 @@ export const WidgetCard = ({
|
||||
isEditing,
|
||||
isDragging,
|
||||
className,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: WidgetCardProps) => {
|
||||
const isPageLayoutInEditMode = useRecoilComponentValue(
|
||||
isPageLayoutInEditModeComponentState,
|
||||
@@ -156,6 +152,8 @@ export const WidgetCard = ({
|
||||
isEditing={isEditing}
|
||||
isDragging={isDragging}
|
||||
className={className}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{children}
|
||||
</StyledWidgetCard>
|
||||
|
||||
+38
-17
@@ -1,3 +1,4 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ReactNode } from 'react';
|
||||
@@ -5,9 +6,11 @@ import { IconTrash, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
|
||||
import { WidgetGrip } from '@/page-layout/widgets/widget-card/components/WidgetGrip';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type WidgetCardHeaderProps = {
|
||||
isWidgetCardHovered: boolean;
|
||||
isInEditMode: boolean;
|
||||
isEmpty?: boolean;
|
||||
title: string;
|
||||
@@ -39,11 +42,14 @@ const StyledRightContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
`;
|
||||
|
||||
const StyledIconButton = styled(IconButton)`
|
||||
display: none;
|
||||
const StyledIconButtonContainer = styled(motion.div)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const WidgetCardHeader = ({
|
||||
isWidgetCardHovered = false,
|
||||
isEmpty = false,
|
||||
isInEditMode = false,
|
||||
title,
|
||||
@@ -51,28 +57,43 @@ export const WidgetCardHeader = ({
|
||||
forbiddenDisplay,
|
||||
className,
|
||||
}: WidgetCardHeaderProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledWidgetCardHeader className={className}>
|
||||
{!isEmpty && isInEditMode && (
|
||||
<WidgetGrip
|
||||
className="drag-handle"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{!isEmpty && isInEditMode && (
|
||||
<WidgetGrip
|
||||
className="drag-handle"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<StyledTitleContainer>
|
||||
<OverflowingTextWithTooltip text={isEmpty ? t`Add Widget` : title} />
|
||||
</StyledTitleContainer>
|
||||
<StyledRightContainer>
|
||||
{isDefined(forbiddenDisplay) && forbiddenDisplay}
|
||||
{!isEmpty && isInEditMode && onRemove && (
|
||||
<StyledIconButton
|
||||
onClick={onRemove}
|
||||
Icon={IconTrash}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
className="widget-card-remove-button"
|
||||
/>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{!isEmpty && isInEditMode && onRemove && isWidgetCardHovered && (
|
||||
<StyledIconButtonContainer
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'auto', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={onRemove}
|
||||
Icon={IconTrash}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
</StyledIconButtonContainer>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</StyledRightContainer>
|
||||
</StyledWidgetCardHeader>
|
||||
);
|
||||
|
||||
+14
-2
@@ -1,8 +1,9 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IconGripVertical } from 'twenty-ui/display';
|
||||
|
||||
const StyledGripContainer = styled.div`
|
||||
const StyledGripContainer = styled(motion.div)`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
@@ -32,7 +33,18 @@ export const WidgetGrip = ({ className, onClick }: WidgetGripProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledGripContainer className={className} onClick={onClick}>
|
||||
<StyledGripContainer
|
||||
layout
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 20, opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.fast,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
<IconGripVertical
|
||||
size={theme.icon.size.sm}
|
||||
color={theme.font.color.extraLight}
|
||||
|
||||
+2
@@ -84,6 +84,7 @@ export const Default: Story = {
|
||||
isDragging={args.isDragging}
|
||||
>
|
||||
<WidgetCardHeader
|
||||
isWidgetCardHovered={false}
|
||||
isInEditMode={true}
|
||||
onRemove={() => {}}
|
||||
title="Widget name"
|
||||
@@ -189,6 +190,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetCard> = {
|
||||
isInEditMode={!isReadMode}
|
||||
onRemove={!isReadMode ? () => {} : undefined}
|
||||
title="Widget name"
|
||||
isWidgetCardHovered={args.state === 'Hover'}
|
||||
/>
|
||||
<WidgetCardContent
|
||||
pageLayoutType={pageLayoutType}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID } from '@/ui/layout/page/constan
|
||||
import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
type IconComponent,
|
||||
IconX,
|
||||
@@ -100,43 +101,45 @@ export const PageHeader = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTopBarContainer className={className} isMobile={isMobile}>
|
||||
<StyledLeftContainer>
|
||||
{!isMobile && !isNavigationDrawerExpanded && (
|
||||
<NavigationDrawerCollapseButton direction="right" />
|
||||
)}
|
||||
{hasClosePageButton && (
|
||||
<LightIconButton
|
||||
Icon={IconX}
|
||||
size="small"
|
||||
accent="tertiary"
|
||||
onClick={() => onClosePage?.()}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledTopBarIconStyledTitleContainer>
|
||||
{Icon && (
|
||||
<StyledIconContainer>
|
||||
<Icon size={theme.icon.size.md} />
|
||||
</StyledIconContainer>
|
||||
<AnimatePresence initial={false}>
|
||||
<StyledTopBarContainer className={className} isMobile={isMobile}>
|
||||
<StyledLeftContainer>
|
||||
{!isMobile && !isNavigationDrawerExpanded && (
|
||||
<NavigationDrawerCollapseButton direction="right" />
|
||||
)}
|
||||
{title && (
|
||||
<StyledTitleContainer data-testid="top-bar-title">
|
||||
{typeof title === 'string' ? (
|
||||
<OverflowingTextWithTooltip text={title} />
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</StyledTitleContainer>
|
||||
{hasClosePageButton && (
|
||||
<LightIconButton
|
||||
Icon={IconX}
|
||||
size="small"
|
||||
accent="tertiary"
|
||||
onClick={() => onClosePage?.()}
|
||||
/>
|
||||
)}
|
||||
</StyledTopBarIconStyledTitleContainer>
|
||||
</StyledLeftContainer>
|
||||
|
||||
<StyledPageActionContainer
|
||||
data-click-outside-id={PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
{children}
|
||||
</StyledPageActionContainer>
|
||||
</StyledTopBarContainer>
|
||||
<StyledTopBarIconStyledTitleContainer>
|
||||
{Icon && (
|
||||
<StyledIconContainer>
|
||||
<Icon size={theme.icon.size.md} />
|
||||
</StyledIconContainer>
|
||||
)}
|
||||
{title && (
|
||||
<StyledTitleContainer data-testid="top-bar-title">
|
||||
{typeof title === 'string' ? (
|
||||
<OverflowingTextWithTooltip text={title} />
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</StyledTitleContainer>
|
||||
)}
|
||||
</StyledTopBarIconStyledTitleContainer>
|
||||
</StyledLeftContainer>
|
||||
|
||||
<StyledPageActionContainer
|
||||
data-click-outside-id={PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
{children}
|
||||
</StyledPageActionContainer>
|
||||
</StyledTopBarContainer>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user