feat: improve mobile display by tab bar and other changes (#2304)

* feat: improve mobile display by tab bar and other changes

* fix: remove unused declaration in mobile navigation

* fix: update desktop navbar stories title

* fix: retrieve old titles for desktop-navbar stories

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: styles, manage active tabs

* fix: update logic for tab bar menu icons

* fix: remove Settings icon for mobile

* fix: resolve comments in pl

* feat: rework mobile navigation bar

* Fix

* Fixes

---------

Co-authored-by: Thaïs Guigon <guigon.thais@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Saba Shavidze
2023-12-02 02:16:34 +04:00
committed by GitHub
parent 74b077f3ca
commit fec8223ab8
50 changed files with 640 additions and 380 deletions

View File

@ -1,63 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useIsSubMenuNavbarDisplayed } from '@/ui/layout/hooks/useIsSubMenuNavbarDisplayed';
import { isNavbarOpenedState } from '@/ui/layout/states/isNavbarOpenedState';
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { leftNavbarWidth, leftSubMenuNavbarWidth } from '../constants';
const StyledNavbarContainer = styled(motion.div)`
align-items: end;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
padding: ${({ theme }) => theme.spacing(2)};
`;
type NavbarAnimatedContainerProps = {
children: React.ReactNode;
};
export const NavbarAnimatedContainer = ({
children,
}: NavbarAnimatedContainerProps) => {
const isNavbarOpened = useRecoilValue(isNavbarOpenedState);
const [, setIsNavbarSwitchingSize] = useRecoilState(
isNavbarSwitchingSizeState,
);
const isInSubMenu = useIsSubMenuNavbarDisplayed();
const theme = useTheme();
const isMobile = useIsMobile();
const leftBarWidth = isInSubMenu
? isMobile
? leftSubMenuNavbarWidth.mobile
: leftSubMenuNavbarWidth.desktop
: isMobile
? leftNavbarWidth.mobile
: leftNavbarWidth.desktop;
return (
<StyledNavbarContainer
onAnimationComplete={() => {
setIsNavbarSwitchingSize(false);
}}
initial={false}
animate={{
width: isNavbarOpened ? leftBarWidth : '0',
opacity: isNavbarOpened ? 1 : 0,
}}
transition={{
duration: theme.animation.duration.normal,
}}
>
{children}
</StyledNavbarContainer>
);
};

View File

@ -1,11 +0,0 @@
export const leftNavbarWidth = {
mobile: 'calc(100% - 16px)',
desktop: '220px',
};
export const leftSubMenuNavbarWidth = {
mobile: 'calc(100% - 16px)',
desktop: '520px',
};
export const githubLink = 'https://github.com/twentyhq/twenty';

View File

