@razorpay/blade-mcp
Version:
Model Context Protocol server for Blade
617 lines (574 loc) • 16.7 kB
Markdown
## Component Name
SideNav
## Description
The SideNav component provides a responsive side navigation layout positioned along the left side of the screen. It enables quick access to different sections or functionalities of an application with support for multi-level navigation, collapsible sections, and mobile responsiveness. SideNav supports both standalone usage and integration with routing libraries like React Router.
## Important Constraints
- `SideNavLink`: `as` prop requires React Router's Link. Always install React Router while using this component. Example:
```jsx
import { Link } from 'react-router-dom';
<SideNavLink as={Link} />;
```
- Explicitly add `position` prop to `SideNav` component to ensure it is positioned correctly.
## TypeScript Types
The following types represent the props that the SideNav component and its subcomponents accept. These types help configure the navigation structure properly.
````typescript
/**
* Props for the SideNav component
*/
type SideNavProps = {
/**
* Children slot.
*
* Supports SideNavFooter, SideNavBody
*/
children: React.ReactNode;
/**
* Only applicable in mobile
*
* State for opening / closing the SideNav in mobile
*/
isOpen?: DrawerProps['isOpen'];
/**
* Only applicable in mobile
*
* Callback when SideNav is closed
*/
onDismiss?: DrawerProps['onDismiss'];
/**
* Callback that gets triggered when L1 is collapsed or expanded.
*
* This callback gets triggered when you-
* - Select the active link changes between L1 and L2 which can collapse or expand the L1
* - When you hover / unhover L1 in collapsed state which can temporarily expand the L1
*/
onVisibleLevelChange?: ({ visibleLevel }: { visibleLevel: number }) => void;
/**
* Banner slot for usecases like adding Activation Panel
*
* IMPORTANT: Avoid adding promotional items in this
*/
banner?: React.ReactElement;
/**
* Position of the SideNav.
*
* It is position="fixed" by default with top="spacing.0" and left="spacing.0" and height="100%"
*
* @default 'fixed'
*/
position?: StyledPropsBlade['position'];
} & StyledPropsBlade &
TestID;
/**
* Props for the SideNavLink component
*/
type SideNavLinkProps = {
/**
* title of the Link
*/
title: string;
/**
* description of the Link
*
* Note: Only applicable for L2 items
*/
description?: string;
/**
* Slot after the title.
*
* Used for <Badge />, <Counter /> in most cases
*/
titleSuffix?: React.ReactElement;
/**
* Trailing slot for item. Only visible on hover of the item
*
* Used for <Button />
*/
trailing?: React.ReactElement;
/**
* href of the link
*/
href?: LinkProps['href'];
/**
* Anchor tag `target` attribute
*/
target?: LinkProps['target'];
/**
* as prop to pass ReactRouter's Link component.
*
* ```jsx
* import { Link } from 'react-router-dom';
*
* <SideNavLink as={Link} />
* ```
*/
as: React.ComponentType<any>;
/**
* Set Active state of SideNavLink.
*
* Checkout SideNav documentation for usage
*/
isActive?: boolean;
/**
* Leading icon for SideNavLink
*/
icon?: IconComponent;
/**
* Children slot to add Nested Menu
*
* ```jsx
* <SideNavLink title="L2 Trigger" href="/l2-first-item">
* <SideNavLevel>
* <SideNavLink title="L2 Item" href="/l2-first-item" />
* <SideNavLink title="L2 Item 2" href="/l2-second-item" />
* </SideNavLevel>
* </SideNavLink>
* ```
*/
children?: React.ReactElement;
/**
* Tooltip object to add tooltip to SideNavLink
*
* ```jsx
* <SideNavLink
* tooltip={{
* title: 'Tooltip Title',
* content: 'Tooltip description'
* }}
* />
* ```
*/
tooltip?: TooltipifyComponentProps['tooltip'];
/**
* Click handler for the link
*/
onClick?: (event: React.MouseEvent) => void;
} & DataAnalyticsAttribute;
/**
* Props for the SideNavSection component
*/
type SideNavSectionProps = {
/**
* Title of the section
*/
title?: string;
/**
* Number of items after which the items are collapsed into `+x more`
*/
maxVisibleItems?: number;
/**
* Default value if the nav section is expanded or collapsed after maxVisibleItems
*
* @default false
*/
defaultIsExpanded?: boolean;
/**
* Callback when `+x more is clicked`
*/
onExpandChange?: ({ isExpanded }: { isExpanded: boolean }) => void;
/**
* Children slot for SideNavLink
*/
children: React.ReactElement[];
} & DataAnalyticsAttribute;
/**
* Props for the SideNavItem component
*/
type SideNavItemProps = {
/**
* Leading slot for SideNavItem.
*
* Meant for Indicator, Icon, etc
*/
leading: React.ReactElement;
/**
* Trailing slot for SideNavItem.
*
* Meant for Button, Switch, etc
*/
trailing: React.ReactElement;
/**
* Title of SideNavItem
*/
title: string;
/**
* Render item of container. Use as="label" when using Switch or form input in trailing
*
* @default div
*/
as?: 'label' | 'div';
/**
* backgroundColor of the item
*
* @default undefined
*/
backgroundColor?: BaseBoxProps['backgroundColor'];
/**
* Tooltip object to add tooltip to SideNavItem
*
* ```jsx
* <SideNavItem
* tooltip={{
* title: 'Tooltip Title',
* content: 'Tooltip description'
* }}
* />
* ```
*/
tooltip?: SideNavLinkProps['tooltip'];
} & DataAnalyticsAttribute;
/**
* Props for the SideNavFooter component
*/
type SideNavFooterProps = {
/**
* Children slot for SideNavLink, SideNavItem
*/
children: React.ReactElement[] | React.ReactElement;
};
/**
* Props for the SideNavBody component
*/
type SideNavBodyProps = {
/**
* Children slot for SideNavSection components
*/
children: React.ReactElement | React.ReactElement[];
};
/**
* Props for the SideNavLevel component
*/
type SideNavLevelProps = {
/**
* Children slot for nested SideNavLink components
*/
children: React.ReactElement | React.ReactElement[];
};
````
## Example
### Comprehensive SideNav with React Router Integration
This example demonstrates a fully-featured SideNav implementation with multi-level navigation, React Router integration, dynamic active state handling, nested sections, responsive design for mobile, and interactive elements like a test mode toggle and activation banner.
```tsx
import React, { useState } from 'react';
import { BrowserRouter as Router, Link, useLocation, matchPath } from 'react-router-dom';
import {
SideNav,
SideNavBody,
SideNavFooter,
SideNavLink,
SideNavLevel,
SideNavSection,
SideNavItem,
Box,
Card,
CardBody,
Text,
ProgressBar,
Indicator,
Switch,
Button,
Tooltip,
HomeIcon,
SettingsIcon,
UserIcon,
CreditCardIcon,
BillIcon,
WalletIcon,
MenuIcon,
PlusIcon,
BankIcon,
ChevronRightIcon,
ArrowUpRightIcon,
} from '@razorpay/blade/components';
// Custom activation card for the banner slot
const ActivationCard = () => {
return (
<Card href="/activate" padding="spacing.4" elevation="none">
<CardBody>
<Box display="flex" justifyContent="space-between" marginBottom="spacing.2">
<Text size="medium" weight="semibold">
Activation Pending
</Text>
<Box>
<ChevronRightIcon />
</Box>
</Box>
<ProgressBar
label="Progress"
showPercentage={true}
value={50}
accessibilityLabel="Activation progress: 50% complete"
/>
</CardBody>
</Card>
);
};
// Navigation link with proper active state handling
const NavLink = (props) => {
const location = useLocation();
// Helper function to check if a link is active
const isItemActive = (pathname, { href, activeOnLinks }) => {
const isCurrentPathActive = matchPath(pathname, {
path: href,
exact: false,
});
const isSubItemActive = activeOnLinks?.some((link) =>
matchPath(pathname, { path: link, exact: false }),
);
return Boolean(isCurrentPathActive || isSubItemActive);
};
return (
<SideNavLink
{...props}
as={Link}
isActive={isItemActive(location.pathname, {
href: props.href,
activeOnLinks: props.activeOnLinks,
})}
/>
);
};
// Main SideNav component with all features
const SideNavExample = () => {
const [isMobileOpen, setIsMobileOpen] = useState(false);
const [isTestModeActive, setIsTestModeActive] = useState(false);
const location = useLocation();
// Helper to get all child hrefs for managing active states
const getAllChildHrefs = (items) => {
if (!items) return [];
const hrefs = [];
items.forEach((item) => {
if (item.href) hrefs.push(item.href);
if (item.items) hrefs.push(...getAllChildHrefs(item.items));
});
return hrefs;
};
// Define navigation items (typically would come from an API or config)
const navigationItems = [
{
type: 'section',
items: [
{
icon: HomeIcon,
title: 'Dashboard',
href: '/dashboard',
'data-analytics-section': 'main-nav',
'data-analytics-element': 'dashboard',
},
{
icon: WalletIcon,
title: 'Payments',
href: '/payments',
tooltip: {
content: 'View all payment transactions',
placement: 'right',
},
trailing: (
<Tooltip content="Create new payment (Ctrl+N)" placement="right">
<Button
icon={PlusIcon}
size="xsmall"
variant="tertiary"
accessibilityLabel="Create new payment"
/>
</Tooltip>
),
'data-analytics-section': 'main-nav',
'data-analytics-element': 'payments',
},
],
},
{
type: 'section',
title: 'Banking',
maxVisibleItems: 3,
items: [
{
icon: CreditCardIcon,
title: 'Credit Cards',
href: '/banking/credit-cards',
'data-analytics-section': 'banking',
'data-analytics-element': 'credit-cards',
items: [
{
title: 'Physical Cards',
href: '/banking/credit-cards/physical',
description: 'RBL20I43',
'data-analytics-section': 'banking',
'data-analytics-element': 'physical-cards',
},
{
title: 'Virtual Cards',
href: '/banking/credit-cards/virtual',
description: 'VIR32L98',
'data-analytics-section': 'banking',
'data-analytics-element': 'virtual-cards',
},
],
},
{
icon: BankIcon,
title: 'Bank Accounts',
href: '/banking/accounts',
'data-analytics-section': 'banking',
'data-analytics-element': 'bank-accounts',
},
{
icon: BillIcon,
title: 'Statements',
href: '/banking/statements',
'data-analytics-section': 'banking',
'data-analytics-element': 'statements',
},
{
icon: ArrowUpRightIcon,
title: 'Transfers',
href: '/banking/transfers',
'data-analytics-section': 'banking',
'data-analytics-element': 'transfers',
},
],
},
];
return (
<Box height="100vh" position="relative">
<SideNav
isOpen={isMobileOpen}
onDismiss={() => setIsMobileOpen(false)}
onVisibleLevelChange={({ visibleLevel }) => console.log('Visible level:', visibleLevel)}
banner={<ActivationCard />}
testID="main-navigation"
position="absolute"
>
<SideNavBody>
{navigationItems.map((section, sectionIndex) => {
// Calculate whether section should be expanded by default
const sectionItems = section.items || [];
const hasActiveItem = sectionItems.some(
(item) =>
matchPath(location.pathname, { path: item.href, exact: false }) ||
getAllChildHrefs(item.items).some((childHref) =>
matchPath(location.pathname, { path: childHref, exact: false }),
),
);
return (
<SideNavSection
key={`section-${sectionIndex}`}
title={section.title}
maxVisibleItems={section.maxVisibleItems}
defaultIsExpanded={hasActiveItem}
onExpandChange={({ isExpanded }) =>
console.log(`Section "${section.title}" expanded:`, isExpanded)
}
data-analytics-section={`nav-section-${sectionIndex}`}
>
{sectionItems.map((item, itemIndex) => {
if (!item.items) {
return <NavLink key={`item-${itemIndex}`} {...item} />;
}
// For items with children, create nested navigation
const childHrefs = getAllChildHrefs(item.items);
return (
<NavLink
key={`item-${itemIndex}`}
{...item}
activeOnLinks={childHrefs}
href={item.items[0]?.href || item.href}
>
<SideNavLevel>
{item.items?.map((subItem, subIndex) => (
<NavLink
key={`subitem-${subIndex}`}
{...subItem}
description={subItem.description}
/>
))}
</SideNavLevel>
</NavLink>
);
})}
</SideNavSection>
);
})}
</SideNavBody>
<SideNavFooter>
{/* Test mode toggle with accessibility improvements */}
<SideNavItem
as="label"
title="Test Mode"
leading={
<Indicator
color={isTestModeActive ? 'notice' : 'positive'}
emphasis="intense"
accessibilityLabel={isTestModeActive ? 'Test mode enabled' : 'Test mode disabled'}
/>
}
backgroundColor={isTestModeActive ? 'feedback.background.notice.subtle' : undefined}
trailing={
<Switch
accessibilityLabel="Toggle test mode"
size="small"
isChecked={isTestModeActive}
onChange={({ isChecked }) => setIsTestModeActive(isChecked)}
/>
}
data-analytics-section="footer"
data-analytics-element="test-mode-toggle"
/>
{/* Settings navigation with nested items */}
<NavLink
title="Settings"
icon={SettingsIcon}
href="/settings"
activeOnLinks={['/settings/user', '/settings/account']}
data-analytics-section="footer"
data-analytics-element="settings"
>
<SideNavLevel>
<NavLink
icon={UserIcon}
title="User Settings"
href="/settings/user"
data-analytics-section="settings"
data-analytics-element="user-settings"
/>
<NavLink
icon={SettingsIcon}
title="Account Settings"
href="/settings/account"
data-analytics-section="settings"
data-analytics-element="account-settings"
/>
</SideNavLevel>
</NavLink>
</SideNavFooter>
</SideNav>
{/* Mobile menu toggle button */}
<Box
display={{ base: 'block', m: 'none' }}
position="fixed"
top="spacing.4"
right="spacing.4"
zIndex="2"
>
<Button
variant="tertiary"
icon={MenuIcon}
onClick={() => setIsMobileOpen(true)}
accessibilityLabel="Open navigation menu"
/>
</Box>
{/* Main content area with proper spacing based on SideNav */}
<Box marginLeft={{ base: 'spacing.0', m: '300px' }} padding="spacing.4" height="100%">
{/* Your application content goes here */}
</Box>
</Box>
);
};
// Wrap with Router for actual usage
const App = () => (
<Router>
<SideNavExample />
</Router>
);
export default App;
```