Skip to content

Commit

Permalink
feat(SideNav): add description and onVisibleLevelChange callback (#2488)
Browse files Browse the repository at this point in the history
* docs: add comment

* feat: add description

* feat: add jsdocs for onLevelChange

* Create lucky-toys-hunt.md

* test: update snapshots for sidenav

* feat: rename prop to onVisibleLevelChange

* Update lucky-toys-hunt.md
  • Loading branch information
saurabhdaware authored Jan 24, 2025
1 parent 4b36c3d commit 194fffc
Show file tree
Hide file tree
Showing 7 changed files with 770 additions and 499 deletions.
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 `onVisibleLevelChange` callback on `SideNav`
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
123 changes: 89 additions & 34 deletions packages/blade/src/components/SideNav/SideNavItems/SideNavLink.web.tsx
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

0 comments on commit 194fffc

Please sign in to comment.