Created a breadcrumb for left nav menu sub items (#6762)

Closes https://github.com/twentyhq/twenty/issues/6484

<img width="270" alt="image"
src="https://github.com/user-attachments/assets/3cfd7a5a-5239-4998-87f7-a9b45e3b5229">
This commit is contained in:
Lucas Bordeau
2024-08-30 15:10:18 +02:00
committed by GitHub
parent 09ac8e3274
commit 26eba76fb5
16 changed files with 450 additions and 165 deletions

View File

@ -4,7 +4,7 @@ import { IconSearch, IconSettings } from 'twenty-ui';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { Favorites } from '@/favorites/components/Favorites';
import { ObjectMetadataNavItems } from '@/object-metadata/components/ObjectMetadataNavItems';
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
@ -41,8 +41,8 @@ export const MainNavigationDrawerItems = () => {
<Favorites />
<ObjectMetadataNavItems isRemote={false} />
<ObjectMetadataNavItems isRemote={true} />
<NavigationDrawerSectionForObjectMetadataItems isRemote={false} />
<NavigationDrawerSectionForObjectMetadataItems isRemote={true} />
</>
);
};

View File

@ -1,11 +1,10 @@
import { AnimatePresence, motion } from 'framer-motion';
import { useLocation } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { isDefined, useIcons } from 'twenty-ui';
import { currentUserState } from '@/auth/states/currentUserState';
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
import { ObjectMetadataNavItemsSkeletonLoader } from '@/object-metadata/components/ObjectMetadataNavItemsSkeletonLoader';
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
@ -15,9 +14,9 @@ import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/compo
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState';
import { View } from '@/views/types/View';
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
import { Theme, useTheme } from '@emotion/react';
const ORDERED_STANDARD_OBJECTS = [
'person',
@ -27,20 +26,11 @@ const ORDERED_STANDARD_OBJECTS = [
'note',
];
const navItemsAnimationVariants = (theme: Theme) => ({
hidden: {
height: 0,
opacity: 0,
marginTop: 0,
},
visible: {
height: 'auto',
opacity: 1,
marginTop: theme.spacing(1),
},
});
export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => {
export const NavigationDrawerSectionForObjectMetadataItems = ({
isRemote,
}: {
isRemote: boolean;
}) => {
const currentUser = useRecoilValue(currentUserState);
const { toggleNavigationSection, isNavigationSectionOpenState } =
@ -57,13 +47,13 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => {
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const loading = useIsPrefetchLoading();
const theme = useTheme();
const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
if (loading && isDefined(currentUser)) {
return <ObjectMetadataNavItemsSkeletonLoader />;
return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
}
// TODO: refactor this by splitting into separate components
return (
filteredActiveObjectMetadataItems.length > 0 && (
<NavigationDrawerSection>
@ -121,6 +111,17 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => {
currentPath === `/objects/${objectMetadataItem.namePlural}` &&
objectMetadataViews.length > 1;
const sortedObjectMetadataViews = [...objectMetadataViews].sort(
(viewA, viewB) =>
viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position,
);
const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(
(view) => viewId === view.id,
);
const subItemArrayLength = sortedObjectMetadataViews.length;
return (
<div key={objectMetadataItem.id}>
<NavigationDrawerItem
@ -132,33 +133,21 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => {
currentPath === `/objects/${objectMetadataItem.namePlural}`
}
/>
<AnimatePresence>
{shouldSubItemsBeDisplayed && (
<motion.div
initial="hidden"
animate="visible"
exit="hidden"
variants={navItemsAnimationVariants(theme)}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
{objectMetadataViews
.sort((viewA, viewB) =>
viewA.key === 'INDEX'
? -1
: viewA.position - viewB.position,
)
.map((view) => (
<NavigationDrawerSubItem
label={view.name}
to={`/objects/${objectMetadataItem.namePlural}?view=${view.id}`}
active={viewId === view.id}
Icon={getIcon(view.icon)}
key={view.id}
/>
))}
</motion.div>
)}
</AnimatePresence>
{shouldSubItemsBeDisplayed &&
sortedObjectMetadataViews.map((view, index) => (
<NavigationDrawerSubItem
label={view.name}
to={`/objects/${objectMetadataItem.namePlural}?view=${view.id}`}
active={viewId === view.id}
subItemState={getNavigationSubItemState({
index,
arrayLength: subItemArrayLength,
selectedIndex: selectedSubItemIndex,
})}
Icon={getIcon(view.icon)}
key={view.id}
/>
))}
</div>
);
})}

View File

@ -0,0 +1,29 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
const StyledSkeletonColumn = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
height: 76px;
padding-left: ${({ theme }) => theme.spacing(1)};
`;
export const NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader: React.FC =
() => {
const theme = useTheme();
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.light}
borderRadius={4}
>
<StyledSkeletonColumn>
<Skeleton width={196} height={16} />
<Skeleton width={196} height={16} />
<Skeleton width={196} height={16} />
</StyledSkeletonColumn>
</SkeletonTheme>
);
};

View File

@ -1,28 +0,0 @@
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
const StyledSkeletonColumn = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
height: 76px;
padding-left: ${({ theme }) => theme.spacing(1)};
`;
export const ObjectMetadataNavItemsSkeletonLoader: React.FC = () => {
const theme = useTheme();
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.light}
borderRadius={4}
>
<StyledSkeletonColumn>
<Skeleton width={196} height={16} />
<Skeleton width={196} height={16} />
<Skeleton width={196} height={16} />
</StyledSkeletonColumn>
</SkeletonTheme>
);
};

View File

@ -9,12 +9,12 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
import { PrefetchLoadedDecorator } from '~/testing/decorators/PrefetchLoadedDecorator';
import { ObjectMetadataNavItems } from '../ObjectMetadataNavItems';
const meta: Meta<typeof ObjectMetadataNavItems> = {
title: 'Modules/ObjectMetadata/ObjectMetadataNavItems',
component: ObjectMetadataNavItems,
const meta: Meta<typeof NavigationDrawerSectionForObjectMetadataItems> = {
title: 'Modules/ObjectMetadata/NavigationDrawerSectionForObjectMetadataItems',
component: NavigationDrawerSectionForObjectMetadataItems,
decorators: [
IconsProviderDecorator,
ObjectMetadataItemsDecorator,
@ -29,7 +29,7 @@ const meta: Meta<typeof ObjectMetadataNavItems> = {
};
export default meta;
type Story = StoryObj<typeof ObjectMetadataNavItems>;
type Story = StoryObj<typeof NavigationDrawerSectionForObjectMetadataItems>;
export const Default: Story = {
play: async () => {

View File

@ -63,7 +63,7 @@ export const useHandleToggleTrashColumnFilter = ({
upsertCombinedViewFilter(newFilter);
}, [
columnDefinitions,
objectMetadataItem.fields,
objectMetadataItem,
objectNameSingular,
upsertCombinedViewFilter,
]);

View File

@ -6,32 +6,38 @@ import {
NavigationDrawerItem,
NavigationDrawerItemProps,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
type SettingsNavigationDrawerItemProps = Pick<
NavigationDrawerItemProps,
'Icon' | 'label' | 'level' | 'soon'
'Icon' | 'label' | 'indentationLevel' | 'soon'
> & {
matchSubPages?: boolean;
path: SettingsPath;
subItemState?: NavigationDrawerSubItemState;
};
export const SettingsNavigationDrawerItem = ({
Icon,
label,
level,
indentationLevel,
matchSubPages = false,
path,
soon,
subItemState,
}: SettingsNavigationDrawerItemProps) => {
const href = getSettingsPagePath(path);
const pathName = useResolvedPath(href).pathname;
const isActive = !!useMatch({
path: useResolvedPath(href).pathname,
path: pathName,
end: !matchSubPages,
});
return (
<NavigationDrawerItem
level={level}
indentationLevel={indentationLevel}
subItemState={subItemState}
label={label}
to={href}
Icon={Icon}

View File

@ -5,6 +5,7 @@ import {
IconCalendarEvent,
IconCode,
IconColorSwatch,
IconComponent,
IconCurrencyDollar,
IconDoorEnter,
IconFunction,
@ -19,12 +20,26 @@ import {
import { useAuth } from '@/auth/hooks/useAuth';
import { billingState } from '@/client-config/states/billingState';
import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import {
NavigationDrawerItem,
NavigationDrawerItemIndentationLevel,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemGroup } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemGroup';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { matchPath, resolvePath, useLocation } from 'react-router-dom';
type SettingsNavigationItem = {
label: string;
path: SettingsPath;
Icon: IconComponent;
matchSubPages?: boolean;
indentationLevel?: NavigationDrawerItemIndentationLevel;
};
export const SettingsNavigationDrawerItems = () => {
const { signOut } = useAuth();
@ -38,6 +53,39 @@ export const SettingsNavigationDrawerItems = () => {
const isBillingPageEnabled =
billing?.isBillingEnabled && !isFreeAccessEnabled;
// TODO: Refactor this part to only have arrays of navigation items
const currentPathName = useLocation().pathname;
const accountSubSettings: SettingsNavigationItem[] = [
{
label: 'Emails',
path: SettingsPath.AccountsEmails,
Icon: IconMail,
matchSubPages: true,
indentationLevel: 2,
},
{
label: 'Calendars',
path: SettingsPath.AccountsCalendars,
Icon: IconCalendarEvent,
matchSubPages: true,
indentationLevel: 2,
},
];
const selectedIndex = accountSubSettings.findIndex((accountSubSetting) => {
const href = getSettingsPagePath(accountSubSetting.path);
const pathName = resolvePath(href).pathname;
return matchPath(
{
path: pathName,
end: !accountSubSetting.matchSubPages,
},
currentPathName,
);
});
return (
<>
<NavigationDrawerSection>
@ -58,23 +106,21 @@ export const SettingsNavigationDrawerItems = () => {
path={SettingsPath.Accounts}
Icon={IconAt}
/>
<SettingsNavigationDrawerItem
level={2}
label="Emails"
path={SettingsPath.AccountsEmails}
Icon={IconMail}
matchSubPages
/>
<SettingsNavigationDrawerItem
level={2}
label="Calendars"
path={SettingsPath.AccountsCalendars}
Icon={IconCalendarEvent}
matchSubPages
/>
{accountSubSettings.map((navigationItem, index) => (
<SettingsNavigationDrawerItem
label={navigationItem.label}
path={navigationItem.path}
Icon={navigationItem.Icon}
indentationLevel={navigationItem.indentationLevel}
subItemState={getNavigationSubItemState({
arrayLength: accountSubSettings.length,
index,
selectedIndex,
})}
/>
))}
</NavigationDrawerItemGroup>
</NavigationDrawerSection>
<NavigationDrawerSection>
<NavigationDrawerSectionTitle label="Workspace" />
<SettingsNavigationDrawerItem
@ -125,7 +171,6 @@ export const SettingsNavigationDrawerItems = () => {
/>
)}
</NavigationDrawerSection>
<NavigationDrawerSection>
<NavigationDrawerSectionTitle label="Other" />
<SettingsNavigationDrawerItem

View File

@ -1,3 +1,5 @@
import { NavigationDrawerItemBreadcrumb } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb';
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import isPropValid from '@emotion/is-prop-valid';
@ -9,10 +11,15 @@ import { useSetRecoilState } from 'recoil';
import { IconComponent, MOBILE_VIEWPORT, Pill } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
const DEFAULT_INDENTATION_LEVEL = 1;
export type NavigationDrawerItemIndentationLevel = 1 | 2;
export type NavigationDrawerItemProps = {
className?: string;
label: string;
level?: 1 | 2;
indentationLevel?: NavigationDrawerItemIndentationLevel;
subItemState?: NavigationDrawerSubItemState;
to?: string;
onClick?: () => void;
Icon: IconComponent;
@ -23,13 +30,10 @@ export type NavigationDrawerItemProps = {
keyboard?: string[];
};
type StyledItemProps = {
active?: boolean;
danger?: boolean;
level: 1 | 2;
soon?: boolean;
to?: string;
};
type StyledItemProps = Pick<
NavigationDrawerItemProps,
'active' | 'danger' | 'indentationLevel' | 'soon' | 'to'
>;
const StyledItem = styled('div', {
shouldForwardProp: (prop) =>
@ -59,13 +63,17 @@ const StyledItem = styled('div', {
font-family: 'Inter';
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(2)};
margin-left: ${({ level, theme }) => theme.spacing((level - 1) * 4)};
padding-bottom: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(1)};
padding-right: ${({ theme }) => theme.spacing(1)};
padding-top: ${({ theme }) => theme.spacing(1)};
pointer-events: ${(props) => (props.soon ? 'none' : 'auto')};
margin-top: ${({ indentationLevel }) =>
indentationLevel === 2 ? '2px' : '0'};
pointer-events: ${(props) => (props.soon ? 'none' : 'auto')};
width: 100%;
:hover {
background: ${({ theme }) => theme.background.transparent.light};
color: ${(props) =>
@ -116,10 +124,16 @@ const StyledKeyBoardShortcut = styled.div`
visibility: hidden;
`;
const StyledNavigationDrawerItemContainer = styled.div`
display: flex;
flex-grow: 1;
width: 100%;
`;
export const NavigationDrawerItem = ({
className,
label,
level = 1,
indentationLevel = DEFAULT_INDENTATION_LEVEL,
Icon,
to,
onClick,
@ -128,6 +142,7 @@ export const NavigationDrawerItem = ({
soon,
count,
keyboard,
subItemState,
}: NavigationDrawerItemProps) => {
const theme = useTheme();
const isMobile = useIsMobile();
@ -136,6 +151,8 @@ export const NavigationDrawerItem = ({
isNavigationDrawerOpenState,
);
const showBreadcrumb = indentationLevel === 2;
const handleItemClick = () => {
if (isMobile) {
setIsNavigationDrawerOpen(false);
@ -152,26 +169,33 @@ export const NavigationDrawerItem = ({
};
return (
<StyledItem
className={className}
level={level}
onClick={handleItemClick}
active={active}
aria-selected={active}
danger={danger}
soon={soon}
as={to ? Link : 'div'}
to={to ? to : undefined}
>
{Icon && <Icon size={theme.icon.size.md} stroke={theme.icon.stroke.md} />}
<StyledItemLabel>{label}</StyledItemLabel>
{soon && <Pill label="Soon" />}
{!!count && <StyledItemCount>{count}</StyledItemCount>}
{keyboard && (
<StyledKeyBoardShortcut className="keyboard-shortcuts">
{keyboard}
</StyledKeyBoardShortcut>
)}
</StyledItem>
<StyledNavigationDrawerItemContainer>
<StyledItem
className={className}
onClick={handleItemClick}
active={active}
aria-selected={active}
danger={danger}
soon={soon}
as={to ? Link : 'div'}
to={to ? to : undefined}
indentationLevel={indentationLevel}
>
{showBreadcrumb && (
<NavigationDrawerItemBreadcrumb state={subItemState} />
)}
{Icon && (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.md} />
)}
<StyledItemLabel>{label}</StyledItemLabel>
{soon && <Pill label="Soon" />}
{!!count && <StyledItemCount>{count}</StyledItemCount>}
{keyboard && (
<StyledKeyBoardShortcut className="keyboard-shortcuts">
{keyboard}
</StyledKeyBoardShortcut>
)}
</StyledItem>
</StyledNavigationDrawerItemContainer>
);
};

View File

@ -0,0 +1,79 @@
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
import styled from '@emotion/styled';
export type NavigationDrawerItemBreadcrumbProps = {
state?: NavigationDrawerSubItemState;
};
const StyledNavigationDrawerItemBreadcrumbContainer = styled.div`
margin-left: 7.5px;
height: 28px;
width: 9px;
`;
const StyledGapVerticalLine = styled.div<{ darker: boolean }>`
background: ${({ theme, darker }) =>
darker ? theme.font.color.tertiary : theme.border.color.strong};
position: relative;
top: -2px;
height: 2px;
width: 1px;
`;
const StyledSecondaryFullVerticalBar = styled.div<{ darker: boolean }>`
background: ${({ theme, darker }) =>
darker ? theme.font.color.tertiary : theme.border.color.strong};
position: relative;
top: -17px;
height: 28px;
width: 1px;
`;
const StyledRoundedProtrusion = styled.div<{ darker: boolean }>`
position: relative;
top: -2px;
border-bottom-left-radius: 4px;
border: 1px solid
${({ theme, darker }) =>
darker ? theme.font.color.tertiary : theme.border.color.strong};
${({ darker }) => (darker ? 'z-index: 1;' : '')}
border-top: none;
border-right: none;
height: 14px;
width: 8px;
`;
export const NavigationDrawerItemBreadcrumb = ({
state,
}: NavigationDrawerItemBreadcrumbProps) => {
const showVerticalBar =
state !== 'last-not-selected' && state !== 'last-selected';
const verticalBarShouldBeDarker = state === 'intermediate-before-selected';
const protrusionShouldBeDarker =
state === 'intermediate-selected' || state === 'last-selected';
const gapShouldBeDarker =
state === 'intermediate-before-selected' ||
state === 'intermediate-selected' ||
state === 'last-selected';
return (
<StyledNavigationDrawerItemBreadcrumbContainer>
<StyledGapVerticalLine darker={gapShouldBeDarker} />
<StyledRoundedProtrusion darker={protrusionShouldBeDarker} />
{showVerticalBar && (
<StyledSecondaryFullVerticalBar darker={verticalBarShouldBeDarker} />
)}
</StyledNavigationDrawerItemBreadcrumbContainer>
);
};

View File

@ -3,7 +3,6 @@ import styled from '@emotion/styled';
const StyledGroup = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(0.5)};
`;
export { StyledGroup as NavigationDrawerItemGroup };

View File

@ -2,21 +2,12 @@ import {
NavigationDrawerItem,
NavigationDrawerItemProps,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import styled from '@emotion/styled';
const StyledItem = styled.div`
&:not(:last-child) {
margin-bottom: ${({ theme }) => theme.spacing(0.5)};
}
margin-left: ${({ theme }) => theme.spacing(4)};
`;
type NavigationDrawerSubItemProps = NavigationDrawerItemProps;
export const NavigationDrawerSubItem = ({
className,
label,
level = 1,
Icon,
to,
onClick,
@ -25,22 +16,22 @@ export const NavigationDrawerSubItem = ({
soon,
count,
keyboard,
subItemState,
}: NavigationDrawerSubItemProps) => {
return (
<StyledItem>
<NavigationDrawerItem
className={className}
label={label}
level={level}
Icon={Icon}
to={to}
onClick={onClick}
active={active}
danger={danger}
soon={soon}
count={count}
keyboard={keyboard}
/>
</StyledItem>
<NavigationDrawerItem
className={className}
label={label}
indentationLevel={2}
subItemState={subItemState}
Icon={Icon}
to={to}
onClick={onClick}
active={active}
danger={danger}
soon={soon}
count={count}
keyboard={keyboard}
/>
);
};

View File

@ -23,6 +23,7 @@ import { GithubVersionLink } from '@/ui/navigation/link/components/GithubVersion
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { NavigationDrawer } from '../NavigationDrawer';
import { NavigationDrawerItem } from '../NavigationDrawerItem';
import { NavigationDrawerItemGroup } from '../NavigationDrawerItemGroup';
@ -108,17 +109,17 @@ export const Submenu: Story = {
to={getSettingsPagePath(SettingsPath.Accounts)}
Icon={IconAt}
/>
<NavigationDrawerItem
level={2}
<NavigationDrawerSubItem
label="Emails"
to={getSettingsPagePath(SettingsPath.AccountsEmails)}
Icon={IconMail}
subItemState="intermediate-before-selected"
/>
<NavigationDrawerItem
level={2}
<NavigationDrawerSubItem
label="Calendar"
to={getSettingsPagePath(SettingsPath.AccountsCalendars)}
Icon={IconCalendarEvent}
subItemState="last-selected"
/>
</NavigationDrawerItemGroup>
</NavigationDrawerSection>

View File

@ -37,6 +37,115 @@ export const Default: Story = {
],
};
export const Breadcrumb: Story = {
decorators: [
(Story) => (
<StyledContainer>
<h1>Breadcrumb</h1>
<Story
args={{
indentationLevel: 1,
label: 'Search',
Icon: IconSearch,
}}
/>
<Story
args={{
indentationLevel: 2,
subItemState: 'intermediate-before-selected',
label: 'First not selected',
Icon: IconSearch,
}}
/>
<Story
args={{
indentationLevel: 2,
subItemState: 'intermediate-before-selected',
label: 'Before selected',
Icon: IconSearch,
}}
/>
<Story
args={{
indentationLevel: 2,
subItemState: 'intermediate-selected',
label: 'Selected',
Icon: IconSearch,
}}
/>
<Story
args={{
indentationLevel: 2,
subItemState: 'intermediate-after-selected',
label: 'After selected',
Icon: IconSearch,
}}
/>
<Story
args={{
indentationLevel: 2,
subItemState: 'last-not-selected',
label: 'Last not selected',
Icon: IconSearch,
}}
/>
</StyledContainer>
),
ComponentWithRouterDecorator,
],
};
export const BreadcrumbCatalog: CatalogStory<
Story,
typeof NavigationDrawerItem
> = {
decorators: [
(Story) => (
<StyledContainer>
<Story />
</StyledContainer>
),
CatalogDecorator,
MemoryRouterDecorator,
],
args: {
indentationLevel: 2,
},
parameters: {
pseudo: { hover: ['.hover'] },
catalog: {
dimensions: [
{
name: 'subItemState',
values: [
'Intermediate before selected',
'Intermediate selected',
'Intermediate after selected',
'Last not selected',
'Last selected',
],
props: (state: string) => {
switch (state) {
case 'Intermediate before selected':
return { subItemState: 'intermediate-before-selected' };
case 'Intermediate selected':
return { subItemState: 'intermediate-selected' };
case 'Intermediate after selected':
return { subItemState: 'intermediate-after-selected' };
case 'Last not selected':
return { subItemState: 'last-not-selected' };
case 'Last selected':
return { subItemState: 'last-selected' };
default:
throw new Error(`Unknown state: ${state}`);
}
},
},
],
},
},
};
export const Catalog: CatalogStory<Story, typeof NavigationDrawerItem> = {
decorators: [
(Story) => (

View File

@ -0,0 +1,6 @@
export type NavigationDrawerSubItemState =
| 'intermediate-before-selected'
| 'intermediate-selected'
| 'intermediate-after-selected'
| 'last-selected'
| 'last-not-selected';

View File

@ -0,0 +1,35 @@
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
export const getNavigationSubItemState = ({
index,
arrayLength,
selectedIndex,
}: {
index: number;
arrayLength: number;
selectedIndex: number;
}): NavigationDrawerSubItemState => {
const thereIsOnlyOneItem = arrayLength === 1;
const itsTheLastItem = index === arrayLength - 1;
const itsTheSelectedItem = index === selectedIndex;
const itsBeforeTheSelectedItem = index < selectedIndex;
if (thereIsOnlyOneItem || itsTheLastItem) {
if (itsTheSelectedItem) {
return 'last-selected';
} else {
return 'last-not-selected';
}
} else {
if (itsTheSelectedItem) {
return 'intermediate-selected';
} else if (itsBeforeTheSelectedItem) {
return 'intermediate-before-selected';
} else {
return 'intermediate-after-selected';
}
}
};