@ -0,0 +1,33 @@
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { NavigationBarItem } from './NavigationBarItem';
const StyledContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: center;
padding: ${({ theme }) => theme.spacing(3)};
`;
type NavigationBarProps = {
activeItemName: string;
items: { name: string; Icon: IconComponent; onClick: () => void }[];
};
export const NavigationBar = ({
activeItemName,
items,
}: NavigationBarProps) => (
<StyledContainer>
{items.map(({ Icon, name, onClick }) => (
<NavigationBarItem
key={name}
Icon={Icon}
isActive={activeItemName === name}
onClick={onClick}
/>
))}
</StyledContainer>
);

View File

@ -0,0 +1,42 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
const StyledIconButton = styled.div<{ isActive?: boolean }>`
align-items: center;
background-color: ${({ isActive, theme }) =>
isActive ? theme.background.transparent.light : 'none'};
border-radius: ${({ theme }) => theme.spacing(1)};
cursor: pointer;
display: flex;
height: ${({ theme }) => theme.spacing(10)};
justify-content: center;
transition: background-color ${({ theme }) => theme.animation.duration.fast}s
ease;
width: ${({ theme }) => theme.spacing(10)};
&:hover {
background-color: ${({ theme }) => theme.background.transparent.light};
}
`;
type NavigationBarItemProps = {
Icon: IconComponent;
isActive: boolean;
onClick: () => void;
};
export const NavigationBarItem = ({
Icon,
isActive,
onClick,
}: NavigationBarItemProps) => {
const theme = useTheme();
return (
<StyledIconButton isActive={isActive} onClick={onClick}>
<Icon color={theme.color.gray50} size={theme.icon.size.lg} />
</StyledIconButton>
);
};

View File

@ -0,0 +1,33 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { NavigationBar } from '../NavigationBar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import {
IconList,
IconSearch,
IconCheckbox,
IconSettings,
} from '@/ui/display/icon';
const meta: Meta<typeof NavigationBar> = {
title: 'UI/Navigation/NavigationBar/NavigationBar',
component: NavigationBar,
args: {
activeItemName: 'main',
items: [
{ name: 'main', Icon: IconList, onClick: () => undefined },
{ name: 'search', Icon: IconSearch, onClick: () => undefined },
{ name: 'tasks', Icon: IconCheckbox, onClick: () => undefined },
{ name: 'settings', Icon: IconSettings, onClick: () => undefined },
],
},
};
export default meta;
type Story = StoryObj<typeof NavigationBar>;
export const Default: Story = {
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
};

View File

@ -10,11 +10,13 @@ type MainNavbarProps = {
};
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
margin-bottom: ${({ theme }) => theme.spacing(2.5)};
padding: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;

View File

@ -1,9 +1,9 @@
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { IconChevronLeft } from '@/ui/display/icon/index';
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
type NavBackButtonProps = {
title: string;
@ -32,24 +32,19 @@ const StyledContainer = styled.div`
const NavBackButton = ({ title }: NavBackButtonProps) => {
const navigate = useNavigate();
const [, setIsNavbarSwitchingSize] = useRecoilState(
isNavbarSwitchingSizeState,
);
const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState);
return (
<>
<StyledContainer>
<StyledIconAndButtonContainer
onClick={() => {
setIsNavbarSwitchingSize(true);
navigate('/', { replace: true });
}}
>
<IconChevronLeft />
<span>{title}</span>
</StyledIconAndButtonContainer>
</StyledContainer>
</>
<StyledContainer>
<StyledIconAndButtonContainer
onClick={() => {
navigate(navigationMemorizedUrl, { replace: true });
}}
>
<IconChevronLeft />
<span>{title}</span>
</StyledIconAndButtonContainer>
</StyledContainer>
);
};

View File

