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

feat(SideNav): add description and onLevelChange callback #2488

Merged
merged 8 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lucky-toys-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@razorpay/blade": minor
---

feat(SideNav): add `description` on SideNavLink and `onLevelChange` callback on `SideNav`
Copy link
Member

Choose a reason for hiding this comment

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

onVisualLevelChange

58 changes: 40 additions & 18 deletions packages/blade/src/components/SideNav/SideNav.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const BannerContainer = styled(BaseBox)((props) => {
*
*/
const _SideNav = (
{ children, isOpen, onDismiss, banner, testID, ...rest }: SideNavProps,
{ children, isOpen, onDismiss, onVisibleLevelChange, banner, testID, ...rest }: SideNavProps,
ref: React.Ref<BladeElementRef>,
): React.ReactElement => {
const l2PortalContainerRef = React.useRef(null);
Expand All @@ -143,6 +143,7 @@ const _SideNav = (
if (isMobile) {
setIsMobileL2Open(false);
onDismiss?.();
onVisibleLevelChange?.({ visibleLevel: 0 });
}
};

Expand All @@ -155,6 +156,36 @@ const _SideNav = (
timeoutIdsRef.current.push(clearTransitionTimeout);
};

const collapseL1 = (title: string): void => {
if (isMobile) {
setL2DrawerTitle(title);
setIsMobileL2Open(true);
onVisibleLevelChange?.({ visibleLevel: 2 });
return;
}

if (!isL1Collapsed) {
setIsL1Collapsed(true);
onVisibleLevelChange?.({ visibleLevel: 2 });
}
};

const expandL1 = (): void => {
if (isMobile) {
setIsMobileL2Open(false);
onVisibleLevelChange?.({ visibleLevel: 1 });
return;
}
// Ensures that if Normal L1 item is clicked, the L1 stays expanded
if (isL1Collapsed) {
setIsL1Collapsed(false);
// We want to avoid calling onVisibleLevelChange twice when L1 is hovered and then item on L1 is selected
if (!isL1Hovered) {
onVisibleLevelChange?.({ visibleLevel: 1 });
}
}
};

/**
* Handles L1 -> L2 menu changes based on active item
*/
Expand All @@ -164,13 +195,7 @@ const _SideNav = (
if (isL1ItemActive) {
if (args.isL2Trigger) {
// Click on L2 Trigger
if (isMobile) {
setL2DrawerTitle(args.title);
setIsMobileL2Open(true);
return;
}

setIsL1Collapsed(true);
collapseL1(args.title);

// `args.isFirstRender` checks if the item that triggered this change, triggered it during first render or during subsequent change
if (!args.isFirstRender) {
Expand All @@ -186,12 +211,7 @@ const _SideNav = (
}
} else {
// Click on normal L1 Item
// eslint-disable-next-line no-lonely-if
if (isMobile) {
setIsMobileL2Open(false);
}
// Ensures that if Normal L1 item is clicked, the L1 stays expanded
setIsL1Collapsed(false);
expandL1();
}
}
};
Expand All @@ -205,7 +225,7 @@ const _SideNav = (
setIsL1Collapsed,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[isL1Collapsed, isMobile, isMobileL2Open],
[isL1Collapsed, isMobile, isMobileL2Open, isL1Hovered],
);

React.useEffect(() => {
Expand All @@ -222,7 +242,7 @@ const _SideNav = (
{isMobile && onDismiss ? (
<>
{/* L1 */}
<Drawer isOpen={isOpen ?? false} onDismiss={onDismiss}>
<Drawer isOpen={isOpen ?? false} onDismiss={closeMobileNav}>
<DrawerHeader title="Main Menu" />
<DrawerBody>
<MobileL1Container
Expand All @@ -241,7 +261,7 @@ const _SideNav = (
</DrawerBody>
</Drawer>
{/* L2 */}
<Drawer isOpen={isMobileL2Open} onDismiss={() => setIsMobileL2Open(false)} isLazy={false}>
<Drawer isOpen={isMobileL2Open} onDismiss={() => expandL1()} isLazy={false}>
<DrawerHeader title={l2DrawerTitle} />
<DrawerBody>
<BaseBox ref={l2PortalContainerRef} />
Expand Down Expand Up @@ -320,8 +340,9 @@ const _SideNav = (
if (mouseOverTimeoutRef.current) {
clearTimeout(mouseOverTimeoutRef.current);
}
if (isL1Collapsed && isHoverAgainEnabled) {
if (isL1Collapsed && isHoverAgainEnabled && !isL1Hovered) {
setIsL1Hovered(true);
onVisibleLevelChange?.({ visibleLevel: 1 });
}
}}
onMouseLeave={() => {
Expand All @@ -330,6 +351,7 @@ const _SideNav = (
setIsL1Hovered(false);
setIsTransitioning(true);
cleanupTransition();
onVisibleLevelChange?.({ visibleLevel: 2 });
}, L1_EXIT_HOVER_DELAY);
}
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import { getFocusRingStyles } from '~utils/getFocusRingStyles';
import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect';
import { throwBladeError } from '~utils/logger';
import { makeAnalyticsAttribute } from '~utils/makeAnalyticsAttribute';
import { Text } from '~components/Typography';

const { SHOW_ON_LINK_HOVER, HIDE_WHEN_COLLAPSED, STYLED_NAV_LINK } = classes;

const StyledNavLinkContainer = styled(BaseBox)((props) => {
const StyledNavLinkContainer = styled(BaseBox)<{ $hasDescription: boolean }>((props) => {
return {
width: '100%',
[`.${SHOW_ON_LINK_HOVER}`]: {
Expand All @@ -45,14 +46,15 @@ const StyledNavLinkContainer = styled(BaseBox)((props) => {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
height: makeSize(NAV_ITEM_HEIGHT),
height: props.$hasDescription ? undefined : makeSize(NAV_ITEM_HEIGHT),
width: '100%',
textDecoration: 'none',
overflow: 'hidden',
flexWrap: 'nowrap',
cursor: 'pointer',
padding: `${makeSpace(props.theme.spacing[0])} ${makeSpace(props.theme.spacing[4])}`,
padding: `${makeSpace(props.theme.spacing[props.$hasDescription ? 3 : 0])} ${makeSpace(
props.theme.spacing[4],
)}`,
margin: `${makeSpace(props.theme.spacing[1])} ${makeSpace(props.theme.spacing[0])}`,
color: props.theme.colors.interactive.text.gray.subtle,
borderRadius: props.theme.border.radius.medium,
Expand All @@ -77,40 +79,66 @@ const StyledNavLinkContainer = styled(BaseBox)((props) => {
const NavLinkIconTitle = ({
icon: Icon,
title,
description,
titleSuffix,
isActive,
trailing,
isL1Item,
}: Pick<SideNavLinkProps, 'title' | 'icon' | 'titleSuffix'> & {
}: Pick<
SideNavLinkProps,
'title' | 'isActive' | 'trailing' | 'description' | 'icon' | 'titleSuffix'
> & {
isL1Item: boolean;
}): React.ReactElement => {
return (
<Box display="flex" flexDirection="row" gap="spacing.3">
{Icon ? (
<BaseBox display="flex" flexDirection="row" alignItems="center" justifyContent="center">
<Icon size="medium" color="currentColor" />
</BaseBox>
) : null}
<BaseText
truncateAfterLines={1}
color="currentColor"
fontWeight="medium"
fontSize={100}
lineHeight={100}
as="p"
className={isL1Item ? HIDE_WHEN_COLLAPSED : ''}
>
{title}
</BaseText>
{titleSuffix ? (
<BaseBox display="flex" alignItems="center">
{titleSuffix}
</BaseBox>
<Box width="100%" textAlign="left">
<Box display="flex" justifyContent="space-between" width="100%">
<Box display="flex" flexDirection="row" gap="spacing.3" alignItems="center">
{Icon ? (
<BaseBox display="flex" flexDirection="row" alignItems="center">
<Icon size="medium" color="currentColor" />
</BaseBox>
) : null}
<BaseText
truncateAfterLines={1}
color="currentColor"
fontWeight="medium"
fontSize={100}
lineHeight={100}
as="p"
className={isL1Item ? HIDE_WHEN_COLLAPSED : ''}
>
{title}
</BaseText>
{titleSuffix ? (
<BaseBox display="flex" alignItems="center">
{titleSuffix}
</BaseBox>
) : null}
</Box>
<Box display="flex" alignItems="center">
{trailing}
</Box>
</Box>
{!isL1Item && description ? (
<Text
size="small"
marginLeft="spacing.7"
textAlign="left"
weight="medium"
color={isActive ? 'interactive.text.primary.muted' : 'interactive.text.gray.muted'}
truncateAfterLines={1}
>
{description}
</Text>
) : null}
</Box>
);
};

const L3Trigger = ({
title,
description,
icon,
as,
href,
Expand All @@ -120,7 +148,15 @@ const L3Trigger = ({
onClick,
}: Pick<
SideNavLinkProps,
'title' | 'icon' | 'as' | 'href' | 'titleSuffix' | 'tooltip' | 'target' | 'onClick'
| 'title'
| 'description'
| 'icon'
| 'as'
| 'href'
| 'titleSuffix'
| 'tooltip'
| 'target'
| 'onClick'
>): React.ReactElement => {
const { onExpandChange, isExpanded, collapsibleBodyId } = useCollapsible();

Expand All @@ -135,7 +171,7 @@ const L3Trigger = ({

return (
<TooltipifyNavItem tooltip={tooltip}>
<StyledNavLinkContainer>
<StyledNavLinkContainer $hasDescription={Boolean(description)}>
<BaseBox
className={STYLED_NAV_LINK}
as={href ? as : 'button'}
Expand All @@ -144,10 +180,16 @@ const L3Trigger = ({
onClick={(e: React.MouseEvent) => toggleCollapse(e)}
{...makeAccessible({ expanded: isExpanded, controls: collapsibleBodyId })}
>
<NavLinkIconTitle title={title} icon={icon} isL1Item={false} titleSuffix={titleSuffix} />
<BaseBox display="flex" alignItems="center">
{isExpanded ? <ChevronUpIcon {...iconProps} /> : <ChevronDownIcon {...iconProps} />}
</BaseBox>
<NavLinkIconTitle
title={title}
description={description}
icon={icon}
isL1Item={false}
titleSuffix={titleSuffix}
trailing={
isExpanded ? <ChevronUpIcon {...iconProps} /> : <ChevronDownIcon {...iconProps} />
}
/>
</BaseBox>
</StyledNavLinkContainer>
</TooltipifyNavItem>
Expand Down Expand Up @@ -178,6 +220,7 @@ const CurvedVerticalLine = styled(BaseBox)((props) => {

const SideNavLink = ({
title,
description,
href,
children,
titleSuffix,
Expand Down Expand Up @@ -211,6 +254,13 @@ const SideNavLink = ({
moduleName: 'SideNavLink',
});
}

if (currentLevel === 1 && Boolean(description)) {
throwBladeError({
message: 'Description is not supported for L1 items',
moduleName: 'SideNavLink',
});
}
}

const isFirstRender = useFirstRender();
Expand Down Expand Up @@ -240,6 +290,7 @@ const SideNavLink = ({
>
<L3Trigger
title={title}
description={description}
icon={icon}
as={as}
href={href}
Expand All @@ -252,7 +303,10 @@ const SideNavLink = ({
</Collapsible>
) : (
<>
<StyledNavLinkContainer position="relative">
<StyledNavLinkContainer
$hasDescription={currentLevel !== 1 && Boolean(description)}
position="relative"
>
<TooltipifyNavItem tooltip={tooltip}>
<BaseBox
className={STYLED_NAV_LINK}
Expand Down Expand Up @@ -296,6 +350,8 @@ const SideNavLink = ({
<NavLinkIconTitle
icon={icon}
title={title}
description={description}
isActive={isActive}
isL1Item={currentLevel === 1}
titleSuffix={titleSuffix}
/>
Expand All @@ -321,7 +377,6 @@ const SideNavLink = ({
) : null}
{currentLevel === 3 && isActive ? <CurvedVerticalLine /> : null}
</StyledNavLinkContainer>

{children ? (
<FloatingPortal root={l2PortalContainerRef}>
{isActive && isL1Collapsed ? (
Expand Down
Loading
Loading