Skip to content

Commit

Permalink
feat: navigator render extra item (#688)
Browse files Browse the repository at this point in the history
* feat: add a menu for item actions

* feat: enhance navigation to receive extra items

* feat: display seperator even if there's no item

---------

Co-authored-by: kim <[email protected]>
  • Loading branch information
LinaYahya and pyphilia authored Feb 6, 2024
1 parent 7c9cad1 commit 2458c90
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 59 deletions.
56 changes: 56 additions & 0 deletions src/Navigation/CurrentItemNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import truncate from 'lodash.truncate';

import { Typography } from '@mui/material';

import { DiscriminatedItem, ItemType } from '@graasp/sdk';

import ItemMenu, { ItemMenuProps } from './ItemMenu';
import { CenterAlignWrapper, ITEM_NAME_MAX_LENGTH, StyledLink } from './utils';

export interface CurrentItemProps {
item: DiscriminatedItem;
buildBreadcrumbsItemLinkId?: (id: string) => string;
buildIconId?: (id: string) => string;
buildMenuId?: (id: string) => string;
buildMenuItemId?: (id: string) => string;
useChildren: ItemMenuProps['useChildren'];
buildToItemPath: (id: string) => string;
showArrow: boolean;
}
const CurrentItemNavigation = ({
item,
buildBreadcrumbsItemLinkId,
buildToItemPath,
useChildren,
buildIconId,
buildMenuId,
buildMenuItemId,
showArrow,
}: CurrentItemProps): JSX.Element | null => {
return (
<CenterAlignWrapper>
<StyledLink
id={buildBreadcrumbsItemLinkId?.(item.id)}
key={item.id}
to={buildToItemPath(item?.id)}
>
<Typography>
{truncate(item.name, { length: ITEM_NAME_MAX_LENGTH })}
</Typography>
</StyledLink>
{(item.type === ItemType.FOLDER || showArrow) && (
<ItemMenu
useChildren={useChildren}
itemId={item.id}
buildToItemPath={buildToItemPath}
buildIconId={buildIconId}
buildMenuItemId={buildMenuItemId}
buildMenuId={buildMenuId}
renderArrow={showArrow}
/>
)}
</CenterAlignWrapper>
);
};

export default CurrentItemNavigation;
69 changes: 69 additions & 0 deletions src/Navigation/ExtraItemsMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { IconButtonProps, Menu, MenuItem, Typography } from '@mui/material';

import React from 'react';
import { Link } from 'react-router-dom';

import { MenuItemType } from './Navigation';
import { Separator, StyledIconButton } from './utils';

export type ExtraItemsMenuProps = {
icon?: JSX.Element;
menuItems: MenuItemType[];
buildIconId?: (id: string) => string;
buildMenuId?: (itemId: string) => string;
name: string;
};

const ExtraItemsMenu = ({
icon = Separator,
menuItems,
buildIconId,
buildMenuId,
name,
}: ExtraItemsMenuProps): JSX.Element => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick: IconButtonProps['onClick'] = (event) => {
setAnchorEl(event.currentTarget);
};

const handleClose = (): void => {
setAnchorEl(null);
};

return (
<>
<StyledIconButton
onClick={handleClick}
aria-haspopup='true'
id={buildIconId?.(name)}
aria-expanded={open ? true : undefined}
>
{icon}
</StyledIconButton>
<Menu
anchorEl={anchorEl}
open={open}
id={buildMenuId?.(name)}
onClose={handleClose}
onClick={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
{menuItems?.map(({ name, path }) => (
<MenuItem key={name} component={Link} to={path}>
<Typography>{name}</Typography>
</MenuItem>
))}
</Menu>
</>
);
};

export default ExtraItemsMenu;
41 changes: 41 additions & 0 deletions src/Navigation/ExtraItemsNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import truncate from 'lodash.truncate';

import { Box, SvgIconTypeMap, Typography } from '@mui/material';
import { OverridableComponent } from '@mui/material/OverridableComponent';

import ExtraItemsMenu from './ExtraItemsMenu';
import { MenuItemType } from './Navigation';
import { CenterAlignWrapper, ITEM_NAME_MAX_LENGTH, StyledLink } from './utils';

export interface ExtraItem {
name: string;
path: string;
Icon?: OverridableComponent<SvgIconTypeMap>;
menuItems?: MenuItemType[];
}

const ExtraItemsNavigation = ({
extraItems,
}: {
extraItems: ExtraItem[];
}): JSX.Element[] | null => {
return extraItems.map(({ Icon, name, path, menuItems }) => (
<CenterAlignWrapper>
{/* margin set to -2 as menu list has a default style for text indent
with the same value, So to align menu items with this box menu item */}
<Box display='flex' gap={2} ml={-2}>
{Icon && <Icon />}
<StyledLink to={path}>
<Typography>
{truncate(name, { length: ITEM_NAME_MAX_LENGTH })}
</Typography>
</StyledLink>
</Box>
{menuItems && menuItems.length > 0 && (
<ExtraItemsMenu menuItems={menuItems} name={name} />
)}
</CenterAlignWrapper>
));
};

export default ExtraItemsNavigation;
7 changes: 6 additions & 1 deletion src/Navigation/ItemMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type ItemMenuProps = {
icon?: JSX.Element;
itemId: string;
useChildren: (...args: unknown[]) => UseQueryResult<DiscriminatedItem[]>;
renderArrow?: boolean;
};

const ItemMenu = ({
Expand All @@ -26,6 +27,7 @@ const ItemMenu = ({
icon = Separator,
itemId,
useChildren,
renderArrow,
}: ItemMenuProps): JSX.Element | null => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
Expand All @@ -40,10 +42,13 @@ const ItemMenu = ({
setAnchorEl(null);
};

if (!items?.length && renderArrow) {
// to display icon as a separator specially if there's an extra items after items menu
return icon;
}
if (!items?.length) {
return null;
}

return (
<>
<StyledIconButton
Expand Down
58 changes: 58 additions & 0 deletions src/Navigation/Navigation.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { expect } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library';

import SettingsIcon from '@mui/icons-material/Settings';

import { BrowserRouter } from 'react-router-dom';

import { ItemType, LocalFileItemType, MimeTypes } from '@graasp/sdk';
Expand Down Expand Up @@ -196,3 +198,59 @@ export const FileWithParents: Story = {
expect(canvas.getAllByTestId(dataTestId)).toHaveLength(4);
},
};

const extraItems = [
{
name: 'Settings',
path: '/settings',
Icon: SettingsIcon,
menuItems: [
{ name: 'Information', path: '/info' },
{ name: 'Settings', path: '/settings' },
{ name: 'Publish', path: '/publish' },
],
},
];

export const FolderWithParentsWithExtraItems: Story = {
args: {
buildToItemPath,
useChildren,
item: folder,
maxItems: 10,
renderRoot: () => {
return (
<>
<HomeMenu selected={menu[0]} elements={menu} />
<ItemMenu
itemId={item.id}
useChildren={() => {
return {
data: [buildItem('Home item 1'), buildItem('Home item 2')],
} as UseChildrenHookType;
}}
buildToItemPath={buildToItemPath}
/>
</>
);
},
parents,
extraItems,
},

play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// current item
expect(canvas.getByText(folder.name)).toBeInTheDocument();

// check parents
for (const p of parents) {
const b = canvas.getByText(p!.name);
expect(b).toBeInTheDocument();
}

// 4 = 2 parents + 2 x Home + current item is a folder + 1 extra item
expect(canvas.getAllByTestId(dataTestId)).toHaveLength(6);
},
};
91 changes: 33 additions & 58 deletions src/Navigation/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import truncate from 'lodash.truncate';

import { SxProps } from '@mui/material';
import Breadcrumbs from '@mui/material/Breadcrumbs';
import Typography from '@mui/material/Typography';
import { styled } from '@mui/material/styles';

import { DiscriminatedItem, ItemType } from '@graasp/sdk';
import { DiscriminatedItem } from '@graasp/sdk';

import ItemMenu, { ItemMenuProps } from './ItemMenu';
import { CenterAlignWrapper, ITEM_NAME_MAX_LENGTH, StyledLink } from './utils';
import CurrentItemNavigation from './CurrentItemNavigation';
import ExtraItemsNavigation, { ExtraItem } from './ExtraItemsNavigation';
import { ItemMenuProps } from './ItemMenu';
import ParentsNavigation from './ParentsNavigation';
import { CenterAlignWrapper } from './utils';

const StyledBreadcrumbs = styled(Breadcrumbs)(({ theme }) => ({
'& ol': {
Expand All @@ -30,7 +30,12 @@ export type NavigationProps = {
sx?: SxProps;
useChildren: ItemMenuProps['useChildren'];
maxItems?: number;
extraItems?: ExtraItem[];
};
export interface MenuItemType {
name: string;
path: string;
}

const Navigation = ({
backgroundColor,
Expand All @@ -46,57 +51,8 @@ const Navigation = ({
useChildren,
buildMenuId,
maxItems = 4,
extraItems,
}: NavigationProps): JSX.Element | null => {
const renderParents = (): JSX.Element[] | undefined =>
// need to convert otherwise it returns List<Element>
parents?.map(({ name, id }) => (
<CenterAlignWrapper key={id}>
<StyledLink
id={buildBreadcrumbsItemLinkId?.(id)}
to={buildToItemPath(id)}
>
<Typography>
{truncate(name, { length: ITEM_NAME_MAX_LENGTH })}
</Typography>
</StyledLink>
<ItemMenu
useChildren={useChildren}
itemId={id}
buildToItemPath={buildToItemPath}
/>
</CenterAlignWrapper>
));

const renderCurrentItem = (): JSX.Element | null => {
if (!item) {
return null;
}

return (
<CenterAlignWrapper>
<StyledLink
id={buildBreadcrumbsItemLinkId?.(item.id)}
key={item.id}
to={buildToItemPath(item?.id)}
>
<Typography>
{truncate(item.name, { length: ITEM_NAME_MAX_LENGTH })}
</Typography>
</StyledLink>
{item.type === ItemType.FOLDER && (
<ItemMenu
useChildren={useChildren}
itemId={item.id}
buildToItemPath={buildToItemPath}
buildIconId={buildIconId}
buildMenuItemId={buildMenuItemId}
buildMenuId={buildMenuId}
/>
)}
</CenterAlignWrapper>
);
};

return (
<StyledBreadcrumbs
sx={sx}
Expand All @@ -107,8 +63,27 @@ const Navigation = ({
style={{ backgroundColor }}
>
<CenterAlignWrapper>{renderRoot?.(item)}</CenterAlignWrapper>
{item?.id && renderParents()}
{item?.id && renderCurrentItem()}
{item?.id && parents && (
<ParentsNavigation
useChildren={useChildren}
parents={parents}
buildToItemPath={buildToItemPath}
buildBreadcrumbsItemLinkId={buildBreadcrumbsItemLinkId}
/>
)}
{item?.id && item && (
<CurrentItemNavigation
item={item}
useChildren={useChildren}
buildToItemPath={buildToItemPath}
buildBreadcrumbsItemLinkId={buildBreadcrumbsItemLinkId}
buildIconId={buildIconId}
buildMenuId={buildMenuId}
buildMenuItemId={buildMenuItemId}
showArrow={Boolean(extraItems?.length)}
/>
)}
{extraItems && <ExtraItemsNavigation extraItems={extraItems} />}
</StyledBreadcrumbs>
);
};
Expand Down
Loading

0 comments on commit 2458c90

Please sign in to comment.