@ -1,14 +1,14 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilState } from 'recoil';
import { useSetRecoilState } from 'recoil';
import {
IconLayoutSidebarLeftCollapse,
IconLayoutSidebarRightCollapse,
} from '@/ui/display/icon';
import { IconButton } from '@/ui/input/button/components/IconButton';
import { isNavbarOpenedState } from '@/ui/layout/states/isNavbarOpenedState';
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
const StyledCollapseButton = styled(motion.div)`
align-items: center;
@ -41,8 +41,7 @@ const NavCollapseButton = ({
direction = 'left',
show = true,
}: NavCollapseButtonProps) => {
const [isNavbarOpened, setIsNavbarOpened] =
useRecoilState(isNavbarOpenedState);
const setNavigationDrawer = useSetRecoilState(navigationDrawerState);
const iconSize = 'small';
const theme = useTheme();
@ -57,7 +56,11 @@ const NavCollapseButton = ({
transition={{
duration: theme.animation.duration.normal,
}}
onClick={() => setIsNavbarOpened(!isNavbarOpened)}
onClick={() =>
setNavigationDrawer((navigationDrawer) =>
navigationDrawer === '' ? 'main' : '',
)
}
>
<IconButton
Icon={

View File

@ -1,15 +1,15 @@
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useSetRecoilState } from 'recoil';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isNavbarOpenedState } from '../../../layout/states/isNavbarOpenedState';
type NavItemProps = {
className?: string;
label: string;
to?: string;
onClick?: () => void;
@ -115,6 +115,7 @@ const StyledKeyBoardShortcut = styled.div`
`;
const NavItem = ({
className,
label,
Icon,
to,
@ -126,25 +127,26 @@ const NavItem = ({
keyboard,
}: NavItemProps) => {
const theme = useTheme();
const navigate = useNavigate();
const [, setIsNavbarOpened] = useRecoilState(isNavbarOpenedState);
const isMobile = useIsMobile();
const navigate = useNavigate();
const setNavigationDrawer = useSetRecoilState(navigationDrawerState);
const handleItemClick = () => {
if (isMobile) {
setIsNavbarOpened(false);
setNavigationDrawer('');
}
if (onClick) {
onClick();
} else if (to) {
navigate(to);
return;
}
if (to) navigate(to);
};
return (
<StyledItem
className={className}
onClick={handleItemClick}
active={active}
aria-selected={active}
@ -157,7 +159,7 @@ const NavItem = ({
{!!count && <StyledItemCount>{count}</StyledItemCount>}
{keyboard && (
<StyledKeyBoardShortcut className="keyboard-shortcuts">
{keyboard.map((key) => key)}
{keyboard}
</StyledKeyBoardShortcut>
)}
</StyledItem>

View File

@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
import NavCollapseButton from './NavCollapseButton';
@ -53,7 +54,7 @@ const NavWorkspaceButton = ({
showCollapseButton,
}: NavWorkspaceButtonProps) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isMobile = useIsMobile();
const DEFAULT_LOGO =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
@ -69,7 +70,9 @@ const NavWorkspaceButton = ({
></StyledLogo>
<StyledName>{currentWorkspace?.displayName ?? 'Twenty'}</StyledName>
</StyledLogoAndNameContainer>
<NavCollapseButton direction="left" show={showCollapseButton} />
{!isMobile && (
<NavCollapseButton direction="left" show={showCollapseButton} />
)}
</StyledContainer>
);
};

View File

@ -0,0 +1,55 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { navigationDrawerState } from '@/ui/navigation/states/navigationDrawerState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { desktopNavDrawerWidths } from '../constants';
const StyledNavbarContainer = styled(motion.div)`
align-items: end;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
`;
type NavbarAnimatedContainerProps = {
children: ReactNode;
};
export const NavbarAnimatedContainer = ({
children,
}: NavbarAnimatedContainerProps) => {
const navigationDrawer = useRecoilValue(navigationDrawerState);
const isInSubMenu = useIsSettingsPage();
const theme = useTheme();
const isMobile = useIsMobile();
const desktopWidth =
navigationDrawer === ''
? 12
: isInSubMenu
? desktopNavDrawerWidths.submenu
: desktopNavDrawerWidths.menu;
return (
<StyledNavbarContainer
initial={false}
animate={{
width: !isMobile ? desktopWidth : navigationDrawer ? '100%' : 0,
opacity: navigationDrawer === '' ? 0 : 1,
}}
transition={{
duration: theme.animation.duration.normal,
}}
>
{children}
</StyledNavbarContainer>
);
};

View File

@ -1,17 +1,19 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconBrandGithub } from '@/ui/display/icon';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import packageJson from '../../../../../../package.json';
import { githubLink, leftNavbarWidth } from '../constants';
import { desktopNavDrawerWidths, githubLink } from '../constants';
import NavBackButton from './NavBackButton';
import NavItemsContainer from './NavItemsContainer';
type SubMenuNavbarProps = {
children: React.ReactNode;
children: ReactNode;
backButtonTitle: string;
displayVersion?: boolean;
};
@ -25,10 +27,11 @@ const StyledVersionContainer = styled.div`
const StyledVersion = styled.span`
color: ${({ theme }) => theme.font.color.light};
padding-left: ${({ theme }) => theme.spacing(1)};
:hover {
color: ${({ theme }) => theme.font.color.tertiary};
}
padding-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledVersionLink = styled.a`
@ -36,18 +39,25 @@ const StyledVersionLink = styled.a`
color: ${({ theme }) => theme.font.color.light};
display: flex;
text-decoration: none;
:hover {
color: ${({ theme }) => theme.font.color.tertiary};
}
`;
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
padding-top: ${({ theme }) => theme.spacing(9)};
width: ${() => (useIsMobile() ? '100%' : leftNavbarWidth.desktop)};
padding: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(11)};
width: ${desktopNavDrawerWidths.menu};
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: 100%;
}
`;
const SubMenuNavbar = ({
@ -56,13 +66,14 @@ const SubMenuNavbar = ({
displayVersion,
}: SubMenuNavbarProps) => {
const version = packageJson.version;
const isMobile = useIsMobile();
const theme = useTheme();
return (
<StyledContainer>
<div>
<NavBackButton title={backButtonTitle} />
{!isMobile && <NavBackButton title={backButtonTitle} />}
<NavItemsContainer>{children}</NavItemsContainer>
</div>
{displayVersion && (

View File

@ -18,7 +18,7 @@ import NavItem from '../NavItem';
import NavTitle from '../NavTitle';
const meta: Meta<typeof MainNavbar> = {
title: 'UI/Navigation/Navbar/MainNavbar',
title: 'UI/Navigation/NavigationDrawer/MainNavbar',
component: MainNavbar,
decorators: [SnackBarDecorator],
};

View File

@ -5,7 +5,7 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import NavCollapseButton from '../NavCollapseButton';
const meta: Meta<typeof NavCollapseButton> = {
title: 'UI/Navigation/Navbar/NavCollapseButton',
title: 'UI/Navigation/NavigationDrawer/NavCollapseButton',
component: NavCollapseButton,
};

View File

@ -9,8 +9,14 @@ import { CatalogStory } from '~/testing/types';
import NavItem from '../NavItem';
const meta: Meta<typeof NavItem> = {
title: 'UI/Navigation/Navbar/NavItem',
title: 'UI/Navigation/NavigationDrawer/NavItem',
component: NavItem,
args: {
label: 'Search',
Icon: IconSearch,
active: true,
},
argTypes: { Icon: { control: false } },
};
const StyledNavItemContainer = styled.div`
@ -28,19 +34,11 @@ const ComponentDecorator: Decorator = (Story) => (
export default meta;
type Story = StoryObj<typeof NavItem>;
export const Default: Story = {
args: {
label: 'Search',
Icon: IconSearch,
onClick: () => console.log('clicked'),
active: true,
},
argTypes: { Icon: { control: false }, onClick: { control: false } },
export const Default: Story = {
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
};
export const Catalog: CatalogStory<Story, typeof NavItem> = {
args: Default.args,
decorators: [
ComponentDecorator,
CatalogDecorator,
@ -75,21 +73,28 @@ export const Catalog: CatalogStory<Story, typeof NavItem> = {
},
};
export const Soon: Story = {
export const WithSoonPill: Story = {
...Default,
args: {
...Default.args,
active: false,
soon: true,
},
argTypes: { Icon: { control: false }, onClick: { control: false } },
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
};
export const Count: Story = {
export const WithCount: Story = {
...Default,
args: {
...Default.args,
count: 3,
},
argTypes: { Icon: { control: false }, onClick: { control: false } },
decorators: [ComponentDecorator, ComponentWithRouterDecorator],
};
export const WithKeyboardKeys: Story = {
...Default,
args: {
className: "hover",
keyboard: ['⌘', 'K'],
},
parameters: {
pseudo: { hover: [".hover"] },
}
};

View File

@ -14,7 +14,7 @@ import NavTitle from '../NavTitle';
import SubMenuNavbar from '../SubMenuNavbar';
const meta: Meta<typeof SubMenuNavbar> = {
title: 'UI/Navigation/Navbar/SubMenuNavbar',
title: 'UI/Navigation/NavigationDrawer/SubMenuNavbar',
component: SubMenuNavbar,
};

View File

@ -0,0 +1,6 @@
export const desktopNavDrawerWidths = {
menu: '236px',
submenu: '536px',
};
export const githubLink = 'https://github.com/twentyhq/twenty';

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const navigationDrawerState = atom<'main' | 'settings' | ''>({
key: 'ui/navigationDrawerState',
default: 'main',
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const navigationMemorizedUrlState = atom<string>({
key: 'navigationMemorizedUrlState',
default: '/',
});