diff --git a/.changeset/red-bats-allow.md b/.changeset/red-bats-allow.md new file mode 100644 index 00000000000..012611de304 --- /dev/null +++ b/.changeset/red-bats-allow.md @@ -0,0 +1,7 @@ +--- +"@razorpay/blade": minor +--- + +feat(SideNav): add SideNav component + +Checkout [SideNav Documentation](https://blade.razorpay.com/?path=/docs/components-sidenav--docs) diff --git a/packages/blade/.storybook/react/preview.tsx b/packages/blade/.storybook/react/preview.tsx index 4635af394e3..2f93be9f7a5 100644 --- a/packages/blade/.storybook/react/preview.tsx +++ b/packages/blade/.storybook/react/preview.tsx @@ -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' }; diff --git a/packages/blade/src/components/Accordion/AccordionItem.tsx b/packages/blade/src/components/Accordion/AccordionItem.tsx index 0fff0d29094..53ac40e005f 100644 --- a/packages/blade/src/components/Accordion/AccordionItem.tsx +++ b/packages/blade/src/components/Accordion/AccordionItem.tsx @@ -124,6 +124,7 @@ const AccordionItem = ({ onExpandChange={handleExpandChange} // Accordion has its own width restrictions _shouldApplyWidthRestrictions={false} + _dangerouslyDisableValidations={true} > void; + /** + * **Internal**: disables trigger validations. Used for AccordionButton and SideNavLink internally + */ + _dangerouslyDisableValidations?: boolean; /** * **Internal**: used to override responsive width restrictions */ @@ -67,6 +71,7 @@ const Collapsible = ({ onExpandChange, testID, _shouldApplyWidthRestrictions = true, + _dangerouslyDisableValidations = false, ...styledProps }: CollapsibleProps): ReactElement => { const [isBodyExpanded, setIsBodyExpanded] = useState(isExpanded ?? defaultIsExpanded); @@ -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`, diff --git a/packages/blade/src/components/Collapsible/CollapsibleBody.tsx b/packages/blade/src/components/Collapsible/CollapsibleBody.tsx index 0d9cca6f1f5..be4c18409ce 100644 --- a/packages/blade/src/components/Collapsible/CollapsibleBody.tsx +++ b/packages/blade/src/components/Collapsible/CollapsibleBody.tsx @@ -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 ( - {children} + {children} ); }; diff --git a/packages/blade/src/components/Collapsible/CollapsibleBodyContent.native.tsx b/packages/blade/src/components/Collapsible/CollapsibleBodyContent.native.tsx index 4f12ff07cf8..93225c0a7d7 100644 --- a/packages/blade/src/components/Collapsible/CollapsibleBodyContent.native.tsx +++ b/packages/blade/src/components/Collapsible/CollapsibleBodyContent.native.tsx @@ -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(); @@ -125,7 +128,7 @@ const CollapsibleBodyContent = ({ children }: CollapsibleBodyContentProps): Reac : nativeStyles.collapsibleBodyCollapsed } > - {children} + {children} ); diff --git a/packages/blade/src/components/Collapsible/CollapsibleBodyContent.web.tsx b/packages/blade/src/components/Collapsible/CollapsibleBodyContent.web.tsx index 410f62e5af7..6e669e0e887 100644 --- a/packages/blade/src/components/Collapsible/CollapsibleBodyContent.web.tsx +++ b/packages/blade/src/components/Collapsible/CollapsibleBodyContent.web.tsx @@ -49,7 +49,10 @@ const StyledCollapsibleBodyContent = styled(BaseBox) { +const CollapsibleBodyContent = ({ + children, + _hasMargin, +}: CollapsibleBodyContentProps): ReactElement => { const { isExpanded, defaultIsExpanded, direction } = useCollapsible(); const collapsibleBodyContentRef = useRef(null); @@ -126,7 +129,7 @@ const CollapsibleBodyContent = ({ children }: CollapsibleBodyContentProps): Reac defaultIsExpanded={defaultIsExpanded} onTransitionEnd={onTransitionEnd} > - {children} + {children} ); }; diff --git a/packages/blade/src/components/Collapsible/CollapsibleLink.tsx b/packages/blade/src/components/Collapsible/CollapsibleLink.tsx index 51c2963b99a..554833a850c 100644 --- a/packages/blade/src/components/Collapsible/CollapsibleLink.tsx +++ b/packages/blade/src/components/Collapsible/CollapsibleLink.tsx @@ -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(); @@ -30,6 +35,7 @@ const _CollapsibleLink = ({ {children} diff --git a/packages/blade/src/components/Collapsible/commonStyles.ts b/packages/blade/src/components/Collapsible/commonStyles.ts index 705835fc57b..ee23e117b83 100644 --- a/packages/blade/src/components/Collapsible/commonStyles.ts +++ b/packages/blade/src/components/Collapsible/commonStyles.ts @@ -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); diff --git a/packages/blade/src/components/Collapsible/types.ts b/packages/blade/src/components/Collapsible/types.ts index d61c8efd89c..d58c921fd57 100644 --- a/packages/blade/src/components/Collapsible/types.ts +++ b/packages/blade/src/components/Collapsible/types.ts @@ -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 }; diff --git a/packages/blade/src/components/Drawer/Drawer.web.tsx b/packages/blade/src/components/Drawer/Drawer.web.tsx index 4cbdd64ea0a..1587b6205a6 100644 --- a/packages/blade/src/components/Drawer/Drawer.web.tsx +++ b/packages/blade/src/components/Drawer/Drawer.web.tsx @@ -72,6 +72,7 @@ const _Drawer = ({ accessibilityLabel, showOverlay = true, initialFocusRef, + isLazy = true, testID, }: DrawerProps): React.ReactElement => { const closeButtonRef = React.useRef(null); @@ -87,7 +88,7 @@ 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, @@ -95,10 +96,10 @@ const _Drawer = ({ 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]); @@ -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]); @@ -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 ( - + - {isMounted ? ( - + {isMounted || !isLazy ? ( + void; closeButtonRef?: React.MutableRefObject; + stackingLevel?: number; + isExiting: boolean; +}; + +const DrawerContext = React.createContext({ // eslint-disable-next-line @typescript-eslint/no-empty-function -}>({ close: () => {} }); + close: () => {}, + isExiting: false, +}); export { DrawerContext }; diff --git a/packages/blade/src/components/Drawer/DrawerSubcomponents.web.tsx b/packages/blade/src/components/Drawer/DrawerSubcomponents.web.tsx index 7ade41f7303..ed6509b3326 100644 --- a/packages/blade/src/components/Drawer/DrawerSubcomponents.web.tsx +++ b/packages/blade/src/components/Drawer/DrawerSubcomponents.web.tsx @@ -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'; @@ -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 ( close()} + onCloseButtonClick={() => closeAllDrawers()} + onBackButtonClick={() => close()} title={title} titleSuffix={titleSuffix} subtitle={subtitle} @@ -47,9 +63,11 @@ const DrawerHeader = assignWithoutSideEffects(_DrawerHeader, { componentId: drawerComponentIds.DrawerHeader, }); +const drawerPadding = 'spacing.6'; + const _DrawerBody = ({ children }: { children: React.ReactNode }): React.ReactElement => { return ( - + {children} ); @@ -58,4 +76,4 @@ const DrawerBody = assignWithoutSideEffects(_DrawerBody, { componentId: drawerComponentIds.DrawerBody, }); -export { DrawerHeader, DrawerBody }; +export { DrawerHeader, DrawerBody, drawerPadding }; diff --git a/packages/blade/src/components/Drawer/StackProvider.tsx b/packages/blade/src/components/Drawer/StackProvider.tsx index be2220b8b48..d8fa761cb09 100644 --- a/packages/blade/src/components/Drawer/StackProvider.tsx +++ b/packages/blade/src/components/Drawer/StackProvider.tsx @@ -1,34 +1,47 @@ import React from 'react'; -type StackActionType = (elementId: string) => void; +type AddToStackType = ({ + elementId, + onDismiss, +}: { + elementId: string; + onDismiss: () => void; +}) => void; + +type RemoveFromStackType = ({ elementId }: { elementId: string }) => void; type GlobalStackStateType = { - drawerStack: string[]; - addToDrawerStack: StackActionType; - removeFromDrawerStack: StackActionType; + drawerStack: Record void>; + addToDrawerStack: AddToStackType; + removeFromDrawerStack: RemoveFromStackType; }; const StackingContext = React.createContext({ - drawerStack: [], + drawerStack: {}, // eslint-disable-next-line @typescript-eslint/no-empty-function addToDrawerStack: () => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function removeFromDrawerStack: () => {}, }); -const useStacking = (): [string[], StackActionType, StackActionType] => { - const [stack, setStack] = React.useState([]); +const useStacking = (): [ + GlobalStackStateType['drawerStack'], + AddToStackType, + RemoveFromStackType, +] => { + const [stack, setStack] = React.useState({}); - const addToStack = (elementId: string): void => { - if (stack.includes(elementId)) { + const addToStack: AddToStackType = ({ elementId, onDismiss }) => { + if (stack[elementId]) { return; } - setStack([...stack, elementId]); + setStack({ ...stack, [elementId]: onDismiss }); }; - const removeFromStack = (elementId: string): void => { - setStack(stack.filter((stackElementId) => stackElementId !== elementId)); + const removeFromStack: RemoveFromStackType = ({ elementId }) => { + const { [elementId]: _, ...newStack } = stack; + setStack(newStack); }; return [stack, addToStack, removeFromStack]; diff --git a/packages/blade/src/components/Drawer/__tests__/Drawer.test.stories.tsx b/packages/blade/src/components/Drawer/__tests__/Drawer.test.stories.tsx index 6e43c0fcdc4..ee560fa9ea1 100644 --- a/packages/blade/src/components/Drawer/__tests__/Drawer.test.stories.tsx +++ b/packages/blade/src/components/Drawer/__tests__/Drawer.test.stories.tsx @@ -117,9 +117,14 @@ export const StackingDrawerOpen: StoryFn = (props): React.ReactEl }; StackingDrawerOpen.play = async () => { - const { getByRole, queryByRole, getAllByLabelText, getByText, queryByText } = within( - document.body, - ); + const { + getByRole, + queryByRole, + getAllByLabelText, + getByText, + getByLabelText, + queryByText, + } = within(document.body); // first drawer open await expect(queryByRole('heading', { name: 'Drawer Heading' })).not.toBeInTheDocument(); @@ -133,9 +138,10 @@ StackingDrawerOpen.play = async () => { await waitFor(() => expect(getByText('Drawer 2 Heading')).toBeVisible()); const closeButtons = getAllByLabelText('Close'); + const backButton = getByLabelText('Back'); // 2nd drawer close - await userEvent.click(closeButtons[1]); + await userEvent.click(backButton); await waitFor(() => expect(queryByText('Drawer 2 Heading')).not.toBeInTheDocument()); await expect(getByRole('heading', { name: 'Drawer Heading' })).toBeVisible(); @@ -144,6 +150,16 @@ StackingDrawerOpen.play = async () => { await waitFor(() => expect(queryByRole('heading', { name: 'Drawer Heading' })).not.toBeInTheDocument(), ); + + // Open drawer again and close all at once with close button + await userEvent.click(drawerToggleButton); + await waitFor(() => expect(getByRole('heading', { name: 'Drawer Heading' })).toBeVisible()); + await userEvent.click(getByRole('button', { name: 'Open 2nd Drawer' })); + await waitFor(() => expect(getByText('Drawer 2 Heading')).toBeVisible()); + await userEvent.click(getAllByLabelText('Close')[1]); + await waitFor(() => + expect(queryByRole('heading', { name: 'Drawer 2 Heading' })).not.toBeInTheDocument(), + ); }; export const KeyboardNavigations: StoryFn = (props): React.ReactElement => { @@ -168,6 +184,7 @@ KeyboardNavigations.play = async () => { await waitFor(() => expect(getByText('Drawer 2 Heading')).toBeVisible()); // 2nd drawer close + await userEvent.keyboard('{Tab}'); await userEvent.keyboard('{Enter}'); await waitFor(() => expect(queryByText('Drawer 2 Heading')).not.toBeInTheDocument()); await expect(getByRole('heading', { name: 'Drawer Heading' })).toBeVisible(); @@ -175,11 +192,11 @@ KeyboardNavigations.play = async () => { // 1st drawer close // the test gets flaky if we try to close drawer immediately after it opens so adding some delay here to let drawer open correctly await sleep(300); + await expect(getByRole('button', { name: 'Open 2nd Drawer' })).toHaveFocus(); await userEvent.keyboard('{Escape}'); await waitFor(() => expect(queryByRole('heading', { name: 'Drawer Heading' })).not.toBeInTheDocument(), ); - await waitFor(() => expect(drawerToggleButton).toHaveFocus()); }; export default { diff --git a/packages/blade/src/components/Drawer/docs/Drawer.stories.tsx b/packages/blade/src/components/Drawer/docs/Drawer.stories.tsx index 39bec061372..2e77ce8820d 100644 --- a/packages/blade/src/components/Drawer/docs/Drawer.stories.tsx +++ b/packages/blade/src/components/Drawer/docs/Drawer.stories.tsx @@ -6,12 +6,20 @@ import { Drawer, DrawerBody, DrawerHeader } from '../'; import { DrawerStackingStory } from './stories'; import { Box } from '~components/Box'; import { Button } from '~components/Button'; -import { DownloadIcon } from '~components/Icons'; +import { AnnouncementIcon, DownloadIcon } from '~components/Icons'; import StoryPageWrapper from '~utils/storybook/StoryPageWrapper'; import { Sandbox } from '~utils/storybook/Sandbox'; -import { Heading } from '~components/Typography'; +import { Heading, Text } from '~components/Typography'; import { Badge } from '~components/Badge'; import { TextInput } from '~components/Input/TextInput'; +import { + Card, + CardBody, + CardFooter, + CardFooterTrailing, + CardHeader, + CardHeaderLeading, +} from '~components/Card'; const Page = (): React.ReactElement => { return ( @@ -89,6 +97,82 @@ NoOverlay.args = { showOverlay: false, }; +export const DrawerStacking = (args: DrawerProps): React.ReactElement => { + const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); + const [isSecondDrawerOpen, setIsSecondDrawerOpen] = React.useState(false); + return ( + + + setIsDrawerOpen(false)}> + New} + subtitle="See your payment details here" + trailing={ + + + + + setIsSecondDrawerOpen(false)}> + } + title="Announcements" + subtitle="This is second drawer" + /> + + + + + + + + Book Your Tickets for Razorpay FTX + + + + + + + + + + ); +}; + export const InitialFocus = (args: DrawerProps): React.ReactElement => { const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); const drawerInitialFocusRef = React.useRef(null); diff --git a/packages/blade/src/components/Drawer/types.ts b/packages/blade/src/components/Drawer/types.ts index c956cc34086..5944db28ef1 100644 --- a/packages/blade/src/components/Drawer/types.ts +++ b/packages/blade/src/components/Drawer/types.ts @@ -38,6 +38,14 @@ type DrawerProps = { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any initialFocusRef?: React.MutableRefObject; + + /** + * If `true`, the DrawerBody will be rendered only when it becomes active. + * Set to `false` to keep DrawerBody in DOM + * + * @default true + */ + isLazy?: boolean; } & TestID; type DrawerHeaderProps = { diff --git a/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/AutoComplete.test.stories.tsx b/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/AutoComplete.test.stories.tsx index 33a1bfe2b42..be7b7a0d949 100644 --- a/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/AutoComplete.test.stories.tsx +++ b/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/AutoComplete.test.stories.tsx @@ -49,9 +49,9 @@ ItemSelect.play = async () => { const { getByRole } = within(document.body); const selectInput = getByRole('combobox', { name: 'City' }); await userEvent.click(selectInput); - const option = getByRole('option', { name: 'Bengaluru' }); - await userEvent.click(option); - await expect(getByRole('combobox', { name: 'City' })).toHaveValue('Bengaluru'); + await waitFor(() => expect(getByRole('option', { name: 'Bengaluru' })).toBeVisible()); + await userEvent.click(getByRole('option', { name: 'Bengaluru' })); + await waitFor(() => expect(getByRole('combobox', { name: 'City' })).toHaveValue('Bengaluru')); }; export const ItemSort: StoryFn = (props): React.ReactElement => { @@ -67,7 +67,7 @@ ItemSort.play = async () => { const puneOption = getByRole('option', { name: 'Pune' }); await waitFor(() => expect(puneOption).toBeVisible()); await userEvent.click(puneOption); - await expect(getByRole('combobox', { name: 'City' })).toHaveValue('Pune'); + await waitFor(() => expect(getByRole('combobox', { name: 'City' })).toHaveValue('Pune')); }; export const ItemMultiSelect: StoryFn = (props): React.ReactElement => { @@ -119,7 +119,7 @@ Accessibility.play = async () => { await userEvent.type(autoComplete, 'i'); // keep 1st item as active - await expect(getActiveDescendant(autoComplete)).toBe('Mumbai'); + await waitFor(() => expect(getActiveDescendant(autoComplete)).toBe('Mumbai')); // move to 2nd item await userEvent.keyboard('{ArrowDown}'); @@ -131,7 +131,7 @@ Accessibility.play = async () => { // select item await userEvent.keyboard('{Enter}'); - await expect(getByRole('combobox', { name: 'City' })).toHaveValue('Chennai'); + await waitFor(() => expect(getByRole('combobox', { name: 'City' })).toHaveValue('Chennai')); await waitFor(() => expect(getByTestId('dropdown-overlay')).not.toBeVisible()); // close dropdown @@ -200,7 +200,7 @@ export const ControlledDropdownSingleSelect: StoryFn = (): Reac }; ControlledDropdownSingleSelect.play = async () => { - const { getByRole, getByTestId } = within(document.body); + const { getByRole, getByTestId, findByRole } = within(document.body); const selectInput = getByRole('combobox', { name: 'Select City' }); // external button control selection test @@ -209,7 +209,7 @@ ControlledDropdownSingleSelect.play = async () => { // select input's control test await userEvent.click(selectInput); - await userEvent.click(getByRole('option', { name: 'Pune' })); + await userEvent.click(await findByRole('option', { name: 'Pune' })); await waitFor(() => expect(selectInput).toHaveValue('Pune')); // input value change test @@ -272,7 +272,7 @@ export const ControlledDropdownMultiSelect: StoryFn = (): React }; ControlledDropdownMultiSelect.play = async () => { - const { getByRole, queryAllByLabelText } = within(document.body); + const { getByRole, queryAllByLabelText, findByRole } = within(document.body); const selectInput = getByRole('combobox', { name: 'Select City' }); // Select 1 item controlled @@ -282,7 +282,7 @@ ControlledDropdownMultiSelect.play = async () => { // select 2nd item from actionlist await userEvent.click(selectInput); - await userEvent.click(getByRole('option', { name: 'Pune' })); + await userEvent.click(await findByRole('option', { name: 'Pune' })); await waitFor(() => expect(queryAllByLabelText('Close Pune tag')?.[0]).toBeInTheDocument()); await expect(queryAllByLabelText('Close Bangalore tag')?.[0]).toBeInTheDocument(); diff --git a/packages/blade/src/components/ProgressBar/CircularProgressBar.native.tsx b/packages/blade/src/components/ProgressBar/CircularProgressBar.native.tsx index 7917c29d33d..2c64c4fade7 100644 --- a/packages/blade/src/components/ProgressBar/CircularProgressBar.native.tsx +++ b/packages/blade/src/components/ProgressBar/CircularProgressBar.native.tsx @@ -36,7 +36,10 @@ const StyledSVGText = styled(SVGText), 'size return { ...getBaseTextStyles({ theme, ...textProps }), strokeWidth: 0, - fill: getIn(theme.colors, textProps.color!), + fill: + textProps.color === 'currentColor' + ? textProps.color + : getIn(theme.colors, textProps.color!), }; }, ); diff --git a/packages/blade/src/components/SideNav/SideNav.native.tsx b/packages/blade/src/components/SideNav/SideNav.native.tsx new file mode 100644 index 00000000000..9d34f434b56 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNav.native.tsx @@ -0,0 +1,14 @@ +import type { SideNavProps } from './types'; +import { Text } from '~components/Typography'; +import { throwBladeError } from '~utils/logger'; + +const SideNav = (_props: SideNavProps): React.ReactElement => { + throwBladeError({ + message: 'SideNav is not yet implemented for native', + moduleName: 'SideNav', + }); + + return SideNav Component is not available for Native mobile apps.; +}; + +export { SideNav }; diff --git a/packages/blade/src/components/SideNav/SideNav.web.tsx b/packages/blade/src/components/SideNav/SideNav.web.tsx new file mode 100644 index 00000000000..4ce1552b42a --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNav.web.tsx @@ -0,0 +1,339 @@ +import React from 'react'; +import styled from 'styled-components'; +import { SideNavContext } from './SideNavContext'; +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, +} from './tokens'; +import BaseBox from '~components/Box/BaseBox'; +import { makeMotionTime, makeSize, makeSpace } from '~utils'; +import { Drawer, DrawerBody, DrawerHeader } from '~components/Drawer'; +import { SkipNavContent, SkipNavLink } from '~components/SkipNav/SkipNav'; +import { useIsMobile } from '~utils/useIsMobile'; +import { getStyledProps } from '~components/Box/styledProps'; +import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; + +const { + COLLAPSED, + SHOW_WHEN_COLLAPSED, + HIDE_WHEN_COLLAPSED, + TRANSITIONING, + L1_ITEM_WRAPPER, +} = classes; + +const MobileL1Container = styled(BaseBox)(() => { + return { + [`.${SHOW_WHEN_COLLAPSED}`]: { + display: 'none', + }, + }; +}); + +const StyledL1Menu = styled(BaseBox)((props) => { + const moderate = makeMotionTime(props.theme.motion.duration.moderate); + const gentle = makeMotionTime(props.theme.motion.duration.gentle); + const easing = props.theme.motion.easing; + + const l1Expand = `width ${gentle} ${easing.entrance.revealing}`; + const l1Collapse = `width ${moderate} ${easing.exit.revealing}`; + + return { + width: '100%', + transition: l1Expand, + [`& > .${L1_ITEM_WRAPPER}`]: { + padding: makeSpace(props.theme.spacing[3]), + }, + [`.${SHOW_WHEN_COLLAPSED}`]: { + display: 'none', + }, + [`&.${COLLAPSED}`]: { + width: makeSize(COLLAPSED_L1_WIDTH), + transition: l1Collapse, + [`& > .${L1_ITEM_WRAPPER}`]: { + padding: `${makeSpace(props.theme.spacing[3])} ${makeSpace(props.theme.spacing[3])}`, + }, + [`&:not(.${TRANSITIONING}) .${HIDE_WHEN_COLLAPSED}`]: { + display: 'none', + }, + [`&:not(.${TRANSITIONING}) .${SHOW_WHEN_COLLAPSED}`]: { + display: 'initial', + }, + }, + }; +}); + +const getL1MenuClassName = ({ + isL1Collapsed, + isL1Hovered, + isTransitioning, +}: { + isL1Collapsed: boolean; + isL1Hovered: boolean; + isTransitioning: boolean; +}): string => { + const isMenuCollapsed = isL1Collapsed && !isL1Hovered; + + if (isMenuCollapsed) { + if (isTransitioning) { + return `${COLLAPSED} ${TRANSITIONING}`; + } + + return COLLAPSED; + } + + return ''; +}; + +/** + * ### SideNav component + * + * The side navigation is positioned along the left side of the screen that provides quick access to different sections or functionalities of the application. + * + * --- + * + * #### Usage + * + * SideNav requires handling active state with React Router, Checkout Usage with React Router v6 at - [SideNav Documentation](https://blade.razorpay.com/?path=/docs/components-sidenav--docs) + * + */ +const SideNav = ({ + children, + isOpen, + onDismiss, + banner, + testID, + ...styledProps +}: SideNavProps): React.ReactElement => { + const l2PortalContainerRef = React.useRef(null); + const l1ContainerRef = React.useRef(null); + const timeoutIdsRef = React.useRef([]); + const mouseOverTimeoutRef = React.useRef(); + const [isL1Collapsed, setIsL1Collapsed] = React.useState(false); + const [isMobileL2Open, setIsMobileL2Open] = React.useState(false); + const [isL1Hovered, setIsL1Hovered] = React.useState(false); + const [isHoverAgainEnabled, setIsHoverAgainEnabled] = React.useState(true); + const [isTransitioning, setIsTransitioning] = React.useState(false); + const [l2DrawerTitle, setL2DrawerTitle] = React.useState(''); + + const isMobile = useIsMobile(); + + const closeMobileNav = (): void => { + if (isMobile) { + setIsMobileL2Open(false); + onDismiss?.(); + } + }; + + const cleanupTransition = (): void => { + const clearTransitionTimeout = setTimeout(() => { + if (isTransitioning) { + setIsTransitioning(false); + } + }, TRANSITION_CLEANUP_DELAY); + timeoutIdsRef.current.push(clearTransitionTimeout); + }; + + /** + * Handles L1 -> L2 menu changes based on active item + */ + const onLinkActiveChange: SideNavContextType['onLinkActiveChange'] = (args) => { + const isL1ItemActive = args.level === 1 && args.isActive; + + if (isL1ItemActive) { + if (args.isL2Trigger) { + // Click on L2 Trigger + if (isMobile) { + setL2DrawerTitle(args.title); + setIsMobileL2Open(true); + return; + } + + setIsL1Collapsed(true); + + // `args.isFirstRender` checks if the item that triggered this change, triggered it during first render or during subsequent change + if (!args.isFirstRender) { + setIsTransitioning(true); + cleanupTransition(); + setIsL1Hovered(false); + setIsHoverAgainEnabled(false); + // For some delay, we disable hover to expand behaviour to avoid buggy flicker when cursor is on L1 while its trying to close + const hoverAgainTimeout = setTimeout(() => { + setIsHoverAgainEnabled(true); + }, HOVER_AGAIN_DELAY); + timeoutIdsRef.current.push(hoverAgainTimeout); + } + } else { + // Click on normal L1 Item + // eslint-disable-next-line no-lonely-if + if (isMobile) { + setIsMobileL2Open(false); + } + // Ensures that if Normal L1 item is clicked, the L1 stays expanded + setIsL1Collapsed(false); + } + } + }; + + const contextValue = React.useMemo( + () => ({ + l2PortalContainerRef, + onLinkActiveChange, + closeMobileNav, + isL1Collapsed: isMobile ? isMobileL2Open : isL1Collapsed, + setIsL1Collapsed, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [isL1Collapsed, isMobile, isMobileL2Open], + ); + + React.useEffect(() => { + return () => { + for (const timeoutId of timeoutIdsRef.current) { + clearTimeout(timeoutId); + } + timeoutIdsRef.current = []; + }; + }, []); + + return ( + + {isMobile && onDismiss ? ( + <> + {/* L1 */} + + + + + {children} + + + + {/* L2 */} + setIsMobileL2Open(false)} isLazy={false}> + + + + + + + ) : ( + + {banner ? ( + + {banner} + + ) : null} + + + { + // This check ensures transitioning is set to false only when its true + // And only the l1Container element's transitions are considered and other transitions of l1 expand or child elements are ignored + if (isTransitioning && l1ContainerRef.current === e.target) { + setIsTransitioning(false); + } + }} + // Hmm you might be wondering, why is `onMouseOver` paired with `onMouseLeave`? A sane person would pair `onMouseOver` with `onMouseOut`, and `onMouseEnter` with `onMouseLeave` + // since they are logical equivalents of each other. So why don't we do that? Hold tight, you're in for a ride ☕️. + // + // 1. In an ideal scenario, we would put `onMouseEnter` and `onMouseLeave` here and expect things to work. + // 2. The L2 menu of our SideNav is React Portalled out of the L1 child + // 3. React considers its own children as true children for JS events and not DOM children (Checkout React Portal Caveats - https://react.dev/reference/react-dom/createPortal#caveats) + // 3. In the next ideal scenario, we would put `e.stopPropagation` on child component of portal like React recommends, except mouseenter, mouseleave events don't propagate at all (https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseenter_event#usage_notes) + // 4. So `onMouseEnter` gets triggered on L2 enter. But we don't want to open L1 menu on L2 hover + // 5. Thus we use `onMouseOver` for hover part and call e.stopPropagation in portal child (SideNavLevel). + // 6. But in case of unhover/leave, we don't want to trigger mouseOut for all child components individually. We want 1 hover out of L1 menu. Thus we use `onMouseLeave` + onMouseOver={() => { + if (mouseOverTimeoutRef.current) { + clearTimeout(mouseOverTimeoutRef.current); + } + if (isL1Collapsed && isHoverAgainEnabled) { + setIsL1Hovered(true); + } + }} + onMouseLeave={() => { + if (isL1Collapsed && isL1Hovered) { + mouseOverTimeoutRef.current = setTimeout(() => { + setIsL1Hovered(false); + setIsTransitioning(true); + cleanupTransition(); + }, L1_EXIT_HOVER_DELAY); + } + }} + > + + {children} + + + + + )} + + ); +}; + +export { SideNav }; diff --git a/packages/blade/src/components/SideNav/SideNavBody.native.tsx b/packages/blade/src/components/SideNav/SideNavBody.native.tsx new file mode 100644 index 00000000000..558509df571 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavBody.native.tsx @@ -0,0 +1,14 @@ +import type { SideNavBodyProps } from './types'; +import { Text } from '~components/Typography'; +import { throwBladeError } from '~utils/logger'; + +const SideNavBody = (_props: SideNavBodyProps): React.ReactElement => { + throwBladeError({ + message: 'SideNavBody is not yet implemented for native', + moduleName: 'SideNavBody', + }); + + return SideNavBody Component is not available for Native mobile apps.; +}; + +export { SideNavBody }; diff --git a/packages/blade/src/components/SideNav/SideNavBody.web.tsx b/packages/blade/src/components/SideNav/SideNavBody.web.tsx new file mode 100644 index 00000000000..605745270f8 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavBody.web.tsx @@ -0,0 +1,13 @@ +import { classes } from './tokens'; +import type { SideNavBodyProps } from './types'; +import BaseBox from '~components/Box/BaseBox'; + +const SideNavBody = ({ children }: SideNavBodyProps): React.ReactElement => { + return ( + + {children} + + ); +}; + +export { SideNavBody }; diff --git a/packages/blade/src/components/SideNav/SideNavContext.ts b/packages/blade/src/components/SideNav/SideNavContext.ts new file mode 100644 index 00000000000..a477428077e --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavContext.ts @@ -0,0 +1,24 @@ +import React from 'react'; +import type { NavLinkContextType, SideNavContextType } from './types'; +import { throwBladeError } from '~utils/logger'; + +const SideNavContext = React.createContext({}); + +const useSideNav = (): SideNavContextType => { + const value = React.useContext(SideNavContext); + if (!value) { + throwBladeError({ + moduleName: 'SideNavContext', + message: 'SideNav* components cannot be used outside SideNav', + }); + } + return value; +}; + +const NavLinkContext = React.createContext({}); +const useNavLink = (): NavLinkContextType => { + const value = React.useContext(NavLinkContext); + return value; +}; + +export { SideNavContext, useSideNav, NavLinkContext, useNavLink }; diff --git a/packages/blade/src/components/SideNav/SideNavFooter.native.tsx b/packages/blade/src/components/SideNav/SideNavFooter.native.tsx new file mode 100644 index 00000000000..97bb412edbd --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavFooter.native.tsx @@ -0,0 +1,14 @@ +import type { SideNavFooterProps } from './types'; +import { Text } from '~components/Typography'; +import { throwBladeError } from '~utils/logger'; + +const SideNavFooter = (_props: SideNavFooterProps): React.ReactElement => { + throwBladeError({ + message: 'SideNavFooter is not yet implemented for native', + moduleName: 'SideNavFooter', + }); + + return SideNavFooter Component is not available for Native mobile apps.; +}; + +export { SideNavFooter }; diff --git a/packages/blade/src/components/SideNav/SideNavFooter.web.tsx b/packages/blade/src/components/SideNav/SideNavFooter.web.tsx new file mode 100644 index 00000000000..a831c0b7040 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavFooter.web.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type { SideNavFooterProps } from './types'; +import { classes } from './tokens'; +import BaseBox from '~components/Box/BaseBox'; +import { drawerPadding } from '~components/Drawer'; +import getIn from '~utils/lodashButBetter/get'; +import type { Theme } from '~components/BladeProvider'; +import { makeSpace, useTheme } from '~utils'; + +const getDrawerPadding = (theme: Theme): `${number}px` => { + const negativePaddingValue = getIn(theme, drawerPadding); + return makeSpace(negativePaddingValue); +}; + +const SideNavFooter = ({ children }: SideNavFooterProps): React.ReactElement => { + const { theme } = useTheme(); + return ( + + {children} + + ); +}; + +export { SideNavFooter }; diff --git a/packages/blade/src/components/SideNav/SideNavItems/SideNavItem.native.tsx b/packages/blade/src/components/SideNav/SideNavItems/SideNavItem.native.tsx new file mode 100644 index 00000000000..af1874df1f7 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavItems/SideNavItem.native.tsx @@ -0,0 +1,14 @@ +import type { SideNavItemProps } from '../types'; +import { Text } from '~components/Typography'; +import { throwBladeError } from '~utils/logger'; + +const SideNavItem = (_props: SideNavItemProps): React.ReactElement => { + throwBladeError({ + message: 'SideNavItem is not yet implemented for native', + moduleName: 'SideNavItem', + }); + + return SideNavItem Component is not available for Native mobile apps.; +}; + +export { SideNavItem }; diff --git a/packages/blade/src/components/SideNav/SideNavItems/SideNavItem.web.tsx b/packages/blade/src/components/SideNav/SideNavItems/SideNavItem.web.tsx new file mode 100644 index 00000000000..6023e54172e --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavItems/SideNavItem.web.tsx @@ -0,0 +1,57 @@ +import styled from 'styled-components'; +import { classes, getNavItemTransition, NAV_ITEM_HEIGHT } from '../tokens'; +import type { SideNavItemProps } from '../types'; +import { TooltipifyNavItem } from './TooltipifyNavItem'; +import { Box } from '~components/Box'; +import BaseBox from '~components/Box/BaseBox'; +import { Text } from '~components/Typography'; +import { makeSize } from '~utils'; + +const SideNavItemContainer = styled(BaseBox)((props) => { + return { + transition: getNavItemTransition(props.theme), + }; +}); + +const SideNavItem = ({ + leading, + trailing, + title, + backgroundColor, + tooltip, + as = 'div', +}: SideNavItemProps): React.ReactElement => { + return ( + + + + {leading} + + + {title} + + + + {trailing} + + + ); +}; + +export { SideNavItem }; diff --git a/packages/blade/src/components/SideNav/SideNavItems/SideNavLink.native.tsx b/packages/blade/src/components/SideNav/SideNavItems/SideNavLink.native.tsx new file mode 100644 index 00000000000..d89759bef11 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavItems/SideNavLink.native.tsx @@ -0,0 +1,14 @@ +import type { SideNavLinkProps } from '../types'; +import { Text } from '~components/Typography'; +import { throwBladeError } from '~utils/logger'; + +const SideNavLink = (_props: SideNavLinkProps): React.ReactElement => { + throwBladeError({ + message: 'SideNavLink is not yet implemented for native', + moduleName: 'SideNavLink', + }); + + return SideNavLink Component is not available for Native mobile apps.; +}; + +export { SideNavLink }; diff --git a/packages/blade/src/components/SideNav/SideNavItems/SideNavLink.web.tsx b/packages/blade/src/components/SideNav/SideNavItems/SideNavLink.web.tsx new file mode 100644 index 00000000000..308988baab2 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavItems/SideNavLink.web.tsx @@ -0,0 +1,323 @@ +import React from 'react'; +import styled from 'styled-components'; +import { FloatingFocusManager, FloatingPortal, useFloating } from '@floating-ui/react'; +import { NavLinkContext, useNavLink, useSideNav } from '../SideNavContext'; +import type { SideNavLinkProps } from '../types'; +import { classes, getNavItemTransition, NAV_ITEM_HEIGHT } from '../tokens'; +import { TooltipifyNavItem } from './TooltipifyNavItem'; +import { Box } from '~components/Box'; +import { makeBorderSize, makeSize, makeSpace } from '~utils'; +import { BaseText } from '~components/Typography/BaseText'; +import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon } from '~components/Icons'; +import BaseBox from '~components/Box/BaseBox'; +import { useCollapsible } from '~components/Collapsible/CollapsibleContext'; +import { Collapsible, CollapsibleBody } from '~components/Collapsible'; +import { makeAccessible } from '~utils/makeAccessible'; +import { useFirstRender } from '~utils/useFirstRender'; +import { getFocusRingStyles } from '~utils/getFocusRingStyles'; +import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; +import { throwBladeError } from '~utils/logger'; + +const { SHOW_ON_LINK_HOVER, HIDE_WHEN_COLLAPSED, STYLED_NAV_LINK } = classes; + +const StyledNavLinkContainer = styled(BaseBox)((props) => { + return { + width: '100%', + [`.${SHOW_ON_LINK_HOVER}`]: { + opacity: 0, + '&:focus-within, &:focus-visible': { + opacity: 1, + }, + }, + '&:hover': { + [`.${SHOW_ON_LINK_HOVER}`]: { + opacity: 1, + }, + + [`.${STYLED_NAV_LINK}`]: { + color: props.theme.colors.interactive.text.gray.normal, + backgroundColor: props.theme.colors.interactive.background.gray.default, + }, + }, + [`.${STYLED_NAV_LINK}`]: { + position: 'relative', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + height: makeSize(NAV_ITEM_HEIGHT), + width: '100%', + textDecoration: 'none', + overflow: 'hidden', + flexWrap: 'nowrap', + cursor: 'pointer', + padding: `${makeSpace(props.theme.spacing[0])} ${makeSpace(props.theme.spacing[4])}`, + margin: `${makeSpace(props.theme.spacing[1])} ${makeSpace(props.theme.spacing[0])}`, + color: props.theme.colors.interactive.text.gray.subtle, + borderRadius: props.theme.border.radius.medium, + borderWidth: makeBorderSize(props.theme.border.width.none), + backgroundColor: props.theme.colors.transparent, + transition: getNavItemTransition(props.theme), + '&[aria-current]': { + color: props.theme.colors.interactive.text.primary.subtle, + backgroundColor: props.theme.colors.interactive.background.primary.faded, + }, + '&[aria-current]:hover': { + color: props.theme.colors.interactive.text.primary.normal, + backgroundColor: props.theme.colors.interactive.background.primary.fadedHighlighted, + }, + '&:focus-visible': { + ...getFocusRingStyles({ theme: props.theme }), + }, + }, + }; +}); + +const NavLinkIconTitle = ({ + icon: Icon, + title, + titleSuffix, + isL1Item, +}: Pick & { + isL1Item: boolean; +}): React.ReactElement => { + return ( + + {Icon ? ( + + + + ) : null} + + {title} + + {titleSuffix ? ( + + {titleSuffix} + + ) : null} + + ); +}; + +const L3Trigger = ({ + title, + icon, + as, + href, + target, + titleSuffix, + tooltip, +}: Pick< + SideNavLinkProps, + 'title' | 'icon' | 'as' | 'href' | 'titleSuffix' | 'tooltip' | 'target' +>): React.ReactElement => { + const { onExpandChange, isExpanded, collapsibleBodyId } = useCollapsible(); + + const toggleCollapse = (): void => onExpandChange(!isExpanded); + const iconProps = { + size: 'medium', + color: 'currentColor', + } as const; + + return ( + + + + + + {isExpanded ? : } + + + + + ); +}; + +/** + * This is the curved line that appears when you select L3 item + */ +const CurvedVerticalLine = styled(BaseBox)((props) => { + const { colors, border, spacing } = props.theme; + return { + borderWidth: makeBorderSize(props.theme.border.width.thin), + borderColor: `${colors.transparent} ${colors.transparent} ${colors.surface.border.primary.muted} ${colors.surface.border.primary.muted}`, + borderStyle: 'solid', + borderRadius: `${makeBorderSize(border.radius.none)} ${makeBorderSize( + border.radius.none, + )} ${makeBorderSize(border.radius.none)} ${makeBorderSize(border.radius.medium)}`, + // We set veritical line infinitely tall (full size of screen) and then hide the overflowing part from top + height: '100vh', + position: 'absolute', + // We want the active line to be positioned in the middle of item's height thus divide by 2 + top: `calc(-100vh + ${makeSize(NAV_ITEM_HEIGHT / 2)})`, + width: makeSpace(spacing[3]), + left: makeSpace(-spacing[3]), + }; +}); + +const SideNavLink = ({ + title, + href, + children, + titleSuffix, + trailing, + isActive, + icon, + tooltip, + as, + target, +}: SideNavLinkProps): React.ReactElement => { + const { + l2PortalContainerRef, + onLinkActiveChange, + closeMobileNav, + isL1Collapsed, + setIsL1Collapsed, + } = useSideNav(); + const { level: _prevLevel } = useNavLink(); + const prevLevel = _prevLevel ?? 0; + const currentLevel = prevLevel + 1; + const isL2Trigger = Boolean(children) && currentLevel === 1; + const isL3Trigger = Boolean(children) && currentLevel === 2; + + if (__DEV__) { + if (Boolean(children) && currentLevel >= 3) { + throwBladeError({ + message: + 'SideNav only supports nesting upto L3 but L4 nesting was found. Check the nesting of your SideNavLevel items', + moduleName: 'SideNavLink', + }); + } + } + + const isFirstRender = useFirstRender(); + + const { refs, context } = useFloating({ + open: isActive, + }); + + useIsomorphicLayoutEffect(() => { + onLinkActiveChange?.({ + level: currentLevel, + title, + isActive: Boolean(isActive), + isL2Trigger, + isFirstRender, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive]); + + return ( + + {isL3Trigger ? ( + + + + {children} + + + ) : ( + <> + + + { + // Close the mobile nav when item is clicked and its not trigger for next menu + if (!isL2Trigger) { + closeMobileNav?.(); + } + + if (isActive) { + onLinkActiveChange?.({ + level: currentLevel, + title, + isActive: Boolean(isActive), + isL2Trigger, + isFirstRender: false, + }); + } + }} + onFocus={(e: { target: HTMLDivElement }) => { + // FloatinFocusManager by default focusses on last clicked element when you move to different tab and come back to the original tab + // Which can make L1 to expand when tabs / windows are changed + // Adding focus-visible check ensures this behaviour of closing menus is only applicable when there is visible focus ring on it (while tabbing) + const hasFocusRing = e.target?.matches(':focus-visible'); + if (isL1Collapsed && currentLevel === 1 && hasFocusRing) { + setIsL1Collapsed?.(false); + } + }} + aria-current={isActive ? 'page' : undefined} + data-level={currentLevel} + data-l2trigger={isL2Trigger} + > + + {isL2Trigger ? ( + + + + ) : null} + + + {trailing && !isL2Trigger ? ( + + {trailing} + + ) : null} + {currentLevel === 3 && isActive ? : null} + + + {children ? ( + + {isActive && isL1Collapsed ? ( + + {children} + + ) : null} + + ) : null} + + )} + + ); +}; + +export { SideNavLink }; diff --git a/packages/blade/src/components/SideNav/SideNavItems/TooltipifyNavItem.tsx b/packages/blade/src/components/SideNav/SideNavItems/TooltipifyNavItem.tsx new file mode 100644 index 00000000000..d9b7e30cb62 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavItems/TooltipifyNavItem.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import type { SideNavLinkProps } from '../types'; +import { Tooltip } from '~components/Tooltip'; + +const TooltipifyNavItem = ({ + children, + tooltip, +}: { + children: React.ReactElement; + tooltip: SideNavLinkProps['tooltip']; +}): React.ReactElement => { + if (!tooltip) { + return children; + } + + return ( + + {children} + + ); +}; + +export { TooltipifyNavItem }; diff --git a/packages/blade/src/components/SideNav/SideNavLevel.native.tsx b/packages/blade/src/components/SideNav/SideNavLevel.native.tsx new file mode 100644 index 00000000000..7b83418323d --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavLevel.native.tsx @@ -0,0 +1,14 @@ +import type { SideNavLevelProps } from './types'; +import { Text } from '~components/Typography'; +import { throwBladeError } from '~utils/logger'; + +const SideNavLevel = (_props: SideNavLevelProps): React.ReactElement => { + throwBladeError({ + message: 'SideNavLevel is not yet implemented for native', + moduleName: 'SideNavLevel', + }); + + return SideNavLevel Component is not available for Native mobile apps.; +}; + +export { SideNavLevel }; diff --git a/packages/blade/src/components/SideNav/SideNavLevel.web.tsx b/packages/blade/src/components/SideNav/SideNavLevel.web.tsx new file mode 100644 index 00000000000..9bb612f40b8 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavLevel.web.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { useNavLink } from './SideNavContext'; +import { COLLAPSED_L1_WIDTH } from './tokens'; +import type { SideNavLevelProps } from './types'; +import BaseBox from '~components/Box/BaseBox'; +import { Text } from '~components/Typography'; +import { makeSize } from '~utils'; + +const SideNavLevel = ({ children }: SideNavLevelProps): React.ReactElement => { + const { level: _prevLevel, title: headingTitle } = useNavLink(); + const prevLevel = _prevLevel ?? 0; + const currentLevel = prevLevel + 1; + return ( + { + e.stopPropagation(); + }} + // Although we don't use `onMouseOut` on SideNav, keeping it here in case we start using it in future as stopping propagation in child + // is expected behaviour for us. Checkout https://react.dev/reference/react-dom/createPortal#caveats + onMouseOut={(e) => { + e.stopPropagation(); + }} + > + {currentLevel === 2 && headingTitle ? ( + + + {headingTitle} + + + ) : null} + {children} + + ); +}; + +export { SideNavLevel }; diff --git a/packages/blade/src/components/SideNav/SideNavSection.native.tsx b/packages/blade/src/components/SideNav/SideNavSection.native.tsx new file mode 100644 index 00000000000..f09c4652e33 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavSection.native.tsx @@ -0,0 +1,14 @@ +import type { SideNavSectionProps } from './types'; +import { Text } from '~components/Typography'; +import { throwBladeError } from '~utils/logger'; + +const SideNavSection = (_props: SideNavSectionProps): React.ReactElement => { + throwBladeError({ + message: 'SideNavSection is not yet implemented for native', + moduleName: 'SideNavSection', + }); + + return SideNavSection Component is not available for Native mobile apps.; +}; + +export { SideNavSection }; diff --git a/packages/blade/src/components/SideNav/SideNavSection.web.tsx b/packages/blade/src/components/SideNav/SideNavSection.web.tsx new file mode 100644 index 00000000000..58c1579e941 --- /dev/null +++ b/packages/blade/src/components/SideNav/SideNavSection.web.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import styled from 'styled-components'; +import type { SideNavSectionProps } from './types'; +import { classes } from './tokens'; +import { Box } from '~components/Box'; +import { Text } from '~components/Typography'; +import { Collapsible, CollapsibleBody } from '~components/Collapsible'; +import BaseBox from '~components/Box/BaseBox'; +import { useCollapsible } from '~components/Collapsible/CollapsibleContext'; +import { ChevronDownIcon, ChevronUpIcon } from '~components/Icons'; +import { makeBorderSize } from '~utils'; +import { BaseLink } from '~components/Link/BaseLink'; + +const SideNavTitleDivider = styled(BaseBox)(({ theme }) => { + return { + height: makeBorderSize(theme.border.width.thicker), + width: '100%', + background: `linear-gradient(90deg, ${theme.colors.transparent} 0%, ${theme.colors.surface.border.gray.muted} 50%, ${theme.colors.transparent} 100%)`, + }; +}); + +const FullWidthLink = styled(BaseLink)(() => { + return { + width: '100%', + }; +}); + +const ShowMoreLink = ({ + collapsedItemsCount, +}: { + collapsedItemsCount: number; +}): React.ReactElement => { + const { isExpanded, onExpandChange } = useCollapsible(); + const toggleCollapse = (): void => onExpandChange(!isExpanded); + const linkProps = { + size: 'small', + color: 'neutral', + variant: 'button', + onClick: toggleCollapse, + marginY: 'spacing.2', + } as const; + + return ( + <> + + + {isExpanded ? `` : `+${collapsedItemsCount}`} + + + + + {isExpanded ? 'Show Less' : `+${collapsedItemsCount} More`} + + + + ); +}; + +const StyledSectionTitleContainer = styled(BaseBox)((_props) => { + return { + [`.${classes.COLLAPSED}:not(.${classes.TRANSITIONING}) & p`]: { + // We only make it opacity 0 to maintain the height of the title in collapsed state + opacity: 0, + }, + }; +}); + +const SideNavSection = ({ + children, + title, + defaultIsExpanded, + maxVisibleItems, + onExpandChange, +}: SideNavSectionProps): React.ReactElement => { + const totalItemsCount = React.Children.count(children); + const collapsedItemsCount = maxVisibleItems ? totalItemsCount - maxVisibleItems : undefined; + + return ( + + {title ? ( + + + {title?.toUpperCase()} + + + + + + ) : null} + + {maxVisibleItems ? children.slice(0, maxVisibleItems) : children} + {maxVisibleItems ? ( + + + + {children.slice(maxVisibleItems)} + + + ) : null} + + ); +}; + +export { SideNavSection }; diff --git a/packages/blade/src/components/SideNav/__tests__/SideNav.ssr.test.tsx b/packages/blade/src/components/SideNav/__tests__/SideNav.ssr.test.tsx new file mode 100644 index 00000000000..f819e00513d --- /dev/null +++ b/packages/blade/src/components/SideNav/__tests__/SideNav.ssr.test.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { SideNavExample } from './SideNavExample'; +import renderWithSSR from '~utils/testing/renderWithSSR.web'; + +describe('', () => { + it('should render SideNav ssr', () => { + const { container } = renderWithSSR(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/blade/src/components/SideNav/__tests__/SideNav.test.mobile.stories.tsx b/packages/blade/src/components/SideNav/__tests__/SideNav.test.mobile.stories.tsx new file mode 100644 index 00000000000..70c9b134e15 --- /dev/null +++ b/packages/blade/src/components/SideNav/__tests__/SideNav.test.mobile.stories.tsx @@ -0,0 +1,89 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import React from 'react'; +import type { StoryFn } from '@storybook/react'; +import { within, userEvent, waitFor } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +import type { SideNav } from '../index'; +import { SideNavExample } from './SideNavExample'; +import { Box } from '~components/Box'; +import { Button } from '~components/Button'; + +// Keeping the mobile test in different file because storybook's test runner is not able to change the viewport before running test +// With different file, we ignore running this file in test runner + +export const Mobile: StoryFn = (props): React.ReactElement => { + const [isMobileOpen, setIsMobileOpen] = React.useState(false); + + return ( + + setIsMobileOpen(false)} {...props} /> + + + + + ); +}; + +Mobile.parameters = { + viewport: { + defaultViewport: 'iPhone6', + }, +}; + +Mobile.play = async () => { + const { + queryByText, + getByText, + getAllByText, + queryByRole, + getByLabelText, + getAllByLabelText, + } = within(document.body); + + await expect(queryByRole('heading', { name: 'Main Menu' })).not.toBeInTheDocument(); + + // L1 Open + await userEvent.click(getByText('Open Mobile Nav')); + await waitFor(() => expect(getByText('Main Menu')).toBeVisible()); + await expect(getByText('Home')).toBeVisible(); + await expect(getByText('L2 Trigger')).toBeVisible(); + await expect(queryByText('L2 Item')).not.toBeInTheDocument(); + + // L2 Open + await userEvent.click(getByText('L2 Trigger')); + await waitFor(() => expect(getByText('L2 Item')).toBeVisible()); + + // L3 Open + await userEvent.click(getByText('L3 Trigger')); + await waitFor(() => expect(getByText('L3 Item')).toBeVisible()); + + // back button click + await userEvent.click(getByLabelText('Back')); + await waitFor(() => expect(queryByText('L2 Item')).not.toBeInTheDocument()); + + // +2 more test + await userEvent.click(getByText('+2 More')); + await expect(getByText('Pages')).toBeVisible(); + await userEvent.click(getByText('Show Less')); + await waitFor(() => expect(getByText('Pages')).not.toBeVisible()); + await expect(getByText('Test Mode')).toBeVisible(); + + // Open L2 and close all menus + await userEvent.click(getAllByText('L2 Trigger')[0]); + await waitFor(() => expect(getByText('L2 Item')).toBeVisible()); + await userEvent.click(getAllByLabelText('Close')[1]); + await expect(queryByRole('heading', { name: 'Main Menu' })).not.toBeInTheDocument(); +}; + +export default { + title: 'Components/Interaction Tests/SideNav/Mobile', + component: SideNavExample, + parameters: { + controls: { + disable: true, + }, + a11y: { disable: true }, + essentials: { disable: true }, + actions: { disable: true }, + }, +}; diff --git a/packages/blade/src/components/SideNav/__tests__/SideNav.test.stories.tsx b/packages/blade/src/components/SideNav/__tests__/SideNav.test.stories.tsx new file mode 100644 index 00000000000..d19f6bb1f9b --- /dev/null +++ b/packages/blade/src/components/SideNav/__tests__/SideNav.test.stories.tsx @@ -0,0 +1,131 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import React from 'react'; +import type { StoryFn } from '@storybook/react'; +import { within, userEvent } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +import type { SideNav } from '../index'; +import { SKIP_NAV_ID } from '../tokens'; +import { SideNavExample } from './SideNavExample'; +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +export const MenuNavigation: StoryFn = (props): React.ReactElement => { + return ; +}; + +MenuNavigation.play = async ({ canvasElement }) => { + const { getByRole, queryByRole } = within(canvasElement); + await expect(getByRole('navigation')).toBeVisible(); + + // L1 Selections + await expect(getByRole('link', { name: 'Home' })).toHaveAttribute('aria-current', 'page'); + await userEvent.click(getByRole('link', { name: 'Gateway' })); + await expect(getByRole('link', { name: 'Gateway' })).toHaveAttribute('aria-current', 'page'); + await expect(getByRole('link', { name: 'Home' })).not.toHaveAttribute('aria-current', 'page'); + await expect(queryByRole('link', { name: 'L2 Item 2' })).not.toBeInTheDocument(); + + // L2 Selections + await userEvent.click(getByRole('link', { name: 'L2 Trigger' })); + await expect(getByRole('link', { name: 'L2 Item' })).toHaveAttribute('aria-current', 'page'); + await userEvent.click(getByRole('link', { name: 'L2 Item 2' })); + await expect(getByRole('link', { name: 'L2 Item 2' })).toHaveAttribute('aria-current', 'page'); + + // L1 Hover + await sleep(500); + await expect(queryByRole('link', { name: 'Home' })).not.toBeInTheDocument(); + const l1Element = document.querySelector('#blade-sidenav-l1')!; + await userEvent.hover(l1Element); + await sleep(500); + await expect(getByRole('link', { name: 'Home' })).toBeVisible(); + await userEvent.unhover(l1Element); + + // L3 Expand and Select + await expect(getByRole('button', { name: 'L3 Trigger' })).toHaveAttribute( + 'aria-expanded', + 'false', + ); + await userEvent.click(getByRole('button', { name: 'L3 Trigger' })); + await expect(getByRole('button', { name: 'L3 Trigger' })).toHaveAttribute( + 'aria-expanded', + 'true', + ); + + await userEvent.click(getByRole('link', { name: 'L3 Item 2' })); + await expect(getByRole('link', { name: 'L3 Item 2' })).toHaveAttribute('aria-current', 'page'); + + // Back to L1 Selection + await sleep(500); + await userEvent.hover(l1Element); + await sleep(500); + await expect(getByRole('link', { name: 'Home' })).toBeVisible(); + await userEvent.click(getByRole('link', { name: 'Links' })); + await expect(getByRole('link', { name: 'Links' })).toHaveAttribute('aria-current', 'page'); +}; + +export const Accessibility: StoryFn = (props): React.ReactElement => { + return ; +}; + +Accessibility.play = async ({ canvasElement }) => { + const { getByRole, queryByRole } = within(canvasElement); + await expect(getByRole('navigation')).toBeVisible(); + + await userEvent.keyboard('{TAB}'); + await expect(getByRole('link', { name: 'Skip to content' })).toHaveFocus(); + await expect(getByRole('link', { name: 'Skip to content' })).toHaveAttribute( + 'href', + `#${SKIP_NAV_ID}`, + ); + await userEvent.keyboard('{TAB}'); + await expect(getByRole('link', { name: 'Home' })).toHaveFocus(); + await userEvent.keyboard('{TAB}'); + await expect(getByRole('link', { name: 'L2 Trigger' })).toHaveFocus(); + await userEvent.keyboard('{Enter}'); + await userEvent.keyboard('{TAB}'); + await expect(getByRole('link', { name: 'L2 Item' })).toHaveFocus(); + await sleep(500); + await expect(queryByRole('link', { name: 'Home' })).not.toBeInTheDocument(); + + await userEvent.keyboard('{Shift>}{Tab}'); + await expect(getByRole('link', { name: 'L2 Trigger' })).toHaveFocus(); + await expect(getByRole('link', { name: 'Home' })).toBeVisible(); + await userEvent.keyboard('{Enter}'); + await userEvent.keyboard('{TAB}'); + await userEvent.keyboard('{TAB}'); + await userEvent.keyboard('{TAB}'); + await expect(getByRole('button', { name: 'L3 Trigger' })).toHaveFocus(); + await userEvent.keyboard('{Enter}'); + await userEvent.keyboard('{TAB}'); + await userEvent.keyboard('{Enter}'); + await expect(getByRole('link', { name: 'L3 Item' })).toHaveAttribute('aria-current', 'page'); + await userEvent.keyboard('{Shift>}{Tab}'); + await userEvent.keyboard('{Shift>}{Tab}'); + await userEvent.keyboard('{Shift>}{Tab}'); + await userEvent.keyboard('{Shift>}{Tab}'); + await expect(getByRole('link', { name: 'L2 Trigger' })).toHaveFocus(); + await userEvent.keyboard('{TAB}'); + await userEvent.keyboard('{TAB}'); + await userEvent.keyboard('{TAB}'); + await expect(getByRole('button', { name: '+2 More' })).toHaveFocus(); + await userEvent.keyboard('{Enter}'); + await userEvent.keyboard('{TAB}'); + await expect(getByRole('link', { name: 'Pages' })).toHaveFocus(); + await userEvent.keyboard('{Shift>}{Tab}'); + await expect(getByRole('button', { name: 'Show Less' })).toHaveFocus(); + await userEvent.keyboard('{TAB}'); + await userEvent.keyboard('{TAB}'); + await userEvent.keyboard('{TAB}'); + await expect(getByRole('switch')).toHaveFocus(); +}; + +export default { + title: 'Components/Interaction Tests/SideNav', + component: SideNavExample, + parameters: { + controls: { + disable: true, + }, + a11y: { disable: true }, + essentials: { disable: true }, + actions: { disable: true }, + }, +}; diff --git a/packages/blade/src/components/SideNav/__tests__/SideNav.web.test.tsx b/packages/blade/src/components/SideNav/__tests__/SideNav.web.test.tsx new file mode 100644 index 00000000000..98d1dec0507 --- /dev/null +++ b/packages/blade/src/components/SideNav/__tests__/SideNav.web.test.tsx @@ -0,0 +1,43 @@ +import { SideNavExample, SideNavL4NestingErrorExample } from './SideNavExample'; +import renderWithTheme from '~utils/testing/renderWithTheme'; +import assertAccessible from '~utils/testing/assertAccessible'; + +// Test cases are limited here since SideNav gets flaky in unit tests with all the complexity +// Major functionality tests are added in Interaction tests + +describe('SideNav', () => { + test('should render', () => { + const { container } = renderWithTheme(); + expect(container).toMatchSnapshot(); + }); + + test('should supports styled-props', () => { + const { getByRole } = renderWithTheme( + // Have to pass display="block" otherwise component ignores media query style and selects base display value which is none + , + ); + expect(getByRole('navigation')).toHaveStyle('z-index: 1234; position: absolute'); + }); + + test('should throw error on more than 3 level nesting', () => { + const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(); + expect(() => renderWithTheme()).toThrow( + '[Blade: SideNavLink]: SideNav only supports nesting upto L3 but L4 nesting was found. Check the nesting of your SideNavLevel items', + ); + mockConsoleError.mockRestore(); + }); + + test('should keep L3 Item selected based on URL', () => { + const { getByRole } = renderWithTheme( + , + ); + + expect(getByRole('link', { name: 'L3 Item' })).toHaveAttribute('aria-current', 'page'); + }); + + test('should pass general a11y', async () => { + const { container } = renderWithTheme(); + + await assertAccessible(container); + }); +}); diff --git a/packages/blade/src/components/SideNav/__tests__/SideNavExample.tsx b/packages/blade/src/components/SideNav/__tests__/SideNavExample.tsx new file mode 100644 index 00000000000..70179fa7f3c --- /dev/null +++ b/packages/blade/src/components/SideNav/__tests__/SideNavExample.tsx @@ -0,0 +1,133 @@ +import { Link, MemoryRouter, useLocation, matchPath } from 'react-router-dom'; +import { + SideNav, + SideNavBody, + SideNavFooter, + SideNavSection, + SideNavLink, + SideNavItem, + SideNavLevel, +} from '../index'; +import type { SideNavLinkProps, SideNavProps } from '../index'; +import { Indicator } from '~components/Indicator'; +import { Switch } from '~components/Switch'; +import { + HomeIcon, + LayoutIcon, + PaymentButtonIcon, + PaymentGatewayIcon, + PaymentLinkIcon, + PaymentPagesIcon, +} from '~components/Icons'; + +const isItemActive = ( + location: { pathname: string }, + { href, activeOnLinks }: { href?: string; activeOnLinks?: string[] }, +): boolean => { + const isCurrentPathActive = Boolean( + matchPath(location.pathname, { + path: href, + exact: false, + }), + ); + + const isSubItemActive = Boolean( + activeOnLinks?.find((href) => matchPath(location.pathname, { path: href, exact: false })), + ); + + return isCurrentPathActive || isSubItemActive; +}; + +const NavLink = ( + props: Omit & { + activeOnLinks?: string[]; + }, +): React.ReactElement => { + const location = useLocation(); + + return ( + + ); +}; + +const SideNavExample = ({ + initialEntries = ['/home'], + ...args +}: Omit & { initialEntries?: string[] }): React.ReactElement => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + } + title="Test Mode" + trailing={} + /> + + + + ); +}; + +const SideNavL4NestingErrorExample = (): React.ReactElement => { + return ( + + + + + + + + + + + + + + + + + + + + ); +}; + +export { SideNavExample, SideNavL4NestingErrorExample }; diff --git a/packages/blade/src/components/SideNav/__tests__/__snapshots__/SideNav.ssr.test.tsx.snap b/packages/blade/src/components/SideNav/__tests__/__snapshots__/SideNav.ssr.test.tsx.snap new file mode 100644 index 00000000000..742d467df77 --- /dev/null +++ b/packages/blade/src/components/SideNav/__tests__/__snapshots__/SideNav.ssr.test.tsx.snap @@ -0,0 +1,1472 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render SideNav ssr 1`] = `""`; + +exports[` should render SideNav ssr 2`] = ` +.c0.c0.c0.c0.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + position: fixed; + height: 100%; + width: 264px; + top: 0px; + left: 0px; + background-color: hsla(210,40%,98%,1); +} + +.c1.c1.c1.c1.c1 { + display: block; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + width: 100%; +} + +.c2.c2.c2.c2.c2 { + position: absolute; + height: 100%; + width: 100%; + top: 0px; + left: 0px; + background-color: hsla(210,40%,98%,1); + border-right-width: 1px; + border-right-color: hsla(211,20%,52%,0.18); + border-right-style: solid; +} + +.c3.c3.c3.c3.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + overflow: hidden; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + position: absolute; + height: 100%; + top: 0px; + left: 0px; + background-color: hsla(210,40%,98%,1); + border-right-width: 1px; + border-right-color: hsla(211,20%,52%,0.18); + border-right-style: solid; +} + +.c7.c7.c7.c7.c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c9.c9.c9.c9.c9 { + overflow-x: hidden; + overflow-y: auto; +} + +.c10.c10.c10.c10.c10 { + position: relative; +} + +.c12.c12.c12.c12.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 8px; +} + +.c13.c13.c13.c13.c13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c15.c15.c15.c15.c15 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c16.c16.c16.c16.c16 { + padding-top: 8px; + padding-bottom: 8px; +} + +.c17.c17.c17.c17.c17 { + position: relative; + padding: 4px 12px; +} + +.c20.c20.c20.c20.c20 { + position: absolute; + margin: auto; + top: 50%; + right: 0px; + left: 0px; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} + +.c22.c22.c22.c22.c22 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column-reverse; + -ms-flex-direction: column-reverse; + flex-direction: column-reverse; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + min-width: 0px; + max-width: none; +} + +.c23.c23.c23.c23.c23 { + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + width: 100%; +} + +.c27.c27.c27.c27.c27 { + width: 100%; +} + +.c28.c28.c28.c28.c28 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding-left: 4px; +} + +.c30.c30.c30.c30.c30 { + position: relative; + padding: 12px; + width: calc(100% + 20px + 20px); + right: -20px; + bottom: -20px; + left: -20px; + background-color: hsla(210,40%,98%,1); + border-top-width: 1px; + border-top-color: hsla(211,20%,52%,0.18); + border-top-style: solid; + box-shadow: 0px 2px 16px 0px hsla(217,56%,17%,0.10); +} + +.c31.c31.c31.c31.c31 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + padding-right: 8px; + padding-left: 8px; + height: 40px; + border-radius: 4px; +} + +.c33.c33.c33.c33.c33 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + gap: 8px; +} + +.c34.c34.c34.c34.c34 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; +} + +.c35.c35.c35.c35.c35 { + margin-left: 4px; +} + +.c38.c38.c38.c38.c38 { + display: inline-block; +} + +.c8.c8.c8.c8.c8 { + color: hsla(227,71%,51%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + text-align: center; + margin: 0; + padding: 0; +} + +.c14.c14.c14.c14.c14 { + color: currentColor; + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow-wrap: break-word; +} + +.c19.c19.c19.c19.c19 { + color: hsla(211,22%,56%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.625rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 0.875rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow-wrap: break-word; +} + +.c26.c26.c26.c26.c26 { + color: hsla(211,33%,21%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.75rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.125rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + text-align: center; + margin: 0; + padding: 0; +} + +.c36.c36.c36.c36.c36 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + text-align: left; + margin: 0; + padding: 0; +} + +.c37.c37.c37.c37.c37 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow-wrap: break-word; +} + +.c5.c5.c5.c5.c5 { + padding: 0; + background-color: transparent; + outline: none; + -webkit-text-decoration: none; + text-decoration: none; + border: none; + cursor: pointer; + display: inline-block; + border-radius: 2px; + -webkit-transition-property: box-shadow; + transition-property: box-shadow; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c5.c5.c5.c5.c5 .content-container { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + border-radius: 2px; +} + +.c5.c5.c5.c5.c5:focus-visible .content-container { + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.09); +} + +.c5.c5.c5.c5.c5 * { + -webkit-transition-property: color,fill; + transition-property: color,fill; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c24.c24.c24.c24.c24 { + padding: 0; + background-color: transparent; + outline: none; + -webkit-text-decoration: none; + text-decoration: none; + border: none; + cursor: pointer; + display: inline-block; + border-radius: 2px; + -webkit-transition-property: box-shadow; + transition-property: box-shadow; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; + margin-bottom: 4px; + margin-top: 4px; + margin-right: 12px; + margin-left: 12px; +} + +.c24.c24.c24.c24.c24 .content-container { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + border-radius: 2px; +} + +.c24.c24.c24.c24.c24:focus-visible .content-container { + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.09); +} + +.c24.c24.c24.c24.c24 * { + -webkit-transition-property: color,fill; + transition-property: color,fill; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c6.c6.c6.c6.c6 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + margin: 0 -1px -1px 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + left: 16px; + white-space: nowrap; + word-wrap: normal; + top: 16px; + background-color: hsla(0,0%,100%,1); + z-index: 1; +} + +.c6.c6.c6.c6.c6:focus { + padding: 4px; + -webkit-clip: auto; + clip: auto; + -webkit-clip-path: unset; + clip-path: unset; + width: auto; + height: auto; +} + +.c4.c4.c4.c4.c4 { + width: 100%; + -webkit-transition: width 400ms cubic-bezier(0,0,0,1); + transition: width 400ms cubic-bezier(0,0,0,1); +} + +.c4.c4.c4.c4.c4 > .l1-item-wrapper { + padding: 8px; +} + +.c4.c4.c4.c4.c4 .show-when-collapsed { + display: none; +} + +.c4.c4.c4.c4.c4.collapsed { + width: 56px; + -webkit-transition: width 250ms cubic-bezier(0.5,0,1,1); + transition: width 250ms cubic-bezier(0.5,0,1,1); +} + +.c4.c4.c4.c4.c4.collapsed > .l1-item-wrapper { + padding: 8px 8px; +} + +.c4.c4.c4.c4.c4.collapsed:not(.transitioning) .hide-when-collapsed { + display: none; +} + +.c4.c4.c4.c4.c4.collapsed:not(.transitioning) .show-when-collapsed { + display: initial; +} + +.c29.c29.c29.c29.c29 { + -webkit-transition-duration: 300ms; + transition-duration: 300ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-property: height,opacity; + transition-property: height,opacity; + opacity: 0.8; + height: 0px; + display: none; + overflow-y: hidden; +} + +.c11.c11.c11.c11.c11 { + width: 100%; +} + +.c11.c11.c11.c11.c11 .show-on-link-hover { + opacity: 0; +} + +.c11.c11.c11.c11.c11 .show-on-link-hover:focus-within, +.c11.c11.c11.c11.c11 .show-on-link-hover:focus-visible { + opacity: 1; +} + +.c11.c11.c11.c11.c11:hover .show-on-link-hover { + opacity: 1; +} + +.c11.c11.c11.c11.c11:hover .styled-nav-link { + color: hsla(212,39%,16%,1); + background-color: hsla(211,20%,52%,0.12); +} + +.c11.c11.c11.c11.c11 .styled-nav-link { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + height: 40px; + width: 100%; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + cursor: pointer; + padding: 0px 12px; + margin: 2px 0px; + color: hsla(211,26%,34%,1); + border-radius: 4px; + border-width: 0px; + background-color: hsla(0,0%,100%,0); + -webkit-transition: background-color 70ms cubic-bezier(0.3,0,0.2,1); + transition: background-color 70ms cubic-bezier(0.3,0,0.2,1); +} + +.c11.c11.c11.c11.c11 .styled-nav-link[aria-current] { + color: hsla(227,100%,59%,1); + background-color: hsla(227,100%,59%,0.09); +} + +.c11.c11.c11.c11.c11 .styled-nav-link[aria-current]:hover { + color: hsla(227,71%,51%,1); + background-color: hsla(227,100%,59%,0.18); +} + +.c11.c11.c11.c11.c11 .styled-nav-link:focus-visible { + outline: 4px solid hsla(227,100%,59%,0.18); + outline-offset: 1px; + -webkit-transition-property: outline-width; + transition-property: outline-width; + -webkit-transition-duration: 70ms; + transition-duration: 70ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c21.c21.c21.c21.c21 { + height: 2px; + width: 100%; + background: linear-gradient(90deg,hsla(0,0%,100%,0) 0%,hsla(211,20%,52%,0.18) 50%,hsla(0,0%,100%,0) 100%); +} + +.c25.c25.c25.c25.c25 { + width: 100%; +} + +.collapsed:not(.transitioning) .c18.c18.c18.c18.c18 p { + opacity: 0; +} + +.c32.c32.c32.c32.c32 { + -webkit-transition: background-color 70ms cubic-bezier(0.3,0,0.2,1); + transition: background-color 70ms cubic-bezier(0.3,0,0.2,1); +} + +.c44.c44.c44.c44.c44 { + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-delay: 0ms; + transition-delay: 0ms; + opacity: 0; +} + +.c42.c42.c42.c42.c42 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + width: 16px; + height: 16px; + position: relative; +} + +.c43.c43.c43.c43.c43 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + height: 100%; + position: relative; + will-change: transform,left; + width: 100%; + left: 0%; + -webkit-transform: translateX(0%); + -ms-transform: translateX(0%); + transform: translateX(0%); + -webkit-transition: 150ms; + transition: 150ms; + border-radius: 9999px; + -webkit-animation-duration: 150ms; + animation-duration: 150ms; + background-color: hsla(0,0%,100%,1); +} + +.c41.c41.c41.c41.c41 { + pointer-events: none; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 2px; + padding: 2px; + width: 36px; + height: 20px; + border-radius: 9999px; + background-color: hsla(211,20%,52%,0.12); + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c40.c40.c40.c40.c40 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + margin: 0 -1px -1px 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + left: -10000px; + white-space: nowrap; + word-wrap: normal; +} + +.c40.c40.c40.c40.c40:focus-visible + div { + outline: 4px solid hsla(227,100%,59%,0.18); + outline-offset: 1px; + -webkit-transition-property: outline-width; + transition-property: outline-width; + -webkit-transition-duration: 70ms; + transition-duration: 70ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c40.c40.c40.c40.c40:hover + div { + background-color: hsla(211,20%,52%,0.18); + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c39.c39.c39.c39.c39 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + margin-top: 2px; + margin-bottom: 2px; +} + +@media screen and (min-width:320px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:480px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:768px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:1024px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:1200px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:320px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:480px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:768px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:1024px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:1200px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:320px) { + .c30.c30.c30.c30.c30 { + border-top-style: solid; + } +} + +@media screen and (min-width:480px) { + .c30.c30.c30.c30.c30 { + border-top-style: solid; + } +} + +@media screen and (min-width:768px) { + .c30.c30.c30.c30.c30 { + width: 100%; + right: 0px; + bottom: 0px; + left: 0px; + border-top-style: solid; + } +} + +@media screen and (min-width:1024px) { + .c30.c30.c30.c30.c30 { + border-top-style: solid; + } +} + +@media screen and (min-width:1200px) { + .c30.c30.c30.c30.c30 { + border-top-style: solid; + } +} + +
+ +
+`; diff --git a/packages/blade/src/components/SideNav/__tests__/__snapshots__/SideNav.web.test.tsx.snap b/packages/blade/src/components/SideNav/__tests__/__snapshots__/SideNav.web.test.tsx.snap new file mode 100644 index 00000000000..0832a782727 --- /dev/null +++ b/packages/blade/src/components/SideNav/__tests__/__snapshots__/SideNav.web.test.tsx.snap @@ -0,0 +1,1474 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SideNav should render 1`] = ` +.c0.c0.c0.c0.c0 { + display: none; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + position: fixed; + height: 100%; + width: 264px; + top: 0px; + left: 0px; + background-color: hsla(210,40%,98%,1); +} + +.c1.c1.c1.c1.c1 { + display: block; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + width: 100%; +} + +.c2.c2.c2.c2.c2 { + position: absolute; + height: 100%; + width: 100%; + top: 0px; + left: 0px; + background-color: hsla(210,40%,98%,1); + border-right-width: 1px; + border-right-color: hsla(211,20%,52%,0.18); + border-right-style: solid; +} + +.c3.c3.c3.c3.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + overflow: hidden; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + position: absolute; + height: 100%; + top: 0px; + left: 0px; + background-color: hsla(210,40%,98%,1); + border-right-width: 1px; + border-right-color: hsla(211,20%,52%,0.18); + border-right-style: solid; +} + +.c7.c7.c7.c7.c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c9.c9.c9.c9.c9 { + overflow-x: hidden; + overflow-y: auto; +} + +.c10.c10.c10.c10.c10 { + position: relative; +} + +.c12.c12.c12.c12.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 8px; +} + +.c13.c13.c13.c13.c13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c15.c15.c15.c15.c15 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c16.c16.c16.c16.c16 { + padding-top: 8px; + padding-bottom: 8px; +} + +.c17.c17.c17.c17.c17 { + position: relative; + padding: 4px 12px; +} + +.c20.c20.c20.c20.c20 { + position: absolute; + margin: auto; + top: 50%; + right: 0px; + left: 0px; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} + +.c22.c22.c22.c22.c22 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column-reverse; + -ms-flex-direction: column-reverse; + flex-direction: column-reverse; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + min-width: 0px; + max-width: none; +} + +.c23.c23.c23.c23.c23 { + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + width: 100%; +} + +.c27.c27.c27.c27.c27 { + width: 100%; +} + +.c28.c28.c28.c28.c28 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding-left: 4px; +} + +.c30.c30.c30.c30.c30 { + position: relative; + padding: 12px; + width: calc(100% + 20px + 20px); + right: -20px; + bottom: -20px; + left: -20px; + background-color: hsla(210,40%,98%,1); + border-top-width: 1px; + border-top-color: hsla(211,20%,52%,0.18); + border-top-style: solid; + box-shadow: 0px 2px 16px 0px hsla(217,56%,17%,0.10); +} + +.c31.c31.c31.c31.c31 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + padding-right: 8px; + padding-left: 8px; + height: 40px; + border-radius: 4px; +} + +.c33.c33.c33.c33.c33 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + gap: 8px; +} + +.c34.c34.c34.c34.c34 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; +} + +.c35.c35.c35.c35.c35 { + margin-left: 4px; +} + +.c38.c38.c38.c38.c38 { + display: inline-block; +} + +.c8.c8.c8.c8.c8 { + color: hsla(227,71%,51%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + text-align: center; + margin: 0; + padding: 0; +} + +.c14.c14.c14.c14.c14 { + color: currentColor; + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow-wrap: break-word; +} + +.c19.c19.c19.c19.c19 { + color: hsla(211,22%,56%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.625rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 0.875rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow-wrap: break-word; +} + +.c26.c26.c26.c26.c26 { + color: hsla(211,33%,21%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.75rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.125rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + text-align: center; + margin: 0; + padding: 0; +} + +.c36.c36.c36.c36.c36 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + text-align: left; + margin: 0; + padding: 0; +} + +.c37.c37.c37.c37.c37 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 500; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow-wrap: break-word; +} + +.c5.c5.c5.c5.c5 { + padding: 0; + background-color: transparent; + outline: none; + -webkit-text-decoration: none; + text-decoration: none; + border: none; + cursor: pointer; + display: inline-block; + border-radius: 2px; + -webkit-transition-property: box-shadow; + transition-property: box-shadow; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c5.c5.c5.c5.c5 .content-container { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + border-radius: 2px; +} + +.c5.c5.c5.c5.c5:focus-visible .content-container { + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.09); +} + +.c5.c5.c5.c5.c5 * { + -webkit-transition-property: color,fill; + transition-property: color,fill; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c24.c24.c24.c24.c24 { + padding: 0; + background-color: transparent; + outline: none; + -webkit-text-decoration: none; + text-decoration: none; + border: none; + cursor: pointer; + display: inline-block; + border-radius: 2px; + -webkit-transition-property: box-shadow; + transition-property: box-shadow; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; + margin-bottom: 4px; + margin-top: 4px; + margin-right: 12px; + margin-left: 12px; +} + +.c24.c24.c24.c24.c24 .content-container { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + border-radius: 2px; +} + +.c24.c24.c24.c24.c24:focus-visible .content-container { + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.09); +} + +.c24.c24.c24.c24.c24 * { + -webkit-transition-property: color,fill; + transition-property: color,fill; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c6.c6.c6.c6.c6 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + margin: 0 -1px -1px 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + left: 16px; + white-space: nowrap; + word-wrap: normal; + top: 16px; + background-color: hsla(0,0%,100%,1); + z-index: 1; +} + +.c6.c6.c6.c6.c6:focus { + padding: 4px; + -webkit-clip: auto; + clip: auto; + -webkit-clip-path: unset; + clip-path: unset; + width: auto; + height: auto; +} + +.c4.c4.c4.c4.c4 { + width: 100%; + -webkit-transition: width 400ms cubic-bezier(0,0,0,1); + transition: width 400ms cubic-bezier(0,0,0,1); +} + +.c4.c4.c4.c4.c4 > .l1-item-wrapper { + padding: 8px; +} + +.c4.c4.c4.c4.c4 .show-when-collapsed { + display: none; +} + +.c4.c4.c4.c4.c4.collapsed { + width: 56px; + -webkit-transition: width 250ms cubic-bezier(0.5,0,1,1); + transition: width 250ms cubic-bezier(0.5,0,1,1); +} + +.c4.c4.c4.c4.c4.collapsed > .l1-item-wrapper { + padding: 8px 8px; +} + +.c4.c4.c4.c4.c4.collapsed:not(.transitioning) .hide-when-collapsed { + display: none; +} + +.c4.c4.c4.c4.c4.collapsed:not(.transitioning) .show-when-collapsed { + display: initial; +} + +.c29.c29.c29.c29.c29 { + -webkit-transition-duration: 300ms; + transition-duration: 300ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-property: height,opacity; + transition-property: height,opacity; + opacity: 0.8; + height: 0px; + display: none; + overflow-y: hidden; +} + +.c11.c11.c11.c11.c11 { + width: 100%; +} + +.c11.c11.c11.c11.c11 .show-on-link-hover { + opacity: 0; +} + +.c11.c11.c11.c11.c11 .show-on-link-hover:focus-within, +.c11.c11.c11.c11.c11 .show-on-link-hover:focus-visible { + opacity: 1; +} + +.c11.c11.c11.c11.c11:hover .show-on-link-hover { + opacity: 1; +} + +.c11.c11.c11.c11.c11:hover .styled-nav-link { + color: hsla(212,39%,16%,1); + background-color: hsla(211,20%,52%,0.12); +} + +.c11.c11.c11.c11.c11 .styled-nav-link { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + height: 40px; + width: 100%; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + cursor: pointer; + padding: 0px 12px; + margin: 2px 0px; + color: hsla(211,26%,34%,1); + border-radius: 4px; + border-width: 0px; + background-color: hsla(0,0%,100%,0); + -webkit-transition: background-color 70ms cubic-bezier(0.3,0,0.2,1); + transition: background-color 70ms cubic-bezier(0.3,0,0.2,1); +} + +.c11.c11.c11.c11.c11 .styled-nav-link[aria-current] { + color: hsla(227,100%,59%,1); + background-color: hsla(227,100%,59%,0.09); +} + +.c11.c11.c11.c11.c11 .styled-nav-link[aria-current]:hover { + color: hsla(227,71%,51%,1); + background-color: hsla(227,100%,59%,0.18); +} + +.c11.c11.c11.c11.c11 .styled-nav-link:focus-visible { + outline: 4px solid hsla(227,100%,59%,0.18); + outline-offset: 1px; + -webkit-transition-property: outline-width; + transition-property: outline-width; + -webkit-transition-duration: 70ms; + transition-duration: 70ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c21.c21.c21.c21.c21 { + height: 2px; + width: 100%; + background: linear-gradient(90deg,hsla(0,0%,100%,0) 0%,hsla(211,20%,52%,0.18) 50%,hsla(0,0%,100%,0) 100%); +} + +.c25.c25.c25.c25.c25 { + width: 100%; +} + +.collapsed:not(.transitioning) .c18.c18.c18.c18.c18 p { + opacity: 0; +} + +.c32.c32.c32.c32.c32 { + -webkit-transition: background-color 70ms cubic-bezier(0.3,0,0.2,1); + transition: background-color 70ms cubic-bezier(0.3,0,0.2,1); +} + +.c44.c44.c44.c44.c44 { + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-delay: 0ms; + transition-delay: 0ms; + opacity: 0; +} + +.c42.c42.c42.c42.c42 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + width: 16px; + height: 16px; + position: relative; +} + +.c43.c43.c43.c43.c43 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + height: 100%; + position: relative; + will-change: transform,left; + width: 100%; + left: 0%; + -webkit-transform: translateX(0%); + -ms-transform: translateX(0%); + transform: translateX(0%); + -webkit-transition: 150ms; + transition: 150ms; + border-radius: 9999px; + -webkit-animation-duration: 150ms; + animation-duration: 150ms; + background-color: hsla(0,0%,100%,1); +} + +.c41.c41.c41.c41.c41 { + pointer-events: none; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 2px; + padding: 2px; + width: 36px; + height: 20px; + border-radius: 9999px; + background-color: hsla(211,20%,52%,0.12); + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c40.c40.c40.c40.c40 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + margin: 0 -1px -1px 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + left: -10000px; + white-space: nowrap; + word-wrap: normal; +} + +.c40.c40.c40.c40.c40:focus-visible + div { + outline: 4px solid hsla(227,100%,59%,0.18); + outline-offset: 1px; + -webkit-transition-property: outline-width; + transition-property: outline-width; + -webkit-transition-duration: 70ms; + transition-duration: 70ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c40.c40.c40.c40.c40:hover + div { + background-color: hsla(211,20%,52%,0.18); + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 70ms; + transition-duration: 70ms; +} + +.c39.c39.c39.c39.c39 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + margin-top: 2px; + margin-bottom: 2px; +} + +@media screen and (min-width:768px) { + .c0.c0.c0.c0.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } +} + +@media screen and (min-width:320px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:480px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:768px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:1024px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:1200px) { + .c2.c2.c2.c2.c2 { + border-right-style: solid; + } +} + +@media screen and (min-width:320px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:480px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:768px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:1024px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:1200px) { + .c3.c3.c3.c3.c3 { + border-right-style: solid; + } +} + +@media screen and (min-width:320px) { + .c30.c30.c30.c30.c30 { + border-top-style: solid; + } +} + +@media screen and (min-width:480px) { + .c30.c30.c30.c30.c30 { + border-top-style: solid; + } +} + +@media screen and (min-width:768px) { + .c30.c30.c30.c30.c30 { + width: 100%; + right: 0px; + bottom: 0px; + left: 0px; + border-top-style: solid; + } +} + +@media screen and (min-width:1024px) { + .c30.c30.c30.c30.c30 { + border-top-style: solid; + } +} + +@media screen and (min-width:1200px) { + .c30.c30.c30.c30.c30 { + border-top-style: solid; + } +} + +
+ +
+`; diff --git a/packages/blade/src/components/SideNav/docs/RazorpayLogo.tsx b/packages/blade/src/components/SideNav/docs/RazorpayLogo.tsx new file mode 100644 index 00000000000..33ce730b97e --- /dev/null +++ b/packages/blade/src/components/SideNav/docs/RazorpayLogo.tsx @@ -0,0 +1,58 @@ +const RazorpayLogo = (): React.ReactElement => { + return ( + + + + ); +}; + +const RazorpayLinesSvg = (): React.ReactElement => { + return ( + + + + + + + + + + ); +}; + +export { RazorpayLogo, RazorpayLinesSvg }; diff --git a/packages/blade/src/components/SideNav/docs/SideNav.stories.tsx b/packages/blade/src/components/SideNav/docs/SideNav.stories.tsx new file mode 100644 index 00000000000..27c10b08435 --- /dev/null +++ b/packages/blade/src/components/SideNav/docs/SideNav.stories.tsx @@ -0,0 +1,540 @@ +import React from 'react'; +import type { Meta, StoryFn } from '@storybook/react'; +import { Title } from '@storybook/addon-docs'; +import StoryRouter from 'storybook-react-router'; +import { Link, matchPath, Route, Switch, useLocation } from 'react-router-dom'; +import type { SideNavProps, SideNavSectionProps } from '../types'; +import type { SideNavLinkProps } from '..'; +import { + SideNavBody, + SideNav, + SideNavLink, + SideNavLevel, + SideNavSection, + SideNavFooter, + SideNavItem, +} from '..'; +import { RazorpayLinesSvg, RazorpayLogo } from './RazorpayLogo'; +import { sideNavWithReactRouter } from './code'; +import { getStyledPropsArgTypes } from '~components/Box/BaseBox/storybookArgTypes'; +import { Box } from '~components/Box'; +import { + ArrowUpRightIcon, + BillIcon, + BoxIcon, + BuildingIcon, + CashIcon, + ChevronRightIcon, + CodeSnippetIcon, + ConfettiIcon, + CreditCardIcon, + FilePlusIcon, + FileTextIcon, + HeadsetIcon, + LayoutIcon, + MenuIcon, + PlusIcon, + RazorpayxPayrollIcon, + ReportsIcon, + SettingsIcon, + StampIcon, + UserCheckIcon, + UserIcon, +} from '~components/Icons'; +import { Button } from '~components/Button'; +import { Tooltip } from '~components/Tooltip'; +import { Indicator } from '~components/Indicator'; +import { Switch as BladeSwitch } from '~components/Switch'; +import StoryPageWrapper from '~utils/storybook/StoryPageWrapper'; +import { Sandbox } from '~utils/storybook/Sandbox'; +import { Skeleton } from '~components/Skeleton'; +import { Card, CardBody } from '~components/Card'; +import { ProgressBar } from '~components/ProgressBar'; +import { Text } from '~components/Typography'; +import { Alert } from '~components/Alert'; + +const DocsPage = (): React.ReactElement => { + return ( + + Usage (with React Router v6) + + + + + ); +}; + +export default { + title: 'Components/SideNav', + component: SideNav, + tags: ['autodocs'], + argTypes: { + ...getStyledPropsArgTypes(), + }, + parameters: { + docs: { + page: DocsPage, + }, + }, + // eslint-disable-next-line babel/new-cap + decorators: [StoryRouter(undefined, { initialEntries: ['/settings/user/home'] })] as unknown, +} as Meta; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const Page = ({ match }: { match: any }): React.ReactElement => ( + +
+      {JSON.stringify(match, null, 4)}
+    
+
+); + +const DashboardSkeleton = ({ children }: { children: React.ReactElement }): React.ReactElement => { + return ( + + + + + + + + + + + + + + + + + + {children} + + + ); +}; + +type ItemsType = Pick; +type NavItemsJSONType = { + type: 'section'; + title?: SideNavSectionProps['title']; + maxItemsVisible?: SideNavSectionProps['maxVisibleItems']; + items: (ItemsType & { + items?: (ItemsType & { items?: ItemsType[] })[]; + })[]; +}; +const navItemsJSON: NavItemsJSONType[] = [ + { + type: 'section', + title: undefined, + items: [ + { + icon: LayoutIcon, + title: 'Home', + href: '/app/dashboard', + }, + { + icon: ArrowUpRightIcon, + title: 'Payouts', + href: '/app/payouts', + tooltip: { + content: 'Open Payouts (Cmd + O)', + }, + trailing: ( + + ) : null} {props.figmaURL ? ( - ) : null} diff --git a/packages/blade/src/utils/testing/renderWithTheme.native.tsx b/packages/blade/src/utils/testing/renderWithTheme.native.tsx index 4c93e4c494f..8ee796fed7f 100644 --- a/packages/blade/src/utils/testing/renderWithTheme.native.tsx +++ b/packages/blade/src/utils/testing/renderWithTheme.native.tsx @@ -1,14 +1,15 @@ -import type { RenderAPI } from '@testing-library/react-native'; +import type { RenderAPI, RenderOptions } from '@testing-library/react-native'; import { render } from '@testing-library/react-native'; import type { ReactElement } from 'react'; import { BladeProvider } from '~components/BladeProvider'; import { bladeTheme } from '~tokens/theme'; -const renderWithTheme = (ui: ReactElement): RenderAPI => +const renderWithTheme = (ui: ReactElement, options: RenderOptions = {}): RenderAPI => render( {ui} , + options, ); export default renderWithTheme; diff --git a/packages/blade/src/utils/testing/renderWithTheme.web.tsx b/packages/blade/src/utils/testing/renderWithTheme.web.tsx index ca463690c46..20ce282f4ff 100644 --- a/packages/blade/src/utils/testing/renderWithTheme.web.tsx +++ b/packages/blade/src/utils/testing/renderWithTheme.web.tsx @@ -1,14 +1,15 @@ -import type { RenderResult } from '@testing-library/react'; +import type { RenderOptions, RenderResult } from '@testing-library/react'; import { render } from '@testing-library/react'; import type { ReactElement } from 'react'; import { BladeProvider } from '~components/BladeProvider'; import { bladeTheme } from '~tokens/theme'; -const renderWithTheme = (ui: ReactElement): RenderResult => +const renderWithTheme = (ui: ReactElement, options: RenderOptions = {}): RenderResult => render( {ui} , + options, ); export default renderWithTheme;