Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

406 animate the command menu button #10305

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/wo
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useIsMobile } from 'twenty-ui';
Expand Down Expand Up @@ -65,6 +65,7 @@ export const CommandMenuContainer = ({
callback: closeCommandMenu,
listenerId: 'COMMAND_MENU_LISTENER_ID',
hotkeyScope: AppHotkeyScope.CommandMenuOpen,
excludeClassNames: ['page-header-command-menu-button'],
});

const isMobile = useIsMobile();
Expand Down Expand Up @@ -114,20 +115,22 @@ export const CommandMenuContainer = ({
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
)}
<ActionMenuConfirmationModals />
{isCommandMenuOpened && (
<StyledCommandMenu
data-testid="command-menu"
ref={commandMenuRef}
className="command-menu"
animate={targetVariantForAnimation}
initial="closed"
exit="closed"
variants={COMMAND_MENU_ANIMATION_VARIANTS}
transition={{ duration: theme.animation.duration.normal }}
>
{children}
</StyledCommandMenu>
)}
<AnimatePresence mode="wait">
{isCommandMenuOpened && (
<StyledCommandMenu
data-testid="command-menu"
ref={commandMenuRef}
className="command-menu"
animate={targetVariantForAnimation}
initial="closed"
exit="closed"
variants={COMMAND_MENU_ANIMATION_VARIANTS}
transition={{ duration: theme.animation.duration.normal }}
>
{children}
</StyledCommandMenu>
)}
</AnimatePresence>
</ActionMenuContext.Provider>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { CommandMenuContainer } from '@/command-menu/components/CommandMenuConta
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';

Expand All @@ -20,9 +22,21 @@ export const CommandMenuRouter = () => {
<></>
);

const theme = useTheme();

return (
<CommandMenuContainer>
<CommandMenuTopBar />
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: theme.animation.duration.instant,
delay: 0.1,
}}
>
<CommandMenuTopBar />
</motion.div>
<StyledCommandMenuContent>
{commandMenuPageComponent}
</StyledCommandMenuContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useMemo, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import {
Expand Down Expand Up @@ -80,6 +81,10 @@ const StyledCloseButtonContainer = styled.div`
justify-content: center;
`;

const StyledCloseButtonWrapper = styled.div<{ isVisible: boolean }>`
visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
`;

export const CommandMenuTopBar = () => {
const [commandMenuSearch, setCommandMenuSearch] = useRecoilState(
commandMenuSearchState,
Expand Down Expand Up @@ -123,6 +128,11 @@ export const CommandMenuTopBar = () => {
});
}, [commandMenuNavigationStack, theme.icon.size.sm]);

const location = useLocation();
const isButtonVisible =
!location.pathname.startsWith('/objects/') &&
!location.pathname.startsWith('/object/');
Comment on lines +132 to +134
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a safest way to determine whether the button should be visible? Do you think we'll want to add it to more pages soon? If so, could we make it simpler to update the code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that we will potentially add it everywhere, so we will just have to remove this check ;)


return (
<StyledInputContainer>
<StyledContentContainer>
Expand Down Expand Up @@ -162,7 +172,7 @@ export const CommandMenuTopBar = () => {
)}
</StyledContentContainer>
{!isMobile && (
<>
<StyledCloseButtonWrapper isVisible={isButtonVisible}>
{isCommandMenuV2Enabled ? (
<Button
Icon={IconX}
Expand All @@ -184,7 +194,7 @@ export const CommandMenuTopBar = () => {
/>
</StyledCloseButtonContainer>
)}
</>
</StyledCloseButtonWrapper>
)}
</StyledInputContainer>
);
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good! 💯

Original file line number Diff line number Diff line change
@@ -1,36 +1,149 @@
import {
Button,
AnimatedButton,
IconButton,
IconDotsVertical,
IconX,
getOsControlSymbol,
useIsMobile,
} from 'twenty-ui';

import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { motion } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated/graphql';

const StyledButtonWrapper = styled.div`
z-index: 30;
`;

const xPaths = {
topLeft: `M12 12 L6 6`,
topRight: `M12 12 L18 6`,
bottomLeft: `M12 12 L6 18`,
bottomRight: `M12 12 L18 18`,
};

const AnimatedIcon = ({
isCommandMenuOpened,
}: {
isCommandMenuOpened: boolean;
}) => {
const theme = useTheme();
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={theme.icon.size.sm}
height={theme.icon.size.sm}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={theme.icon.stroke.md}
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
{/* Center dot */}
<motion.circle
cx={12}
cy={12}
r="1"
animate={{
scale: isCommandMenuOpened ? 0 : 1,
opacity: isCommandMenuOpened ? 0 : 1,
}}
transition={{ duration: theme.animation.duration.fast }}
/>

{/* X lines expanding from center */}
{Object.values(xPaths).map((path, index) => (
<motion.path
key={index}
d={path}
initial={{ pathLength: 0 }}
animate={{
pathLength: isCommandMenuOpened ? 1 : 0,
opacity: isCommandMenuOpened ? 1 : 0,
}}
transition={{
duration: theme.animation.duration.fast,
ease: 'easeInOut',
delay: isCommandMenuOpened ? 0.1 : 0,
}}
/>
))}

{/* Top dot */}
<motion.circle
cx="12"
cy="5"
r="1"
animate={{
scale: isCommandMenuOpened ? 0 : 1,
opacity: isCommandMenuOpened ? 0 : 1,
}}
transition={{ duration: theme.animation.duration.fast }}
/>

{/* Bottom dot */}
<motion.circle
cx="12"
cy="19"
r="1"
animate={{
scale: isCommandMenuOpened ? 0 : 1,
opacity: isCommandMenuOpened ? 0 : 1,
}}
transition={{ duration: theme.animation.duration.fast }}
/>
</svg>
);
};

export const PageHeaderOpenCommandMenuButton = () => {
const { openRootCommandMenu } = useCommandMenu();
const { toggleCommandMenu } = useCommandMenu();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);

const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);

const isMobile = useIsMobile();

const ariaLabel = isCommandMenuOpened
? t`Close command menu`
: t`Open command menu`;

const Icon = isCommandMenuOpened ? IconX : IconDotsVertical;

const theme = useTheme();

return (
<>
<StyledButtonWrapper>
{isCommandMenuV2Enabled ? (
<Button
Icon={IconDotsVertical}
dataTestId="page-header-open-command-menu-button"
<AnimatedButton
animatedSvg={
<AnimatedIcon isCommandMenuOpened={isCommandMenuOpened} />
}
className="page-header-command-menu-button"
dataTestId="page-header-command-menu-button"
size={isMobile ? 'medium' : 'small'}
variant="secondary"
accent="default"
hotkeys={[getOsControlSymbol(), 'K']}
ariaLabel="Open command menu"
onClick={openRootCommandMenu}
ariaLabel={ariaLabel}
onClick={toggleCommandMenu}
animate={{
rotate: isCommandMenuOpened ? 90 : 0,
}}
transition={{
duration: theme.animation.duration.normal,
ease: 'easeInOut',
}}
/>
) : (
<IconButton
Expand All @@ -39,9 +152,9 @@ export const PageHeaderOpenCommandMenuButton = () => {
dataTestId="more-showpage-button"
accent="default"
variant="secondary"
onClick={openRootCommandMenu}
onClick={toggleCommandMenu}
/>
)}
</>
</StyledButtonWrapper>
);
};
Loading
Loading