Skip to content

Commit 16eba2e

Browse files
committed
Created new TreeNavigation
1 parent 9713d2c commit 16eba2e

15 files changed

+1097
-16
lines changed

packages/lib/src/sidenav/Sidenav.tsx

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import DxcFlex from "../flex/Flex";
44
import SidenavPropsType, { Logo } from "./types";
55
import DxcDivider from "../divider/Divider";
66
import DxcButton from "../button/Button";
7-
import DxcContextualMenu from "../contextual-menu/ContextualMenu";
87
import DxcImage from "../image/Image";
9-
import { useState } from "react";
8+
import { ReactElement, useState } from "react";
109
import DxcTextInput from "../text-input/TextInput";
10+
import DxcNavigationTree from "../tree-navigation/NavigationTree";
1111

1212
const SidenavContainer = styled.div<{ expanded: boolean }>`
1313
box-sizing: border-box;
@@ -50,6 +50,10 @@ const DxcSidenav = ({ title, children, items, logo, displayGroupLines = false }:
5050

5151
const renderedChildren = typeof children === "function" ? children(isExpanded) : children;
5252

53+
function isLogoObject(logo: Logo | ReactElement): logo is Logo {
54+
return (logo as Logo).src !== undefined;
55+
}
56+
5357
return (
5458
<SidenavContainer expanded={isExpanded}>
5559
<DxcFlex justifyContent={isExpanded ? "space-between" : "center"}>
@@ -66,17 +70,22 @@ const DxcSidenav = ({ title, children, items, logo, displayGroupLines = false }:
6670
<DxcFlex direction="column" gap="var(--spacing-gap-m)" justifyContent="center" alignItems="flex-start">
6771
{/* TODO: ADD GORGORITO TO COVER CASES WITH NO ICON? */}
6872
{logo && (
69-
<LogoContainer
70-
onClick={logo?.onClick}
71-
hasAction={!!logo?.onClick || !!logo?.href}
72-
role={logo?.onClick ? "button" : logo?.href ? "link" : "presentation"}
73-
as={logo?.href ? "a" : undefined}
74-
href={logo?.href}
75-
aria-label={(logo?.onClick || logo?.href) && (title || "Avatar")}
76-
// tabIndex={logo?.onClick || logo?.href ? tabIndex : undefined}
77-
>
78-
<DxcImage alt={logo?.alt} src={logo?.src} height="100%" width="100%" />
79-
</LogoContainer>
73+
<>
74+
{isLogoObject(logo) ? (
75+
<LogoContainer
76+
onClick={logo.onClick}
77+
hasAction={!!logo.onClick || !!logo.href}
78+
role={logo.onClick ? "button" : logo.href ? "link" : "presentation"}
79+
as={logo.href ? "a" : undefined}
80+
href={logo.href}
81+
aria-label={(logo.onClick || logo.href) && (title || "Avatar")}
82+
>
83+
<DxcImage alt={logo.alt ?? ""} src={logo.src} height="100%" width="100%" />
84+
</LogoContainer>
85+
) : (
86+
logo
87+
)}
88+
</>
8089
)}
8190
<SidenavTitle>{title}</SidenavTitle>
8291
</DxcFlex>
@@ -100,7 +109,7 @@ const DxcSidenav = ({ title, children, items, logo, displayGroupLines = false }:
100109
}}
101110
/> */}
102111
{items && (
103-
<DxcContextualMenu
112+
<DxcNavigationTree
104113
items={items}
105114
displayGroupLines={displayGroupLines}
106115
displayBorder={false}

packages/lib/src/sidenav/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export type Logo = {
7676
/**
7777
* Alternative text for the logo image.
7878
*/
79-
alt: string;
79+
alt?: string;
8080
/**
8181
* URL to navigate to when the logo is clicked. If not provided, the logo will not be clickable.
8282
*/
@@ -107,7 +107,7 @@ type Props = {
107107
/**
108108
* Object with the properties of the logo placed at the top of the sidenav.
109109
*/
110-
logo?: Logo;
110+
logo?: Logo | ReactElement;
111111
/**
112112
* If true the nav menu will have lines marking the groups.
113113
*/
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { useContext, useMemo, useState, useId } from "react";
2+
import DxcIcon from "../icon/Icon";
3+
import SubMenu from "./SubMenu";
4+
import ItemAction from "./ItemAction";
5+
import MenuItem from "./MenuItem";
6+
import { GroupItemProps } from "./types";
7+
import NavigationTreeContext from "./NavigationTreeContext";
8+
import { isGroupSelected } from "./utils";
9+
import * as Popover from "@radix-ui/react-popover";
10+
11+
const GroupItem = ({ items, ...props }: GroupItemProps) => {
12+
const groupMenuId = `group-menu-${useId()}`;
13+
const { selectedItemId, responsiveView } = useContext(NavigationTreeContext) ?? {};
14+
const groupSelected = useMemo(() => isGroupSelected(items, selectedItemId), [items, selectedItemId]);
15+
const [isOpen, setIsOpen] = useState(groupSelected && selectedItemId === -1);
16+
17+
const NavigationTreeId = `sidenav-${useId()}`;
18+
19+
const contextValue = useContext(NavigationTreeContext) ?? {};
20+
21+
return responsiveView ? (
22+
<>
23+
<Popover.Root open={isOpen}>
24+
<Popover.Trigger
25+
aria-controls={undefined}
26+
aria-expanded={undefined}
27+
aria-haspopup={undefined}
28+
asChild
29+
type={undefined}
30+
>
31+
<ItemAction
32+
aria-controls={isOpen ? groupMenuId : undefined}
33+
aria-expanded={isOpen ? true : undefined}
34+
aria-pressed={groupSelected && !isOpen}
35+
collapseIcon={isOpen ? <DxcIcon icon="filled_expand_less" /> : <DxcIcon icon="filled_expand_more" />}
36+
onClick={() => setIsOpen((isCurrentlyOpen) => !isCurrentlyOpen)}
37+
selected={groupSelected && !isOpen}
38+
{...props}
39+
/>
40+
</Popover.Trigger>
41+
<Popover.Portal container={document.getElementById(`${NavigationTreeId}-portal`)}>
42+
<NavigationTreeContext.Provider value={{ ...contextValue, displayGroupLines: false, responsiveView: false }}>
43+
<Popover.Content
44+
aria-label="Group details"
45+
onCloseAutoFocus={(event) => {
46+
event.preventDefault();
47+
}}
48+
onOpenAutoFocus={(event) => {
49+
event.preventDefault();
50+
}}
51+
align="start"
52+
side="right"
53+
style={{ zIndex: "var(--z-contextualmenu)" }}
54+
>
55+
<SubMenu id={groupMenuId} depthLevel={props.depthLevel}>
56+
{items.map((item, index) => (
57+
<MenuItem item={item} depthLevel={props.depthLevel + 1} key={`${item.label}-${index}`} />
58+
))}
59+
</SubMenu>
60+
</Popover.Content>
61+
</NavigationTreeContext.Provider>
62+
</Popover.Portal>
63+
</Popover.Root>
64+
<div id={`${NavigationTreeId}-portal`} style={{ position: "absolute" }} />
65+
</>
66+
) : (
67+
<>
68+
<ItemAction
69+
aria-controls={isOpen ? groupMenuId : undefined}
70+
aria-expanded={isOpen ? true : undefined}
71+
aria-pressed={groupSelected && !isOpen}
72+
collapseIcon={isOpen ? <DxcIcon icon="filled_expand_less" /> : <DxcIcon icon="filled_expand_more" />}
73+
onClick={() => setIsOpen((isCurrentlyOpen) => !isCurrentlyOpen)}
74+
selected={groupSelected && !isOpen}
75+
{...props}
76+
/>
77+
{isOpen && (
78+
<SubMenu id={groupMenuId} depthLevel={props.depthLevel}>
79+
{items.map((item, index) => (
80+
<MenuItem item={item} depthLevel={props.depthLevel + 1} key={`${item.label}-${index}`} />
81+
))}
82+
</SubMenu>
83+
)}
84+
</>
85+
);
86+
};
87+
88+
export default GroupItem;
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { cloneElement, forwardRef, memo, MouseEvent, useContext, useState } from "react";
2+
import styled from "@emotion/styled";
3+
import { ItemActionProps } from "./types";
4+
import DxcIcon from "../icon/Icon";
5+
import { TooltipWrapper } from "../tooltip/Tooltip";
6+
import NavigationTreeContext from "./NavigationTreeContext";
7+
8+
const Action = styled.button<{
9+
depthLevel: ItemActionProps["depthLevel"];
10+
selected: ItemActionProps["selected"];
11+
displayGroupLines: boolean;
12+
responsiveView?: boolean;
13+
}>`
14+
box-sizing: content-box;
15+
border: none;
16+
border-radius: var(--border-radius-s);
17+
${({ displayGroupLines, depthLevel, responsiveView }) => `
18+
${!responsiveView ? `padding: var(--spacing-padding-xxs) var(--spacing-padding-xxs) var(--spacing-padding-xxs) calc(var(--spacing-padding-xs) + ${!displayGroupLines ? depthLevel : 0} * var(--spacing-padding-l))` : "padding: var(--spacing-padding-xxs) var(--spacing-padding-none)"};
19+
${displayGroupLines && depthLevel > 0 ? "margin-left: var(--spacing-padding-xs);" : ""}
20+
`}
21+
display: flex;
22+
align-items: center;
23+
gap: var(--spacing-gap-m);
24+
justify-content: ${({ responsiveView }) => (responsiveView ? "center" : "space-between")};
25+
background-color: ${({ selected }) => (selected ? "var(--color-bg-primary-lighter)" : "transparent")};
26+
height: var(--height-s);
27+
cursor: pointer;
28+
overflow: hidden;
29+
text-decoration: none;
30+
31+
&:hover {
32+
background-color: ${({ selected }) =>
33+
selected ? "var(--color-bg-primary-medium)" : "var(--color-bg-neutral-light)"};
34+
}
35+
&:active {
36+
background-color: ${({ selected }) =>
37+
selected ? "var(--color-bg-primary-medium)" : "var(--color-bg-neutral-light)"};
38+
}
39+
&:focus {
40+
outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);
41+
outline-offset: -2px;
42+
}
43+
`;
44+
45+
const Label = styled.span`
46+
display: flex;
47+
align-items: center;
48+
gap: var(--spacing-gap-s);
49+
overflow: hidden;
50+
`;
51+
52+
const Icon = styled.span`
53+
display: flex;
54+
color: var(--color-fg-neutral-dark);
55+
font-size: var(--height-xxs);
56+
svg {
57+
height: var(--height-xxs);
58+
width: 16px;
59+
}
60+
`;
61+
62+
const Text = styled.span<{ selected: ItemActionProps["selected"] }>`
63+
color: var(--color-fg-neutral-dark);
64+
font-family: var(--typography-font-family);
65+
font-size: var(--typography-label-m);
66+
font-weight: ${({ selected }) => (selected ? "var(--typography-label-semibold)" : "var(--typography-label-regular)")};
67+
text-overflow: ellipsis;
68+
white-space: nowrap;
69+
overflow: hidden;
70+
`;
71+
72+
const Control = styled.span`
73+
display: flex;
74+
align-items: center;
75+
padding: var(--spacing-padding-none);
76+
justify-content: flex-end;
77+
align-items: center;
78+
gap: var(--spacing-gap-s);
79+
`;
80+
81+
const ItemAction = memo(
82+
forwardRef<HTMLButtonElement, ItemActionProps>(
83+
({ badge, collapseIcon, depthLevel, icon, label, href, ...props }, ref) => {
84+
const [hasTooltip, setHasTooltip] = useState(false);
85+
const modifiedBadge = badge && cloneElement(badge, { size: "small" });
86+
const { displayControlsAfter, responsiveView, displayGroupLines, allowNavigation } =
87+
useContext(NavigationTreeContext) ?? {};
88+
89+
return (
90+
<TooltipWrapper condition={hasTooltip} label={label}>
91+
<Action
92+
as={allowNavigation && href ? "a" : "button"}
93+
role={allowNavigation && href ? "link" : "button"}
94+
ref={ref}
95+
depthLevel={depthLevel}
96+
displayGroupLines={!!displayGroupLines}
97+
responsiveView={responsiveView}
98+
{...(allowNavigation && href && { href })}
99+
{...props}
100+
>
101+
<Label>
102+
{!displayControlsAfter && <Control>{collapseIcon && <Icon>{collapseIcon}</Icon>}</Control>}
103+
<TooltipWrapper condition={responsiveView} label={label}>
104+
<Icon>{typeof icon === "string" ? <DxcIcon icon={icon} /> : icon}</Icon>
105+
</TooltipWrapper>
106+
{!responsiveView && (
107+
<Text
108+
selected={props.selected}
109+
onMouseEnter={(event: MouseEvent<HTMLSpanElement>) => {
110+
const text = event.currentTarget;
111+
setHasTooltip(text.scrollWidth > text.clientWidth);
112+
}}
113+
>
114+
{label}
115+
</Text>
116+
)}
117+
</Label>
118+
{!responsiveView && (
119+
<Control>
120+
{modifiedBadge}
121+
{displayControlsAfter && collapseIcon && <Icon>{collapseIcon}</Icon>}
122+
</Control>
123+
)}
124+
</Action>
125+
</TooltipWrapper>
126+
);
127+
}
128+
)
129+
);
130+
131+
ItemAction.displayName = "ItemAction";
132+
133+
export default ItemAction;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import styled from "@emotion/styled";
2+
import GroupItem from "./GroupItem";
3+
import SingleItem from "./SingleItem";
4+
import { MenuItemProps } from "./types";
5+
6+
const MenuItemContainer = styled.li`
7+
display: grid;
8+
gap: var(--spacing-gap-xs);
9+
`;
10+
11+
export default function MenuItem({ item, depthLevel = 0 }: MenuItemProps) {
12+
return (
13+
<MenuItemContainer role="menuitem">
14+
{"items" in item ? (
15+
<GroupItem {...item} depthLevel={depthLevel} />
16+
) : (
17+
<SingleItem {...item} depthLevel={depthLevel} />
18+
)}
19+
</MenuItemContainer>
20+
);
21+
}

0 commit comments

Comments
 (0)