UNPKG

@razorpay/blade-mcp

Version:

Model Context Protocol server for Blade

617 lines (574 loc) 16.7 kB
## 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; ```