Skip to content

Commit

Permalink
feat(blade): add TopNav component (#2257)
Browse files Browse the repository at this point in the history
* feat: add topnav & tabnav

* chore: update

* feat: integrate with menu

* refactor: minor changes

* chore: add trailing/leading gap & fix types

* fix: topnav padding issue

* chore: add native files

* chore: update snaps

* chore: change props

* chore: review comments

* chore: fix left/right button alignment

* chore: update

* chore: update

* feat: add support for scrollintoview

* chore: check for overflow

* chore: change scrollintoview block

* chore: pass down background color from topnav to subcomps

* docs: TopNav documentation (#2270)

* docs: add stories

* chore: update

* chore: add code example for topnav

* chore: add tabnav example

* chore: update

* chore: review comments

* chore: update

* chore: add kitchen sink

* chore: add bg

* test: add topnav tests (#2272)

* test: add topnav basic tests

* chore: add tabnav tests

* chore: update

* chore: update

* chore: rename

* chore: wire up explore links

* chore: update color in docs

* chore: add meta attributes

* Create gorgeous-months-move.md

* chore: fix tsc
  • Loading branch information
anuraghazra authored Jun 27, 2024
1 parent 290dc63 commit fe9a0cf
Show file tree
Hide file tree
Showing 34 changed files with 4,189 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-months-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@razorpay/blade": minor
---

feat(blade): add TopNav component
1 change: 1 addition & 0 deletions packages/blade/.storybook/react/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const StoryCanvas = styled.div<{ context }>(
context.kind.includes('/Dropdown/With Button') ||
context.kind.includes('/Dropdown/With AutoComplete') ||
context.kind.includes('/Carousel') ||
context.kind.includes('/TopNav') ||
context.kind.includes('/Examples') ||
context.kind.includes('/SideNav')
? '0rem'
Expand Down
8 changes: 6 additions & 2 deletions packages/blade/src/components/SideNav/SideNav.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import type { SideNavContextType, SideNavProps } from './types';
import {
classes,
COLLAPSED_L1_WIDTH,
EXPANDED_L1_WIDTH,
HOVER_AGAIN_DELAY,
L1_EXIT_HOVER_DELAY,
SKIP_NAV_ID,
TRANSITION_CLEANUP_DELAY,
SIDE_NAV_EXPANDED_L1_WIDTH_BASE,
SIDE_NAV_EXPANDED_L1_WIDTH_XL,
} from './tokens';
import BaseBox from '~components/Box/BaseBox';
import { makeMotionTime, makeSize, makeSpace } from '~utils';
Expand Down Expand Up @@ -241,7 +242,10 @@ const SideNav = ({
left="spacing.0"
display={{ base: 'none', m: 'flex' }}
flexDirection="column"
width={makeSize(EXPANDED_L1_WIDTH)}
width={{
base: makeSize(SIDE_NAV_EXPANDED_L1_WIDTH_BASE),
xl: makeSize(SIDE_NAV_EXPANDED_L1_WIDTH_XL),
}}
as="nav"
{...metaAttribute({
name: MetaConstants.SideNav,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ exports[`SideNav should render 1`] = `
flex-direction: column;
position: fixed;
height: 100%;
width: 264px;
width: 245px;
top: 0px;
left: 0px;
background-color: hsla(210,40%,98%,1);
Expand Down Expand Up @@ -758,6 +758,12 @@ exports[`SideNav should render 1`] = `
}
}
@media screen and (min-width:1200px) {
.c0.c0.c0.c0.c0 {
width: 264px;
}
}
@media screen and (min-width:320px) {
.c2.c2.c2.c2.c2 {
border-right-style: solid;
Expand Down
1 change: 1 addition & 0 deletions packages/blade/src/components/SideNav/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { SideNavSection } from './SideNavSection';
export { SideNavItem } from './SideNavItems/SideNavItem';
export { SideNavFooter } from './SideNavFooter';
export { SideNavBody } from './SideNavBody';
export { SIDE_NAV_EXPANDED_L1_WIDTH_BASE, SIDE_NAV_EXPANDED_L1_WIDTH_XL } from './tokens';
export type {
SideNavProps,
SideNavLinkProps,
Expand Down
6 changes: 4 additions & 2 deletions packages/blade/src/components/SideNav/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const classes = {
const SKIP_NAV_ID = 'blade-side-nav-skip';

const COLLAPSED_L1_WIDTH = size['56'];
const EXPANDED_L1_WIDTH = size['264'];
const SIDE_NAV_EXPANDED_L1_WIDTH_XL = size['264'];
const SIDE_NAV_EXPANDED_L1_WIDTH_BASE = size['245'];
const NAV_ITEM_HEIGHT = size['40'];

// This is the delay after which transition cleanup happens for rare cases where transitionEnd is not triggered
Expand All @@ -36,7 +37,8 @@ export {
SKIP_NAV_ID,
classes,
COLLAPSED_L1_WIDTH,
EXPANDED_L1_WIDTH,
SIDE_NAV_EXPANDED_L1_WIDTH_XL,
SIDE_NAV_EXPANDED_L1_WIDTH_BASE,
NAV_ITEM_HEIGHT,
TRANSITION_CLEANUP_DELAY,
HOVER_AGAIN_DELAY,
Expand Down
13 changes: 13 additions & 0 deletions packages/blade/src/components/TopNav/TabNav/TabNav.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Text } from '~components/Typography';
import { throwBladeError } from '~utils/logger';

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

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

export { TabNav };
181 changes: 181 additions & 0 deletions packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React from 'react';
import styled from 'styled-components';
import { useTopNavContext } from '../TopNavContext';
import { approximatelyEqual, MIXED_BG_COLOR, useHasOverflow } from './utils';
import { TabNavContext } from './TabNavContext';
import BaseBox from '~components/Box/BaseBox';
import type { StyledPropsBlade } from '~components/Box/styledProps';
import { getStyledProps } from '~components/Box/styledProps';
import { Button } from '~components/Button';
import { Divider } from '~components/Divider';
import { ChevronLeftIcon, ChevronRightIcon } from '~components/Icons';
import { makeMotionTime, makeSize } from '~utils';
import { size } from '~tokens/global';
import getIn from '~utils/lodashButBetter/get';
import type { BoxProps } from '~components/Box';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';

const GRADIENT_WIDTH = 54 as const;
const GRADIENT_OFFSET = -8 as const;
const OFFSET_BOTTOM = -12 as const;
const SCROLL_AMOUNT = 200;

type TabNavProps = {
children: React.ReactNode;
};

const ScrollableArea = styled(BaseBox)(() => {
return {
'&::-webkit-scrollbar': { display: 'none' },
};
});

const GradientOverlay = styled(BaseBox)<{
shouldShow?: boolean;
variant: 'left' | 'right';
$color: BoxProps['backgroundColor'];
}>(({ theme, shouldShow, variant, $color }) => {
const color = getIn(theme.colors, $color as never, MIXED_BG_COLOR);

return {
position: 'absolute',
[variant]: 0,
pointerEvents: shouldShow ? 'auto' : 'none',
transform: shouldShow ? 'scale(1)' : 'scale(0.5)',
opacity: shouldShow ? 1 : 0,
transitionTimingFunction: `${theme.motion.easing.standard.revealing}`,
transitionDuration: `${makeMotionTime(theme.motion.duration.xquick)}`,
transitionProperty: 'opacity, transform',
zIndex: 1,
':before': {
content: "''",
pointerEvents: 'none',
position: 'absolute',
[variant]: 0,
top: makeSize(GRADIENT_OFFSET),
bottom: makeSize(GRADIENT_OFFSET),
width: makeSize(GRADIENT_WIDTH),
background: `linear-gradient(to ${variant}, transparent 0%, ${color} 30%, ${color} 100%);`,
},
};
});

const TabNav = ({
children,
...styledProps
}: TabNavProps & StyledPropsBlade): React.ReactElement => {
const ref = React.useRef<HTMLDivElement>(null);
const hasOverflow = useHasOverflow(ref);
const [scrollStatus, setScrollStatus] = React.useState<'start' | 'end' | 'middle'>('start');
const { backgroundColor } = useTopNavContext();

// Check if the scroll is at start, end or middle
const handleScrollStatus = React.useCallback(
(e: React.UIEvent<HTMLDivElement, UIEvent>): void => {
const target = e.target as HTMLDivElement;
const isAtStart = target.scrollLeft === 0;
const isAtEnd = approximatelyEqual(
target.scrollLeft,
target.scrollWidth - target.offsetWidth,
);

if (isAtStart) {
setScrollStatus('start');
} else if (isAtEnd) {
setScrollStatus('end');
} else {
setScrollStatus('middle');
}
},
[],
);

const scrollRight = (): void => {
if (!ref.current) return;
ref.current.scrollBy({
behavior: 'smooth',
left: SCROLL_AMOUNT,
});
};

const scrollLeft = (): void => {
if (!ref.current) return;
ref.current.scrollBy({
behavior: 'smooth',
left: -SCROLL_AMOUNT,
});
};

return (
<TabNavContext.Provider value={{ containerRef: ref, hasOverflow }}>
<BaseBox
as="nav"
display="flex"
width="100%"
alignItems="center"
position="relative"
marginBottom={makeSize(OFFSET_BOTTOM)}
{...getStyledProps(styledProps)}
{...metaAttribute({ name: MetaConstants.TabNav })}
>
<GradientOverlay
variant="left"
$color={backgroundColor}
shouldShow={hasOverflow && scrollStatus !== 'start'}
>
<Button
size="xsmall"
variant="tertiary"
icon={ChevronLeftIcon}
accessibilityLabel="Scroll Left"
onClick={scrollLeft}
/>
</GradientOverlay>
<ScrollableArea
ref={ref}
onScroll={handleScrollStatus}
display="flex"
width="100%"
position="relative"
whiteSpace="nowrap"
gap="spacing.0"
overflowY="hidden"
overflowX="auto"
>
<BaseBox display="flex" flexDirection="row" width="max-content">
{React.Children.map(children, (child, index) => {
return (
<>
{index > 0 ? (
<Divider
margin="auto"
variant="muted"
orientation="vertical"
height={makeSize(size[16])}
/>
) : null}
{child}
</>
);
})}
</BaseBox>
</ScrollableArea>
<GradientOverlay
variant="right"
$color={backgroundColor}
shouldShow={hasOverflow && scrollStatus !== 'end'}
>
<Button
size="xsmall"
variant="tertiary"
icon={ChevronRightIcon}
accessibilityLabel="Scroll Right"
onClick={scrollRight}
/>
</GradientOverlay>
</BaseBox>
</TabNavContext.Provider>
);
};

export { TabNav };
21 changes: 21 additions & 0 deletions packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { throwBladeError } from '~utils/logger';

type TabNavContextProps = {
containerRef: React.RefObject<HTMLDivElement>;
hasOverflow: boolean;
};
const TabNavContext = React.createContext<TabNavContextProps | null>(null);

const useTabNavContext = (): TabNavContextProps => {
const context = React.useContext(TabNavContext);
if (!context) {
throwBladeError({
message: 'useTabNavContext must be used within a TabNavProvider',
moduleName: 'TabNav',
});
}
return context!;
};

export { TabNavContext, useTabNavContext };
13 changes: 13 additions & 0 deletions packages/blade/src/components/TopNav/TabNav/TabNavItem.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Text } from '~components/Typography';
import { throwBladeError } from '~utils/logger';

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

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

export { TabNavItem };
Loading

0 comments on commit fe9a0cf

Please sign in to comment.