Skip to content

Commit

Permalink
feat(BottomNav): Implementation, Documentation, Tests (#2395)
Browse files Browse the repository at this point in the history
* docs(BottomNav): add BottomNav API

* feat: update title image

* feat: add examples of navitem

* fix: adjust image width

* feat: remove rel and target prop

* feat: add description

* feat(BottomNav): add bottomnav implementation

* feat: add native exports

* docs: add bottomnavitem documentation

* feat: add items sink stories

* docs: add documentation for BottomNav

* tests: add unit tests for bottomnav

* fix: linting

* feat: add rel and target

* fix: lint again

* feat: add onClick event handler type

* Create shy-files-smile.md

* fix: ts

* fix: lint

* feat: export BottomNav

* feat: set default route in example
  • Loading branch information
saurabhdaware authored Oct 30, 2024
1 parent 9d0bf3e commit b63190b
Show file tree
Hide file tree
Showing 17 changed files with 1,465 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-files-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@razorpay/blade": minor
---

feat(BottomNav): add BottomNav component
23 changes: 23 additions & 0 deletions packages/blade/src/components/BottomNav/BottomNav.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { BottomNavProps, BottomNavItemProps } from './types';
import { Text } from '~components/Typography';
import { throwBladeError } from '~utils/logger';

const BottomNav = (_props: BottomNavProps): React.ReactElement => {
throwBladeError({
message: 'BottomNav is not yet implemented for native',
moduleName: 'BottomNav',
});

return <Text>BottomNav Component is not available for Native mobile apps.</Text>;
};

const BottomNavItem = (_props: BottomNavItemProps): React.ReactElement => {
throwBladeError({
message: 'BottomNavItem is not yet implemented for native',
moduleName: 'BottomNavItem',
});

return <Text>BottomNav Component is not available for Native mobile apps.</Text>;
};

export { BottomNav, BottomNavItem };
272 changes: 272 additions & 0 deletions packages/blade/src/components/BottomNav/BottomNav.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import React from 'react';
import type { StoryFn, Meta } from '@storybook/react';
import { Title } from '@storybook/addon-docs';
import StoryRouter from 'storybook-react-router';
import { NavLink, matchPath, Route, Switch, useLocation } from 'react-router-dom';
import { bottomNavWithReactRouter } from './docsCode';
import { BottomNav, BottomNavItem } from '.';
import type { BottomNavItemProps, BottomNavProps } from '.';
import { Sandbox } from '~utils/storybook/Sandbox';
import StoryPageWrapper from '~utils/storybook/StoryPageWrapper';
import { getStyledPropsArgTypes } from '~components/Box/BaseBox/storybookArgTypes';
import type { SideNavLinkProps } from '~components/SideNav';
import { SideNav, SideNavBody, SideNavLink } from '~components/SideNav';
import {
CurrentAccountIcon,
HomeIcon,
MenuDotsIcon,
PaymentButtonIcon,
PaymentGatewayIcon,
PaymentLinkIcon,
PaymentPagesIcon,
RazorpayIcon,
TransactionsIcon,
} from '~components/Icons';
import { Box } from '~components/Box';
import { Heading } from '~components/Typography';
import { Alert } from '~components/Alert';

const Page = (): React.ReactElement => {
return (
<StoryPageWrapper
componentName="BottomNav"
componentDescription="Bottom navigation component is a persistent user interface element at the bottom of a mobile app screen, providing quick access to core functionalities through icons and labels."
figmaURL="https://www.figma.com/proto/jubmQL9Z8V7881ayUD95ps/Blade-DSL?node-id=96508-47113&node-type=frame&m=dev&scaling=min-zoom&content-scaling=fixed&page-id=91244%3A54900"
>
<Title>Usage (with React Router v6)</Title>
<Alert
color="notice"
title="State Management Note"
description="BottomNav component requires you to handle active link and active menu item on consumer end
since the component is detached from React Router. The example below includes some boilerplate in handling these active states using React Router v6. Make sure to test your edge cases while implementing. Checkout API Decision of BottomNav for more details."
isFullWidth
isDismissible={false}
/>

<Sandbox
files={bottomNavWithReactRouter}
editorHeight={600}
hideNavigation={false}
openFile="App.tsx,bottomNavItems.ts,BottomNavExample.tsx"
/>
</StoryPageWrapper>
);
};

export default {
title: 'Components/BottomNav',
component: BottomNav,
tags: ['autodocs'],
argTypes: getStyledPropsArgTypes(),
parameters: {
viewport: {
defaultViewport: 'iPhone6',
},
docs: {
page: Page,
},
},
// eslint-disable-next-line babel/new-cap
decorators: [StoryRouter(undefined, { initialEntries: ['/payments'] })] as unknown,
} as Meta<typeof BottomNav>;

const bottomNavItems = [
{
title: 'Payments',
href: '/payments',
icon: PaymentGatewayIcon,
},
{
title: 'Transactions',
href: '/transactions',
icon: TransactionsIcon,
isActive: true,
},
{
title: 'Links',
href: '/payment-links',
icon: PaymentLinkIcon,
},
{
title: 'Pages',
href: '/payment-pages',
icon: PaymentPagesIcon,
},
{
title: 'Buttons',
href: '/payment-buttons',
icon: PaymentButtonIcon,
},
];

const sideNavItems = [
{
title: 'Home',
href: '/home',
icon: HomeIcon,
},
{
title: 'Current Account',
href: '/x/current-account',
icon: CurrentAccountIcon,
},
{
title: 'Rize',
href: '/rize',
icon: RazorpayIcon,
},
];

// eslint-disable-next-line
const SamplePage = ({ match }: { match: any }): React.ReactElement => (
<Box padding={{ base: 'spacing.2', m: 'spacing.6' }}>
<pre>
<code>{JSON.stringify(match, null, 4)}</code>
</pre>
</Box>
);

const isItemActive = (
location: { pathname: string },
{ href, activeOnLinks }: { href?: string; activeOnLinks?: string[] },
): boolean => {
const isCurrentPathActive = Boolean(
matchPath(location.pathname, {
path: href,
exact: true,
}),
);

const isSubItemActive = Boolean(
activeOnLinks?.find((href) => matchPath(location.pathname, { path: href, exact: true })),
);

return isCurrentPathActive || isSubItemActive;
};

const BottomNavRouterItem = (
props: Omit<BottomNavItemProps, 'as'> & {
activeOnLinks?: string[];
},
): React.ReactElement => {
const location = useLocation();

return (
<BottomNavItem
{...props}
as={NavLink}
isActive={isItemActive(location, { href: props.href, activeOnLinks: props.activeOnLinks })}
/>
);
};

const SideNavRouterLink = (
props: Omit<SideNavLinkProps, 'as'> & {
activeOnLinks?: string[];
},
): React.ReactElement => {
const location = useLocation();

return (
<SideNavLink
{...props}
as={NavLink}
isActive={isItemActive(location, { href: props.href, activeOnLinks: props.activeOnLinks })}
/>
);
};

const BottomNavTemplate: StoryFn<BottomNavProps> = ({ children, ...args }) => {
return (
<BottomNav {...args}>
{bottomNavItems.map((item, index) => (
<BottomNavItem key={index} {...item} />
))}
</BottomNav>
);
};

const WithRoutingTemplate: StoryFn<BottomNavProps> = ({ children, ...args }) => {
const [isSideNavOpen, setIsSideNavOpen] = React.useState(false);
return (
<>
<Switch>
{[...Object.values(bottomNavItems), ...Object.values(sideNavItems)].map((route) => (
<Route key={route.href} path={route.href} component={SamplePage} />
))}
</Switch>
<SideNav
display={{ base: 'block', m: 'none' }}
isOpen={isSideNavOpen}
onDismiss={() => setIsSideNavOpen(false)}
position="absolute"
>
<SideNavBody>
{sideNavItems.map((item) => (
<SideNavRouterLink key={item.title} {...item} />
))}
</SideNavBody>
</SideNav>
<BottomNav {...args}>
{children ?? (
<>
{bottomNavItems.slice(0, -1).map((item, index) => (
<BottomNavRouterItem key={index} {...item} />
))}
<BottomNavRouterItem
title="More"
onClick={() => setIsSideNavOpen(true)}
icon={MenuDotsIcon}
activeOnLinks={Object.values(sideNavItems).map((item) => item.href)}
/>
</>
)}
</BottomNav>
</>
);
};

export const SimpleBottomNav = BottomNavTemplate.bind({});
SimpleBottomNav.args = {};

export const WithRouting = WithRoutingTemplate.bind({});
WithRouting.args = {};

export const ItemsCount = (): React.ReactElement => {
return (
<Box display="flex" flexDirection="column" gap="spacing.10">
<Box>
<Heading>2 Items</Heading>
<BottomNav position="relative">
{bottomNavItems.slice(0, 2).map((item, index) => (
<BottomNavItem key={index} {...item} />
))}
</BottomNav>
</Box>
<Box>
<Heading>3 Items</Heading>
<BottomNav position="relative">
{bottomNavItems.slice(0, 3).map((item, index) => (
<BottomNavItem key={index} {...item} />
))}
</BottomNav>
</Box>
<Box>
<Heading>4 Items</Heading>
<BottomNav position="relative">
{bottomNavItems.slice(0, 4).map((item, index) => (
<BottomNavItem key={index} {...item} />
))}
</BottomNav>
</Box>
<Box>
<Heading>Max Items</Heading>
<BottomNav position="relative">
{bottomNavItems.map((item, index) => (
<BottomNavItem key={index} {...item} />
))}
</BottomNav>
</Box>
</Box>
);
};
Loading

0 comments on commit b63190b

Please sign in to comment.