Skip to content

Commit

Permalink
feat(SideNav): Implementation, Documentation, Tests (#2167)
Browse files Browse the repository at this point in the history
* poc

* feat: create L1 menu

* feat: add basic L2

* feat: attempt l2

* fix: no clue what I am doing

* feat: finally made l1 active states work correctly

* feat: add mutation observer

* current page check

* feat: isCurrentPage implementation

* feat: get implementation working with isCurrentPage

* feat: get level 1 hover working

* feat: add collapse animation

* feat: add animation

* feat: add animations

* fix: remove unused navlink

* fix: types

* fix: remove unused code

* feat: add mobile sidenav with drawer

* fix: click to open again in l2 trigger

* feat: add basic l3

* feat: get l3 working in example

* fix: background color in collapsible

* fix: ts errors and add collapsible

* feat: add footer

* refactor: move items to separate dir

* fix: drawer heading, add header

* feat: add collapsed state for show more link

* fix: transition delay

* feat: add l2 drawer heading

* feat: add titleSuffix and trailing

* feat: add tooltip to item

* fix: mobile check

* feat: add active line

* fix: active link animation on page load

* fix: initial focus

* hide open nav button in mobile

* fix: perf, refactor code

* fix: matchPath logic

* fix: responsiveness in docs

* feat: add comment

* feat: add new json

* fix: latest documentation

* fix: selected state when items are collapsed

* feat: add SideNavItem component

* feat: add jsdoc

* docs: add dashboard template example

* fix: ts

* feat: export SideNav

* feat: add v6 docs

* feat: remove StrictMode

* feat: add tests

* refactor: move example out of test

* feat: add interaction test

* feat: add kitchensink

* fix: ts

* fix: SideNav snapshots

* feat: prettify route output

* fix: remove unused variable

* fix: resolve anurag's amazing comments

* feat: add comment for isFirstRender

* fix: tests

* fix: remove flaky sidenav test

* fix: animations and layout shift padding

* fix: tests

* fix: edgecases

* feat: add activation card

* fix: snapshots

* fix: react-router example

* docs: divide sandbox code in multiple files

* fix: add comments to router example

* feat: add url bar to stackblitz

* feat: add alert in usage

* fix: text

* add link playground

* fix: remove unused code

* tests: fix snapshots

* feat: add ham menu in mobile stories

* feat: add mobile sidenav

* fix: add ham menu button in examples

* fix: broken types

* fix: snapshots

* fix: missing skeleton in dashboard layout

* feat: add jsdoc

* Create red-bats-allow.md

* fix: Drawer tests for back button

* fix: interaction tests for safari

* fix: sidenav mobile test

* feat: add background transition to items and make show more link area more clickable

* feat: add delay in exit, fix ui panic

* docs: add comment to SideNavLevel

* fix: snapshots

* fix: small mobile scroll

* feat: add error for L4 items

* fix: typecheck

* docs: resolve chaitanya's comment

* fix: snapshots

* fix: drawer interaction test

* fix: spacing right
  • Loading branch information
saurabhdaware authored May 28, 2024
1 parent 2d82e41 commit 0d3e260
Show file tree
Hide file tree
Showing 62 changed files with 6,292 additions and 120 deletions.
7 changes: 7 additions & 0 deletions .changeset/red-bats-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@razorpay/blade": minor
---

feat(SideNav): add SideNav component

Checkout [SideNav Documentation](https://blade.razorpay.com/?path=/docs/components-sidenav--docs)
3 changes: 2 additions & 1 deletion packages/blade/.storybook/react/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ const StoryCanvas = styled.div<{ context }>(
context.kind.includes('/Dropdown/With Button') ||
context.kind.includes('/Dropdown/With AutoComplete') ||
context.kind.includes('/Carousel') ||
context.kind.includes('/Examples')
context.kind.includes('/Examples') ||
context.kind.includes('/SideNav')
? '0rem'
: '2rem'
};
Expand Down
1 change: 1 addition & 0 deletions packages/blade/src/components/Accordion/AccordionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const AccordionItem = ({
onExpandChange={handleExpandChange}
// Accordion has its own width restrictions
_shouldApplyWidthRestrictions={false}
_dangerouslyDisableValidations={true}
>
<AccordionButton
index={_index}
Expand Down
11 changes: 8 additions & 3 deletions packages/blade/src/components/Collapsible/Collapsible.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ type CollapsibleProps = {
*/
onExpandChange?: ({ isExpanded }: { isExpanded: boolean }) => void;

/**
* **Internal**: disables trigger validations. Used for AccordionButton and SideNavLink internally
*/
_dangerouslyDisableValidations?: boolean;
/**
* **Internal**: used to override responsive width restrictions
*/
Expand All @@ -67,6 +71,7 @@ const Collapsible = ({
onExpandChange,
testID,
_shouldApplyWidthRestrictions = true,
_dangerouslyDisableValidations = false,
...styledProps
}: CollapsibleProps): ReactElement => {
const [isBodyExpanded, setIsBodyExpanded] = useState(isExpanded ?? defaultIsExpanded);
Expand Down Expand Up @@ -110,9 +115,9 @@ const Collapsible = ({
!(
isValidAllowedChildren(child, MetaConstants.CollapsibleBody) ||
isValidAllowedChildren(child, MetaConstants.CollapsibleButton) ||
isValidAllowedChildren(child, MetaConstants.CollapsibleLink) ||
isValidAllowedChildren(child, MetaConstants.AccordionButton)
)
isValidAllowedChildren(child, MetaConstants.CollapsibleLink)
) &&
!_dangerouslyDisableValidations
) {
throwBladeError({
message: `only the following are supported as valid children: CollapsibleBody, CollapsibleButton, CollapsibleLink`,
Expand Down
19 changes: 9 additions & 10 deletions packages/blade/src/components/Collapsible/CollapsibleBody.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import type { ReactElement, ReactNode } from 'react';
import type { ReactElement } from 'react';
import { CollapsibleBodyContent } from './CollapsibleBodyContent';
import { useCollapsible } from './CollapsibleContext';
import type { BaseBoxProps } from '~components/Box/BaseBox';
import type { CollapsibleBodyProps } from './types';
import BaseBox from '~components/Box/BaseBox';
import type { TestID } from '~utils/types';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import { makeAccessible } from '~utils/makeAccessible';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';

type CollapsibleBodyProps = {
children: ReactNode;
width?: BaseBoxProps['width'];
} & TestID;

const _CollapsibleBody = ({ children, testID, width }: CollapsibleBodyProps): ReactElement => {
const _CollapsibleBody = ({
children,
testID,
width,
_hasMargin = true,
}: CollapsibleBodyProps): ReactElement => {
const { collapsibleBodyId, isExpanded } = useCollapsible();
return (
<BaseBox
Expand All @@ -22,7 +21,7 @@ const _CollapsibleBody = ({ children, testID, width }: CollapsibleBodyProps): Re
{...makeAccessible({ role: 'region', hidden: !isExpanded })}
{...metaAttribute({ name: MetaConstants.CollapsibleBody, testID })}
>
<CollapsibleBodyContent>{children}</CollapsibleBodyContent>
<CollapsibleBodyContent _hasMargin={_hasMargin}>{children}</CollapsibleBodyContent>
</BaseBox>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ const AnimatedStyledCollapsibleBodyContent = styled(
};
});

const CollapsibleBodyContent = ({ children }: CollapsibleBodyContentProps): ReactElement => {
const CollapsibleBodyContent = ({
children,
_hasMargin,
}: CollapsibleBodyContentProps): ReactElement => {
const { isExpanded, direction } = useCollapsible();
const { theme } = useTheme();

Expand Down Expand Up @@ -125,7 +128,7 @@ const CollapsibleBodyContent = ({ children }: CollapsibleBodyContentProps): Reac
: nativeStyles.collapsibleBodyCollapsed
}
>
<Box {...getCollapsibleBodyContentBoxProps({ direction })}>{children}</Box>
<Box {...getCollapsibleBodyContentBoxProps({ direction, _hasMargin })}>{children}</Box>
</View>
</AnimatedStyledCollapsibleBodyContent>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ const StyledCollapsibleBodyContent = styled(BaseBox)<StyledCollapsibleBodyConten
};
});

const CollapsibleBodyContent = ({ children }: CollapsibleBodyContentProps): ReactElement => {
const CollapsibleBodyContent = ({
children,
_hasMargin,
}: CollapsibleBodyContentProps): ReactElement => {
const { isExpanded, defaultIsExpanded, direction } = useCollapsible();
const collapsibleBodyContentRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -126,7 +129,7 @@ const CollapsibleBodyContent = ({ children }: CollapsibleBodyContentProps): Reac
defaultIsExpanded={defaultIsExpanded}
onTransitionEnd={onTransitionEnd}
>
<Box {...getCollapsibleBodyContentBoxProps({ direction })}>{children}</Box>
<Box {...getCollapsibleBodyContentBoxProps({ direction, _hasMargin })}>{children}</Box>
</StyledCollapsibleBodyContent>
);
};
Expand Down
11 changes: 9 additions & 2 deletions packages/blade/src/components/Collapsible/CollapsibleLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ import type { LinkProps } from '~components/Link';
import { MetaConstants } from '~utils/metaAttribute';
import { BaseLink } from '~components/Link/BaseLink';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import type { StyledPropsBlade } from '~components/Box/styledProps';
import { getStyledProps } from '~components/Box/styledProps';

type CollapsibleLinkProps = Pick<
LinkProps,
'size' | 'isDisabled' | 'testID' | 'accessibilityLabel' | 'children'
>;
'color' | 'size' | 'isDisabled' | 'testID' | 'accessibilityLabel' | 'children'
> &
StyledPropsBlade;

const _CollapsibleLink = ({
children,
size,
color = 'primary',
isDisabled,
testID,
accessibilityLabel,
...styledProps
}: CollapsibleLinkProps): ReactElement => {
const { onExpandChange, isExpanded, collapsibleBodyId } = useCollapsible();

Expand All @@ -30,6 +35,7 @@ const _CollapsibleLink = ({
<BaseLink
variant="button"
size={size}
color={color}
icon={CollapsibleChevronIcon}
iconPosition="right"
isDisabled={isDisabled}
Expand All @@ -40,6 +46,7 @@ const _CollapsibleLink = ({
controls: collapsibleBodyId,
expanded: isExpanded,
}}
{...getStyledProps(styledProps)}
>
{children}
</BaseLink>
Expand Down
25 changes: 17 additions & 8 deletions packages/blade/src/components/Collapsible/commonStyles.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import type { CollapsibleProps } from './Collapsible';
import type { CollapsibleBodyProps } from './types';
import type { Theme } from '~components/BladeProvider';
import type { BoxProps } from '~components/Box';
import { makeMotionTime } from '~utils';

const getCollapsibleBodyContentBoxProps = ({
direction,
_hasMargin,
}: {
direction: CollapsibleProps['direction'];
}): BoxProps => ({
/**
* Need a margin inside the outside wrapper so this is
* included in height calculations and prevents jank
*/
marginTop: direction === 'bottom' ? 'spacing.5' : 'spacing.0',
marginBottom: direction === 'top' ? 'spacing.5' : 'spacing.0',
});
_hasMargin: CollapsibleBodyProps['_hasMargin'];
}): BoxProps => {
if (!_hasMargin) {
return {};
}

return {
/**
* Need a margin inside the outside wrapper so this is
* included in height calculations and prevents jank
*/
marginTop: direction === 'bottom' ? 'spacing.5' : 'spacing.0',
marginBottom: direction === 'top' ? 'spacing.5' : 'spacing.0',
};
};

const getOpacity = ({ isExpanded }: { isExpanded: boolean }): number => (isExpanded ? 1 : 0.8);

Expand Down
18 changes: 17 additions & 1 deletion packages/blade/src/components/Collapsible/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import type { ReactNode } from 'react';
import type { BaseBoxProps } from '~components/Box/BaseBox';
import type { TestID } from '~utils/types';

export type CollapsibleBodyContentProps = {
type CollapsibleBodyProps = {
children: ReactNode;
width?: BaseBoxProps['width'];
/**
* Internal
*
* Set to false to remove margin of CollapsibleBody
*/
_hasMargin?: boolean;
} & TestID;

type CollapsibleBodyContentProps = {
children: ReactNode;
_hasMargin?: CollapsibleBodyProps['_hasMargin'];
};

export type { CollapsibleBodyProps, CollapsibleBodyContentProps };
32 changes: 24 additions & 8 deletions packages/blade/src/components/Drawer/Drawer.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const _Drawer = ({
accessibilityLabel,
showOverlay = true,
initialFocusRef,
isLazy = true,
testID,
}: DrawerProps): React.ReactElement => {
const closeButtonRef = React.useRef<HTMLDivElement>(null);
Expand All @@ -87,18 +88,18 @@ const _Drawer = ({
const drawerId = useId('drawer');
const { drawerStack, addToDrawerStack, removeFromDrawerStack } = useDrawerStack();

const { isMounted, isVisible } = usePresence(isOpen, {
const { isMounted, isVisible, isExiting } = usePresence(isOpen, {
enterTransitionDuration: theme.motion.duration.gentle,
exitTransitionDuration: theme.motion.duration.xmoderate,
initialEnter: true,
});

const { stackingLevel, isFirstDrawerInStack } = React.useMemo(() => {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
const level = drawerStack.indexOf(drawerId) + 1;
const level = Object.keys(drawerStack).indexOf(drawerId) + 1;
return {
stackingLevel: level,
isFirstDrawerInStack: level === 1 && drawerStack.length > 1,
isFirstDrawerInStack: level === 1 && Object.keys(drawerStack).length > 1,
};
}, [drawerId, drawerStack]);

Expand All @@ -108,9 +109,9 @@ const _Drawer = ({

React.useEffect(() => {
if (isOpen) {
addToDrawerStack(drawerId);
addToDrawerStack({ elementId: drawerId, onDismiss });
} else {
removeFromDrawerStack(drawerId);
removeFromDrawerStack({ elementId: drawerId });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
Expand All @@ -123,12 +124,27 @@ const _Drawer = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMounted]);

const contextValue = React.useMemo(
() => ({
close: onDismiss,
closeButtonRef,
stackingLevel,
isExiting,
}),
[isExiting, onDismiss, stackingLevel],
);

return (
<DrawerContext.Provider value={{ close: onDismiss, closeButtonRef }}>
<DrawerContext.Provider value={contextValue}>
<FloatingPortal>
{isMounted ? (
<FloatingFocusManager context={context} initialFocus={initialFocusRef ?? closeButtonRef}>
{isMounted || !isLazy ? (
<FloatingFocusManager
context={context}
initialFocus={initialFocusRef ?? closeButtonRef}
returnFocus={true}
>
<BaseBox
display={isLazy ? undefined : isMounted ? 'block' : 'none'}
position="fixed"
{...metaAttribute({
name: MetaConstants.Drawer,
Expand Down
11 changes: 9 additions & 2 deletions packages/blade/src/components/Drawer/DrawerContext.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';

const DrawerContext = React.createContext<{
type DrawerContextType = {
close: () => void;
closeButtonRef?: React.MutableRefObject<any>;
stackingLevel?: number;
isExiting: boolean;
};

const DrawerContext = React.createContext<DrawerContextType>({
// eslint-disable-next-line @typescript-eslint/no-empty-function
}>({ close: () => {} });
close: () => {},
isExiting: false,
});

export { DrawerContext };
26 changes: 22 additions & 4 deletions packages/blade/src/components/Drawer/DrawerSubcomponents.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { drawerComponentIds } from './drawerComponentIds';
import { DrawerContext } from './DrawerContext';
import type { DrawerHeaderProps } from './types';
import { useDrawerStack } from './StackProvider';
import { BaseHeader } from '~components/BaseHeaderFooter/BaseHeader';
import { Box } from '~components/Box';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
Expand All @@ -13,13 +14,28 @@ const _DrawerHeader = ({
trailing,
titleSuffix,
}: DrawerHeaderProps): React.ReactElement => {
const { close, closeButtonRef } = React.useContext(DrawerContext);
const { close, closeButtonRef, stackingLevel, isExiting } = React.useContext(DrawerContext);
const { drawerStack } = useDrawerStack();
const closeAllDrawers = (): void => {
for (const onDismiss of Object.values(drawerStack)) {
onDismiss();
}
};

const isStackedDrawer = stackingLevel && stackingLevel > 1;

const isAtleastOneDrawerOpen = Object.keys(drawerStack).length > 0;
// This condition is to avoid back button disappear while stacked drawer is in the exiting transition
const isDrawerExiting = isAtleastOneDrawerOpen && isExiting && stackingLevel !== 1;

return (
<BaseHeader
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
showBackButton={isStackedDrawer || isDrawerExiting}
showCloseButton={true}
closeButtonRef={closeButtonRef}
onCloseButtonClick={() => close()}
onCloseButtonClick={() => closeAllDrawers()}
onBackButtonClick={() => close()}
title={title}
titleSuffix={titleSuffix}
subtitle={subtitle}
Expand Down Expand Up @@ -47,9 +63,11 @@ const DrawerHeader = assignWithoutSideEffects(_DrawerHeader, {
componentId: drawerComponentIds.DrawerHeader,
});

const drawerPadding = 'spacing.6';

const _DrawerBody = ({ children }: { children: React.ReactNode }): React.ReactElement => {
return (
<Box padding="spacing.6" overflow="auto" flex="1">
<Box padding={drawerPadding} overflow="auto" flex="1">
{children}
</Box>
);
Expand All @@ -58,4 +76,4 @@ const DrawerBody = assignWithoutSideEffects(_DrawerBody, {
componentId: drawerComponentIds.DrawerBody,
});

export { DrawerHeader, DrawerBody };
export { DrawerHeader, DrawerBody, drawerPadding };
Loading

0 comments on commit 0d3e260

Please sign in to comment.