diff --git a/.changeset/wicked-oranges-whisper.md b/.changeset/wicked-oranges-whisper.md new file mode 100644 index 00000000000..40c47c1b373 --- /dev/null +++ b/.changeset/wicked-oranges-whisper.md @@ -0,0 +1,9 @@ +--- +"@razorpay/blade": minor +--- + +feat(blade): revamped top-nav component + +> NOTE: +> This might be a breaking change for you, if your project uses the older deprecated TopNav. + diff --git a/packages/blade/src/components/Menu/VisualSubComponents/MenuHeaderFooter.web.tsx b/packages/blade/src/components/Menu/VisualSubComponents/MenuHeaderFooter.web.tsx index 631bb6f69d0..e22bfb6e3c9 100644 --- a/packages/blade/src/components/Menu/VisualSubComponents/MenuHeaderFooter.web.tsx +++ b/packages/blade/src/components/Menu/VisualSubComponents/MenuHeaderFooter.web.tsx @@ -18,7 +18,7 @@ const _MenuHeader = ({ testID, }: MenuHeaderProps): React.ReactElement => { return ( - <> + - > + ); }; @@ -47,7 +47,7 @@ const MenuHeader = assignWithoutSideEffects(_MenuHeader, { const _MenuFooter = ({ children, testID }: MenuFooterProps): React.ReactElement => { return ( - + - + + Header Title + + + - Header Title + Subtitle - - Subtitle - - - + data-blade-component="base-box" + > + + + - Custom Slot Account @@ -789,7 +798,7 @@ exports[`Menu renders a Menu 1`] = ` data-blade-component="box" > Accounts @@ -797,23 +806,23 @@ exports[`Menu renders a Menu 1`] = ` Profile @@ -860,35 +869,35 @@ exports[`Menu renders a Menu 1`] = ` /> Settings @@ -900,11 +909,11 @@ exports[`Menu renders a Menu 1`] = ` /> Cmd + S @@ -915,30 +924,30 @@ exports[`Menu renders a Menu 1`] = ` Share @@ -950,7 +959,7 @@ exports[`Menu renders a Menu 1`] = ` /> Log Out @@ -1008,26 +1017,26 @@ exports[`Menu renders a Menu 1`] = ` /> Footer slot diff --git a/packages/blade/src/components/Menu/docs/Menu.stories.tsx b/packages/blade/src/components/Menu/docs/Menu.stories.tsx index 72e7964ab1f..bd83739f3f0 100644 --- a/packages/blade/src/components/Menu/docs/Menu.stories.tsx +++ b/packages/blade/src/components/Menu/docs/Menu.stories.tsx @@ -170,7 +170,7 @@ type TemplateProps = MenuProps & { trigger: React.ReactElement }; const accountsMenuOverlayContent = ( <> } /> - + Razorpay Pvt Ltd @@ -182,7 +182,7 @@ const accountsMenuOverlayContent = ( Switch Merchant - + } diff --git a/packages/blade/src/components/Menu/types.ts b/packages/blade/src/components/Menu/types.ts index 44b81a90c12..caf8a4ef12c 100644 --- a/packages/blade/src/components/Menu/types.ts +++ b/packages/blade/src/components/Menu/types.ts @@ -114,7 +114,7 @@ type MenuOverlayProps = { /** * JSX Slot for MenuItem or anything else */ - children: React.ReactElement[] | React.ReactElement; + children: React.ReactElement[] | React.ReactElement | React.ReactNode; /** * zIndex override diff --git a/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx b/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx index c9b1b93d265..6c89d280429 100644 --- a/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx +++ b/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx @@ -1,181 +1,116 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable consistent-return */ import React from 'react'; -import styled from 'styled-components'; -import { useTopNavContext } from '../TopNavContext'; -import { approximatelyEqual, MIXED_BG_COLOR, useHasOverflow } from './utils'; +import ReactDOM from 'react-dom'; import { TabNavContext } from './TabNavContext'; +import { useResize } from './utils'; +import type { TabNavItemData, TabNavProps } from './types'; import BaseBox from '~components/Box/BaseBox'; import type { StyledPropsBlade } from '~components/Box/styledProps'; import { getStyledProps } from '~components/Box/styledProps'; -import { Button } from '~components/Button'; import { Divider } from '~components/Divider'; -import { ChevronLeftIcon, ChevronRightIcon } from '~components/Icons'; -import { makeMotionTime, makeSize } from '~utils'; +import { makeSize } from '~utils'; import { size } from '~tokens/global'; -import getIn from '~utils/lodashButBetter/get'; -import type { BoxProps } from '~components/Box'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import type { BoxProps } from '~components/Box'; +import { Box } from '~components/Box'; -const GRADIENT_WIDTH = 54 as const; -const GRADIENT_OFFSET = -8 as const; -const OFFSET_BOTTOM = -12 as const; -const SCROLL_AMOUNT = 200; - -type TabNavProps = { - children: React.ReactNode; +const TabNavItems = ({ children, ...props }: BoxProps): React.ReactElement => { + return ( + + {React.Children.map(children, (child, index) => { + return ( + <> + {index > 0 ? ( + + ) : null} + {React.cloneElement(child as React.ReactElement, { + __isInsideTabNavItems: true, + __index: index, + })} + > + ); + })} + + + ); }; -const ScrollableArea = styled(BaseBox)(() => { - return { - '&::-webkit-scrollbar': { display: 'none' }, - }; -}); - -const GradientOverlay = styled(BaseBox)<{ - shouldShow?: boolean; - variant: 'left' | 'right'; - $color: BoxProps['backgroundColor']; -}>(({ theme, shouldShow, variant, $color }) => { - const color = getIn(theme.colors, $color as never, MIXED_BG_COLOR); - - return { - position: 'absolute', - [variant]: 0, - pointerEvents: shouldShow ? 'auto' : 'none', - transform: shouldShow ? 'scale(1)' : 'scale(0.5)', - opacity: shouldShow ? 1 : 0, - transitionTimingFunction: `${theme.motion.easing.standard.revealing}`, - transitionDuration: `${makeMotionTime(theme.motion.duration.xquick)}`, - transitionProperty: 'opacity, transform', - zIndex: 1, - ':before': { - content: "''", - pointerEvents: 'none', - position: 'absolute', - [variant]: 0, - top: makeSize(GRADIENT_OFFSET), - bottom: makeSize(GRADIENT_OFFSET), - width: makeSize(GRADIENT_WIDTH), - background: `linear-gradient(to ${variant}, transparent 0%, ${color} 30%, ${color} 100%);`, - }, - }; -}); - const TabNav = ({ children, + items, ...styledProps }: TabNavProps & StyledPropsBlade): React.ReactElement => { const ref = React.useRef(null); - const hasOverflow = useHasOverflow(ref); - const [scrollStatus, setScrollStatus] = React.useState<'start' | 'end' | 'middle'>('start'); - const { backgroundColor } = useTopNavContext(); + const [controlledItems, setControlledItems] = React.useState(items); - // Check if the scroll is at start, end or middle - const handleScrollStatus = React.useCallback( - (e: React.UIEvent): void => { - const target = e.target as HTMLDivElement; - const isAtStart = target.scrollLeft === 0; - const isAtEnd = approximatelyEqual( - target.scrollLeft, - target.scrollWidth - target.offsetWidth, - ); - - if (isAtStart) { - setScrollStatus('start'); - } else if (isAtEnd) { - setScrollStatus('end'); - } else { - setScrollStatus('middle'); - } - }, - [], + const overflowingItems = controlledItems.filter( + (item) => item.isAlwaysOverflowing ?? item.isOverflowing, ); + const _items = controlledItems.filter((item) => !item.isAlwaysOverflowing && !item.isOverflowing); - const scrollRight = (): void => { - if (!ref.current) return; - ref.current.scrollBy({ - behavior: 'smooth', - left: SCROLL_AMOUNT, - }); - }; + // We need to memoize this callback otherwise it will cause infinite re-renders + // Because the ResizeObserver callback will be a new reference on every render + // and it will trigger a re-render + const resizeCallback = React.useCallback((resizeInfo: ResizeObserverEntry): void => { + const target = resizeInfo.target as HTMLElement; + const updateItems = (): void => { + setControlledItems((items) => { + return items.map((item, index) => { + // never overflow the first item + if (index === 0) return { ...item, isOverflowing: false }; + // add padding to the offsetX to account the "More" menu's width changing due to the selection state (eg: More:ProdctName) + // Currently, hardcoding this to 150, we can make this dynamic too but that's causing layout thrashing + // because first we need to calculate the width of the "More" menu and then update the items + const padding = 150; + const offset = (item.offsetX! + padding)! - target.getBoundingClientRect().left; + if (offset > target.offsetWidth) { + return { ...item, isOverflowing: true }; + } else { + return { ...item, isOverflowing: false }; + } + }); + }); + }; + // https://github.com/webpack/webpack/issues/14814 + const flushSync = (ReactDOM as any)['flushSync'.toString()]; + // Using flushSync to avoid layout thrashing, + // this will force React to flush all pending updates and only then update the DOM + if (flushSync !== undefined) { + flushSync(updateItems); + } else { + updateItems(); + } + }, []); - const scrollLeft = (): void => { - if (!ref.current) return; - ref.current.scrollBy({ - behavior: 'smooth', - left: -SCROLL_AMOUNT, - }); - }; + useResize(ref, resizeCallback); return ( - + - - - - + - {React.Children.map(children, (child, index) => { - return ( - <> - {index > 0 ? ( - - ) : null} - {child} - > - ); - })} + {children({ items: _items, overflowingItems })} - - - - + ); }; -export { TabNav }; +export { TabNav, TabNavItems }; diff --git a/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx b/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx index 5cca5072449..a5a11a368a3 100644 --- a/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx +++ b/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx @@ -1,9 +1,11 @@ import React from 'react'; +import type { TabNavItemData } from './types'; import { throwBladeError } from '~utils/logger'; type TabNavContextProps = { containerRef: React.RefObject; - hasOverflow: boolean; + controlledItems: TabNavItemData[]; + setControlledItems: React.Dispatch>; }; const TabNavContext = React.createContext(null); diff --git a/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx b/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx index 966649f5859..f911864bc2a 100644 --- a/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx +++ b/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable consistent-return */ import React from 'react'; import styled from 'styled-components'; -import { useTopNavContext } from '../TopNavContext'; import type { TabNavItemProps } from './types'; import { useTabNavContext } from './TabNavContext'; import { MIXED_BG_COLOR } from './utils'; @@ -10,11 +12,8 @@ import { makeBorderSize, makeMotionTime, makeSize, makeSpace } from '~utils'; import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects'; import { makeAccessible } from '~utils/makeAccessible'; import { size } from '~tokens/global'; -import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; -import { mergeRefs } from '~utils/useMergeRefs'; -import type { BoxProps } from '~components/Box'; -import getIn from '~utils/lodashButBetter/get'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; const StyledTabNavItem = styled.a<{ $isActive?: boolean }>(({ theme, $isActive }) => { return { @@ -37,6 +36,14 @@ const StyledTabNavItem = styled.a<{ $isActive?: boolean }>(({ theme, $isActive } paddingLeft: makeSpace(theme.spacing[4]), paddingRight: makeSpace(theme.spacing[4]), borderRadius: makeBorderSize(theme.border.radius.medium), + // reset button styles + border: 'none', + background: 'none', + '&[aria-expanded="true"]': $isActive + ? {} + : { + backgroundColor: theme.colors.interactive.background.gray.default, + }, '&:hover': $isActive ? {} : { @@ -47,16 +54,15 @@ const StyledTabNavItem = styled.a<{ $isActive?: boolean }>(({ theme, $isActive } const StyledTabNavItemWrapper = styled(BaseBox)<{ isActive?: boolean; - dividerHiderColor: BoxProps['backgroundColor']; -}>(({ theme, isActive, dividerHiderColor }) => { +}>(({ theme, isActive }) => { const dividerHiderStyle = { content: '""', position: 'absolute', top: '50%', transform: 'translateY(-50%)', width: makeSize(size[1]), - height: '50%', - backgroundColor: getIn(theme.colors, dividerHiderColor as never, MIXED_BG_COLOR), + height: makeSize(size[16]), + backgroundColor: MIXED_BG_COLOR, } as const; return { @@ -66,16 +72,14 @@ const StyledTabNavItemWrapper = styled(BaseBox)<{ backgroundColor: isActive ? theme.colors.surface.background.gray.intense : 'transparent', borderColor: isActive ? theme.colors.surface.border.gray.muted : 'transparent', borderStyle: 'solid', - borderBottomWidth: 0, borderWidth: makeBorderSize(theme.border.width.thin), + borderBottomWidth: 0, borderTopLeftRadius: makeBorderSize(theme.border.radius.medium), borderTopRightRadius: makeBorderSize(theme.border.radius.medium), - // Animation - transform: isActive ? `translateY(${makeSize(size[2])})` : 'none', transition: `${makeMotionTime(theme.motion.duration.moderate)} ${ theme.motion.easing.standard.effective }`, - transitionProperty: 'background, transform', + transitionProperty: 'background', // Hide the left and right divider by overlaying them with a pseudo element as same color as the background ...(isActive @@ -113,47 +117,55 @@ const SelectedBar = styled(BaseBox)<{ isActive?: boolean }>(({ theme, isActive } }); const _TabNavItem: React.ForwardRefRenderFunction = ( - { as, children, isActive, icon: Icon, trailing, accessibilityLabel, href, target, ...props }, + { + as, + title, + isActive, + icon: Icon, + trailing, + accessibilityLabel, + href, + target, + // @ts-expect-error - This prop is only used internally + __isInsideTabNavItems, + // @ts-expect-error - This prop is only used internally + __index, + ...props + }, ref, ): React.ReactElement => { - const { containerRef, hasOverflow } = useTabNavContext(); - const { backgroundColor } = useTopNavContext(); - const linkRef = React.useRef(null); + const { setControlledItems } = useTabNavContext(); + const bodyRef = React.useRef(null); - // Scroll the active tab into view - // Only if the tab is very close to the edge - // Or if the tab is out of view + // Update the controlledItems with the tabWidth and offsetX useIsomorphicLayoutEffect(() => { - if (!isActive || !hasOverflow) return; - if (!('requestAnimationFrame' in window)) return; - - window.requestAnimationFrame(() => { - if (!linkRef.current || !containerRef.current) return; - - const buffer = 100; - const container = containerRef.current; - const linkElement = linkRef.current; - const containerRect = container.getBoundingClientRect(); - const linkRect = linkElement.getBoundingClientRect(); - const isCloseToStart = linkRect.left < containerRect.left + buffer; - const isCloseToEnd = linkRect.right > containerRect.right - buffer; - - if (isCloseToStart || isCloseToEnd) { - linkElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); - } + if (!bodyRef.current) return; + if (!__isInsideTabNavItems) return; + setControlledItems((prev) => { + return prev.map((item, index) => { + if (index !== __index) return item; + const bounds = bodyRef?.current?.getBoundingClientRect()!; + const tabWidth = bounds.width; + const offsetX = bounds.right; + return { + ...item, + tabWidth, + offsetX, + }; + }); }); - }, [hasOverflow, isActive]); + }, [__isInsideTabNavItems, __index, setControlledItems]); return ( ) : null} - {children} + {title} {trailing ? trailing : null} diff --git a/packages/blade/src/components/TopNav/TabNav/types.ts b/packages/blade/src/components/TopNav/TabNav/types.ts index e0d38fc59ab..646e28677df 100644 --- a/packages/blade/src/components/TopNav/TabNav/types.ts +++ b/packages/blade/src/components/TopNav/TabNav/types.ts @@ -32,7 +32,7 @@ type TabNavItemProps = { * ``` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - as?: React.ComponentType; + as?: React.ComponentType | 'a' | 'button'; /** * Selected state of the navigation item. * @@ -52,15 +52,30 @@ type TabNavItemProps = { */ trailing?: React.ReactElement; /** - * Element to render inside the navigation item. - * - * This can either be a string or JSX element (eg: Menu component) + * Title of the navigation item. */ - children?: React.ReactNode; + title?: string; /** * Accessibility label for the navigation item. */ accessibilityLabel?: string; } & MenuTriggerProps; -export type { TabNavItemProps }; +type Item = TabNavItemProps & { + description?: string; + isAlwaysOverflowing?: boolean; +}; +type TabNavItemData = Item & { + isOverflowing?: boolean; + tabWidth?: number; + offsetX?: number; +}; +type TabNavProps = { + items: Item[]; + children: (props: { + items: TabNavItemData[]; + overflowingItems: TabNavItemData[]; + }) => React.ReactElement; +}; + +export type { TabNavItemProps, TabNavItemData, TabNavProps }; diff --git a/packages/blade/src/components/TopNav/TabNav/utils.ts b/packages/blade/src/components/TopNav/TabNav/utils.ts index dd25928377c..5a39973c858 100644 --- a/packages/blade/src/components/TopNav/TabNav/utils.ts +++ b/packages/blade/src/components/TopNav/TabNav/utils.ts @@ -1,51 +1,38 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable consistent-return */ -import React from 'react'; +import type React from 'react'; import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; /** - * Check if an element has scroll overflow + * Hook to observe resize events on a given element */ -const useHasOverflow = ( +const useResize = ( ref: React.RefObject, - callback?: (hasOverflow: boolean) => void, -): boolean => { - const observer = React.useRef(null); - const [hasOverflow, setHasOverflow] = React.useState(false); - + callback?: (entry: ResizeObserverEntry) => void, +) => { useIsomorphicLayoutEffect(() => { if (!ref.current) return; const element = ref.current; - const trigger = (): void => { - const hasOverflow = element.scrollWidth > element.clientWidth; - setHasOverflow(hasOverflow); - - if (callback) callback(hasOverflow); - }; + if (!('ResizeObserver' in window)) return; - trigger(); - if ('ResizeObserver' in window) { - observer.current = new ResizeObserver(trigger); - observer.current.observe(element); - } + const observer = new ResizeObserver((entries) => { + entries.forEach((entry) => { + callback?.(entry); + }); + }); + observer.observe(element); // destroy the observer return (): void => { - if ('ResizeObserver' in window) { - observer.current?.disconnect(); - } + if (!('ResizeObserver' in window)) return; + observer?.disconnect(); }; - }, [callback, ref]); - - return hasOverflow; -}; - -const approximatelyEqual = (v1: number, v2: number, tolerance = 1): boolean => { - return Math.abs(v1 - v2) < tolerance; + }, [callback]); }; // Overlapping color of surface.background.gray.subtle + interactive.background.gray.default // TODO(future): design will tokenize or check if this is needed or not const MIXED_BG_COLOR = '#e1e7ef'; -export { useHasOverflow, approximatelyEqual, MIXED_BG_COLOR }; +export { useResize, MIXED_BG_COLOR }; diff --git a/packages/blade/src/components/TopNav/TopNav.web.tsx b/packages/blade/src/components/TopNav/TopNav.web.tsx index 11ee8dfa4c6..2ca8cf962ce 100644 --- a/packages/blade/src/components/TopNav/TopNav.web.tsx +++ b/packages/blade/src/components/TopNav/TopNav.web.tsx @@ -1,9 +1,7 @@ import React from 'react'; -import { TopNavContext } from './TopNavContext'; import type { BoxProps } from '~components/Box'; import { Box } from '~components/Box'; import BaseBox from '~components/Box/BaseBox'; -import { Divider } from '~components/Divider'; import { SIDE_NAV_EXPANDED_L1_WIDTH_XL, SIDE_NAV_EXPANDED_L1_WIDTH_BASE, @@ -11,42 +9,12 @@ import { import { size } from '~tokens/global'; import { makeSize } from '~utils'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import type { StyledPropsBlade } from '~components/Box/styledProps'; +import { componentZIndices } from '~utils/componentZIndices'; const TOP_NAV_HEIGHT = size[56]; const CONTENT_RIGHT_GAP = size[80]; -const RazorpayLinesSvg = (): React.ReactElement => { - return ( - - - - - - - - - - ); -}; - type TopNavProps = { children: React.ReactNode; } & Pick< @@ -66,40 +34,33 @@ type TopNavProps = { | 'right' | 'width' | 'zIndex' ->; +> & + StyledPropsBlade; -const TopNav = ({ children, ...styledProps }: TopNavProps): React.ReactElement => { +const TopNav = ({ children, ...boxProps }: TopNavProps): React.ReactElement => { return ( - - - {children} - - - - - + + {children} + ); }; const TopNavBrand = ({ children }: { children: React.ReactNode }): React.ReactElement => { return ( {children} - - - ); }; @@ -128,7 +81,7 @@ const TopNavContent = ({ children }: { children: React.ReactNode }): React.React @@ -140,10 +93,15 @@ const TopNavContent = ({ children }: { children: React.ReactNode }): React.React const TopNavActions = ({ children }: { children: React.ReactNode }): React.ReactElement => { return ( {children} diff --git a/packages/blade/src/components/TopNav/TopNavContext.tsx b/packages/blade/src/components/TopNav/TopNavContext.tsx deleted file mode 100644 index c9e01ae14e6..00000000000 --- a/packages/blade/src/components/TopNav/TopNavContext.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { MIXED_BG_COLOR } from './TabNav/utils'; -import type { BoxProps } from '~components/Box'; - -type TopNavContextProps = { - backgroundColor: BoxProps['backgroundColor']; -}; -const TopNavContext = React.createContext({ - backgroundColor: MIXED_BG_COLOR as never, -}); - -const useTopNavContext = (): TopNavContextProps => { - const context = React.useContext(TopNavContext); - return context!; -}; - -export { TopNavContext, useTopNavContext }; diff --git a/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx b/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx index 7a8be71ae64..4d56947df54 100644 --- a/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx +++ b/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx @@ -3,22 +3,102 @@ import type { StoryFn } from '@storybook/react'; import { within, userEvent } from '@storybook/testing-library'; import { expect } from '@storybook/jest'; import React from 'react'; -import { TabNav, TabNavItem } from '../TabNav'; -import { Box } from '~components/Box'; -import { HomeIcon } from '~components/Icons'; +import type { TabNavProps } from '../TabNav'; +import { TabNav, TabNavItem, TabNavItems } from '../TabNav'; +import { + AcceptPaymentsIcon, + AwardIcon, + ChevronDownIcon, + HomeIcon, + MagicCheckoutIcon, + RazorpayxPayrollIcon, +} from '~components/Icons'; +import { Menu, MenuItem, MenuOverlay } from '~components/Menu'; +import { Text } from '~components/Typography'; const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); -export const TestBasicTabNav: StoryFn = (): React.ReactElement => { +const TabNavExample = ({ items }: { items?: TabNavProps['items'] }): React.ReactElement => { return ( - - - Payroll - Payments + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + return ( + { + console.log('clicked', item.title); + }} + > + {item.title} + + ); + })} + + + ) : null} + > + ); + }} ); }; +export const TestBasicTabNav: StoryFn = (): React.ReactElement => { + return ; +}; + TestBasicTabNav.play = async ({ canvasElement }) => { const { getByRole } = within(canvasElement); const homeTab = getByRole('link', { name: 'Home' }); @@ -30,45 +110,90 @@ TestBasicTabNav.play = async ({ canvasElement }) => { }; export const TestOverflow: StoryFn = (): React.ReactElement => { + return ; +}; + +TestOverflow.play = async ({ canvasElement }) => { + const { getByRole, queryByRole } = within(document.body); + canvasElement.style.width = '100%'; + + await sleep(500); + const homeTab = getByRole('link', { name: 'Home' }); + const payrollTab = getByRole('link', { name: 'Payroll' }); + const paymentsTab = getByRole('link', { name: 'Payments' }); + await expect(homeTab).toBeVisible(); + await expect(payrollTab).toBeVisible(); + await expect(paymentsTab).toBeVisible(); + + await sleep(500); + + // reduce the width of the canvas to make the tabs overflow + canvasElement.style.width = '600px'; + await sleep(500); + + const moreTab = getByRole('button', { name: 'More' }); + await userEvent.hover(moreTab); + await sleep(500); + await expect(queryByRole('menu', { name: 'More' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Rize' })).toBeVisible(); + await expect(queryByRole('link', { name: 'Magic Checkout' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Magic Checkout' })).toBeVisible(); + + canvasElement.style.width = '300px'; + await sleep(500); + await expect(queryByRole('link', { name: 'Payroll' })).toBeNull(); + await expect(queryByRole('link', { name: 'Payments' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Payroll' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payments' })).toBeVisible(); + + canvasElement.style.width = '100%'; + await sleep(500); + await expect(queryByRole('menuitem', { name: 'Rize' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payroll' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Payments' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Magic Checkout' })).toBeNull(); +}; + +export const ShouldNotShowMore: StoryFn = (): React.ReactElement => { return ( - - - Item 1 - Item 2 - Item 3 - Item 4 - Item 5 - Item 6 - Item 7 - Item 8 - Item 9 - - + ); }; -TestOverflow.play = async ({ canvasElement }) => { - const { getByRole } = within(canvasElement); +ShouldNotShowMore.play = async ({ canvasElement }) => { + const { getByRole, queryByRole } = within(document.body); + + await sleep(500); + const homeTab = getByRole('link', { name: 'Home' }); + const payrollTab = getByRole('link', { name: 'Payroll' }); + const paymentsTab = getByRole('link', { name: 'Payments' }); + await expect(homeTab).toBeVisible(); + await expect(payrollTab).toBeVisible(); + await expect(paymentsTab).toBeVisible(); + await expect(queryByRole('button', { name: 'More' })).toBeNull(); + await sleep(500); - const item1 = getByRole('link', { name: 'Item 1' }); - const scrollLeftButton = getByRole('button', { name: /Scroll Left/ }); - const scrollRightButton = getByRole('button', { name: /Scroll Right/ }); - await expect(scrollLeftButton).not.toBeVisible(); - - await expect(item1).toBeVisible(); - await expect(scrollRightButton).toBeVisible(); - // scroll - await userEvent.click(scrollRightButton); + + // reduce the width of the canvas to make the tabs overflow + canvasElement.style.width = '200px'; await sleep(500); - await expect(scrollLeftButton).toBeVisible(); - await expect(scrollRightButton).toBeVisible(); + const moreTab = getByRole('button', { name: 'More' }); + await expect(moreTab).toBeVisible(); - // scroll to end - await userEvent.click(scrollRightButton); + // hover over the more tab + await userEvent.hover(moreTab); await sleep(500); - await expect(scrollLeftButton).toBeVisible(); - await expect(scrollRightButton).not.toBeVisible(); + + await expect(queryByRole('menu', { name: 'More' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payments' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payroll' })).toBeVisible(); }; export default { diff --git a/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx b/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx index f30336d1bb6..bf5e19371a7 100644 --- a/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx +++ b/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx @@ -1,11 +1,13 @@ -import { TabNav, TabNavItem } from '../TabNav'; +import { TabNav, TabNavItem, TabNavItems } from '../TabNav'; import { TopNav, TopNavActions, TopNavBrand, TopNavContent } from '../TopNav'; import { Avatar } from '~components/Avatar'; import { Box } from '~components/Box'; import { Button } from '~components/Button'; -import { ActivityIcon, AnnouncementIcon, HomeIcon } from '~components/Icons'; +import { ActivityIcon, AnnouncementIcon, ChevronDownIcon } from '~components/Icons'; import { RazorpayLogo } from '~components/SideNav/docs/RazorpayLogo'; import { Tooltip } from '~components/Tooltip'; +import { Menu, MenuItem, MenuOverlay } from '~components/Menu'; +import { Text } from '~components/Typography'; const TopNavExample = (): React.ReactElement => { return ( @@ -15,11 +17,67 @@ const TopNavExample = (): React.ReactElement => { - - - Payroll - Payments - Magic Checkout + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + return ( + { + console.log('clicked', item.title); + }} + > + {item.title} + + ); + })} + + + ) : null} + > + ); + }} diff --git a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap index e393fa4a3e2..0d44ec52691 100644 --- a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap +++ b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should render TopNav ssr 1`] = `"PayrollPaymentsMagic CheckoutAH"`; +exports[` should render TopNav ssr 1`] = `"HomePayrollPaymentsMagic CheckoutMoreAH"`; exports[` should render TopNav ssr 2`] = ` .c0.c0.c0.c0.c0 { @@ -16,10 +16,12 @@ exports[` should render TopNav ssr 2`] = ` align-items: center; position: -webkit-sticky; position: sticky; - z-index: 1; - grid-template-columns: minmax(0,1fr) auto; - padding-right: 8px; - padding-left: 8px; + z-index: 100; + grid-template-columns: auto minmax(0,1fr) auto; + padding-top: 8px; + padding-bottom: 8px; + padding-right: 12px; + padding-left: 12px; height: 56px; width: 100%; top: 0px; @@ -27,7 +29,6 @@ exports[` should render TopNav ssr 2`] = ` } .c2.c2.c2.c2.c2 { - display: none; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -41,23 +42,6 @@ exports[` should render TopNav ssr 2`] = ` } .c4.c4.c4.c4.c4 { - display: none; - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; -} - -.c5.c5.c5.c5.c5 { - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; - margin-right: -1px; - height: 20px; - border-left-color: hsla(211,20%,52%,0.18); - border-left-style: solid; -} - -.c7.c7.c7.c7.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -66,11 +50,13 @@ exports[` should render TopNav ssr 2`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; padding-right: 0px; - margin-left: 0px; } -.c8.c8.c8.c8.c8 { +.c5.c5.c5.c5.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -79,82 +65,54 @@ exports[` should render TopNav ssr 2`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; position: relative; - margin-bottom: -12px; width: 100%; } -.c12.c12.c12.c12.c12 { +.c6.c6.c6.c6.c6 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -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; - z-index: 1; + position: relative; + width: 100%; } -.c14.c14.c14.c14.c14 { +.c7.c7.c7.c7.c7 { 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-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; } -.c15.c15.c15.c15.c15 { +.c8.c8.c8.c8.c8 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - overflow-x: auto; - overflow-y: hidden; - white-space: nowrap; position: relative; width: 100%; gap: 0px; + left: -1px; } -.c17.c17.c17.c17.c17 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - width: -webkit-max-content; - width: -moz-max-content; - width: max-content; -} - -.c21.c21.c21.c21.c21 { +.c12.c12.c12.c12.c12 { margin: auto; height: 16px; border-left-color: hsla(211,20%,52%,0.18); border-left-style: solid; } -.c24.c24.c24.c24.c24 { +.c14.c14.c14.c14.c14 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -163,25 +121,25 @@ exports[` should render TopNav ssr 2`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; + padding: 8px; margin-top: 2px; gap: 8px; -} - -.c26.c26.c26.c26.c26 { background-color: hsla(0,0%,100%,1); + border-top-left-radius: 4px; + border-top-right-radius: 4px; } -.c28.c28.c28.c28.c28 { - position: relative; - height: 100%; - width: 100%; -} - -.c30.c30.c30.c30.c30 { +.c17.c17.c17.c17.c17 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -194,252 +152,81 @@ exports[` should render TopNav ssr 2`] = ` -ms-flex-pack: center; justify-content: center; z-index: 1; - height: 100%; -} - -.c32.c32.c32.c32.c32 { - position: absolute; - top: 0px; - left: 0px; - pointer-events: none; } -.c10.c10.c10.c10.c10 { - min-height: 28px; - height: 28px; - width: 28px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; +.c19.c19.c19.c19.c19 { + 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-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c10.c10.c10.c10.c10:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c10.c10.c10.c10.c10 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c25.c25.c25.c25.c25 { - min-height: 36px; - height: 36px; - width: 36px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c25.c25.c25.c25.c25:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c25.c25.c25.c25.c25 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c11.c11.c11.c11.c11 { - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); - -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); - transition-duration: cubic-bezier(0.3,0,0.2,1); - -webkit-transition-timing-function: 150px; - transition-timing-function: 150px; -} - -.c31.c31.c31.c31.c31 { - color: hsla(211,33%,21%,1); - font-family: "Inter","Inter Fallback Arial",Arial; - font-size: 0.75rem; - font-weight: 600; - 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; - margin: 0; - padding: 0; -} - -.c13.c13.c13.c13.c13 { - opacity: 1; } -.c6.c6.c6.c6.c6 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 20px; +.c20.c20.c20.c20.c20 { + background-color: hsla(0,0%,100%,1); } .c22.c22.c22.c22.c22 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 16px; -} - -.c16.c16.c16.c16.c16::-webkit-scrollbar { - display: none; + position: relative; + height: 100%; + width: 100%; } -.c9.c9.c9.c9.c9 { - position: absolute; - left: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; +.c24.c24.c24.c24.c24 { + 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; z-index: 1; + height: 100%; } - -.c9.c9.c9.c9.c9:before { - content: ''; - pointer-events: none; - position: absolute; - left: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to left,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); -} - -.c23.c23.c23.c23.c23 { - position: absolute; - right: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; - z-index: 1; + +.c13.c13.c13.c13.c13 { + border-width: 0; + border-left-style: solid; + border-left-width: 1px; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + height: 16px; } -.c23.c23.c23.c23.c23:before { - content: ''; - pointer-events: none; - position: absolute; - right: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to right,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); +.c25.c25.c25.c25.c25 { + color: hsla(211,33%,21%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.75rem; + font-weight: 600; + 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; + margin: 0; + padding: 0; } -.c20.c20.c20.c20.c20 { +.c11.c11.c11.c11.c11 { color: hsla(211,26%,34%,1); font-family: "Inter","Inter Fallback Arial",Arial; font-size: 0.875rem; @@ -479,13 +266,19 @@ exports[` should render TopNav ssr 2`] = ` padding-left: 12px; padding-right: 12px; border-radius: 4px; + border: none; + background: none; } -.c20.c20.c20.c20.c20:hover { +.c11.c11.c11.c11.c11[aria-expanded="true"] { background-color: hsla(211,20%,52%,0.12); } -.c18.c18.c18.c18.c18 { +.c11.c11.c11.c11.c11:hover { + background-color: hsla(211,20%,52%,0.12); +} + +.c9.c9.c9.c9.c9 { position: relative; -webkit-flex-shrink: 0; -ms-flex-negative: 0; @@ -494,21 +287,17 @@ exports[` should render TopNav ssr 2`] = ` background-color: transparent; border-color: transparent; border-style: solid; - border-bottom-width: 0; border-width: 1px; + border-bottom-width: 0; border-top-left-radius: 4px; border-top-right-radius: 4px; - -webkit-transform: none; - -ms-transform: none; - transform: none; -webkit-transition: 250ms cubic-bezier(0.3,0,0.2,1); transition: 250ms cubic-bezier(0.3,0,0.2,1); - -webkit-transition-property: background,-webkit-transform; - -webkit-transition-property: background,transform; - transition-property: background,transform; + -webkit-transition-property: background; + transition-property: background; } -.c19.c19.c19.c19.c19 { +.c10.c10.c10.c10.c10 { position: absolute; top: 0; left: 0; @@ -525,7 +314,7 @@ exports[` should render TopNav ssr 2`] = ` transition-property: opacity; } -.c27.c27.c27.c27.c27 { +.c21.c21.c21.c21.c21 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -536,7 +325,7 @@ exports[` should render TopNav ssr 2`] = ` outline: 0.5px solid hsla(214,28%,84%,1); } -.c29.c29.c29.c29.c29 { +.c23.c23.c23.c23.c23 { display: block; text-align: center; -webkit-text-decoration: none; @@ -550,7 +339,7 @@ exports[` should render TopNav ssr 2`] = ` background-color: hsla(211,20%,52%,0.18); } -.c29.c29.c29.c29.c29 img { +.c23.c23.c23.c23.c23 img { display: block; height: 36px; width: 36px; @@ -558,130 +347,132 @@ exports[` should render TopNav ssr 2`] = ` object-fit: cover; } -@media screen and (min-width:768px) { - .c1.c1.c1.c1.c1 { - grid-template-columns: auto minmax(0,1fr) auto; - } +.c15.c15.c15.c15.c15 { + min-height: 36px; + height: 36px; + width: 36px; + cursor: pointer; + background-color: hsla(211,20%,52%,0.12); + border-color: hsla(214,28%,84%,1); + border-width: 0px; + border-radius: 4px; + border-style: solid; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 0px; + padding-right: 0px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-transition-property: background-color,border-color,box-shadow; + transition-property: background-color,border-color,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: 150ms; + transition-duration: 150ms; + position: relative; } -@media screen and (min-width:768px) { - .c2.c2.c2.c2.c2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } +.c15.c15.c15.c15.c15:hover { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:1200px) { - .c2.c2.c2.c2.c2 { - width: 264px; - } +.c15.c15.c15.c15.c15:active { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:768px) { - .c4.c4.c4.c4.c4 { - display: block; - } +.c15.c15.c15.c15.c15:focus-visible { + background-color: hsla(211,20%,52%,0.18); + outline: 1px solid hsla(227,100%,59%,0.09); + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); } -@media screen and (min-width:320px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c15.c15.c15.c15.c15 * { + -webkit-transition-property: color,fill,opacity; + transition-property: color,fill,opacity; + -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); } -@media screen and (min-width:480px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c16.c16.c16.c16.c16 { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); + transition-duration: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: 150px; + transition-timing-function: 150px; } -@media screen and (min-width:768px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c18.c18.c18.c18.c18 { + opacity: 1; } -@media screen and (min-width:1024px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; +@media screen and (min-width:768px) { + .c1.c1.c1.c1.c1 { + padding-top: 0px; + padding-bottom: 0px; + padding-right: 8px; + padding-left: 8px; } } @media screen and (min-width:1200px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; + .c2.c2.c2.c2.c2 { + width: 264px; } } @media screen and (min-width:768px) { - .c7.c7.c7.c7.c7 { + .c4.c4.c4.c4.c4 { padding-right: 80px; - margin-left: 12px; } } @media screen and (min-width:320px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:480px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:768px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1024px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1200px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } -@media screen and (min-width:320px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:480px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:768px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1024px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1200px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - @@ -724,244 +515,173 @@ exports[` should render TopNav ssr 2`] = ` - - - - + + + + Home + + + + + + Payroll + + + + + + + Payments + + + + + - - - - + Magic Checkout + + - - - - + More - - - - - Payroll - - - - - - - Payments - - - - - - - Magic Checkout - - - - - - - - - - - - - - - should render TopNav ssr 2`] = ` should render TopNav ssr 2`] = ` AH @@ -1052,44 +772,6 @@ exports[` should render TopNav ssr 2`] = ` - - - - - - - - - - - diff --git a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.web.test.tsx.snap b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.web.test.tsx.snap index 42e0731dfa6..9fe67227bfd 100644 --- a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.web.test.tsx.snap +++ b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.web.test.tsx.snap @@ -14,10 +14,12 @@ exports[`TopNav should render 1`] = ` align-items: center; position: -webkit-sticky; position: sticky; - z-index: 1; - grid-template-columns: minmax(0,1fr) auto; - padding-right: 8px; - padding-left: 8px; + z-index: 100; + grid-template-columns: auto minmax(0,1fr) auto; + padding-top: 8px; + padding-bottom: 8px; + padding-right: 12px; + padding-left: 12px; height: 56px; width: 100%; top: 0px; @@ -25,7 +27,6 @@ exports[`TopNav should render 1`] = ` } .c2.c2.c2.c2.c2 { - display: none; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -39,23 +40,6 @@ exports[`TopNav should render 1`] = ` } .c4.c4.c4.c4.c4 { - display: none; - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; -} - -.c5.c5.c5.c5.c5 { - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; - margin-right: -1px; - height: 20px; - border-left-color: hsla(211,20%,52%,0.18); - border-left-style: solid; -} - -.c7.c7.c7.c7.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -64,11 +48,13 @@ exports[`TopNav should render 1`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; padding-right: 0px; - margin-left: 0px; } -.c8.c8.c8.c8.c8 { +.c5.c5.c5.c5.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -77,82 +63,54 @@ exports[`TopNav should render 1`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; position: relative; - margin-bottom: -12px; width: 100%; } -.c12.c12.c12.c12.c12 { +.c6.c6.c6.c6.c6 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -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; - z-index: 1; + position: relative; + width: 100%; } -.c14.c14.c14.c14.c14 { +.c7.c7.c7.c7.c7 { 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-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; } -.c15.c15.c15.c15.c15 { +.c8.c8.c8.c8.c8 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - overflow-x: auto; - overflow-y: hidden; - white-space: nowrap; position: relative; width: 100%; gap: 0px; + left: -1px; } -.c17.c17.c17.c17.c17 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - width: -webkit-max-content; - width: -moz-max-content; - width: max-content; -} - -.c21.c21.c21.c21.c21 { +.c12.c12.c12.c12.c12 { margin: auto; height: 16px; border-left-color: hsla(211,20%,52%,0.18); border-left-style: solid; } -.c24.c24.c24.c24.c24 { +.c14.c14.c14.c14.c14 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -161,25 +119,25 @@ exports[`TopNav should render 1`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; + padding: 8px; margin-top: 2px; gap: 8px; -} - -.c26.c26.c26.c26.c26 { background-color: hsla(0,0%,100%,1); + border-top-left-radius: 4px; + border-top-right-radius: 4px; } -.c28.c28.c28.c28.c28 { - position: relative; - height: 100%; - width: 100%; -} - -.c30.c30.c30.c30.c30 { +.c17.c17.c17.c17.c17 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -192,252 +150,81 @@ exports[`TopNav should render 1`] = ` -ms-flex-pack: center; justify-content: center; z-index: 1; - height: 100%; -} - -.c32.c32.c32.c32.c32 { - position: absolute; - top: 0px; - left: 0px; - pointer-events: none; } -.c10.c10.c10.c10.c10 { - min-height: 28px; - height: 28px; - width: 28px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; +.c19.c19.c19.c19.c19 { + 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-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c10.c10.c10.c10.c10:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c10.c10.c10.c10.c10 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c25.c25.c25.c25.c25 { - min-height: 36px; - height: 36px; - width: 36px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c25.c25.c25.c25.c25:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c25.c25.c25.c25.c25 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c11.c11.c11.c11.c11 { - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); - -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); - transition-duration: cubic-bezier(0.3,0,0.2,1); - -webkit-transition-timing-function: 150px; - transition-timing-function: 150px; -} - -.c31.c31.c31.c31.c31 { - color: hsla(211,33%,21%,1); - font-family: "Inter","Inter Fallback Arial",Arial; - font-size: 0.75rem; - font-weight: 600; - 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; - margin: 0; - padding: 0; -} - -.c13.c13.c13.c13.c13 { - opacity: 1; } -.c6.c6.c6.c6.c6 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 20px; +.c20.c20.c20.c20.c20 { + background-color: hsla(0,0%,100%,1); } .c22.c22.c22.c22.c22 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 16px; -} - -.c16.c16.c16.c16.c16::-webkit-scrollbar { - display: none; + position: relative; + height: 100%; + width: 100%; } -.c9.c9.c9.c9.c9 { - position: absolute; - left: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; +.c24.c24.c24.c24.c24 { + 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; z-index: 1; + height: 100%; } - -.c9.c9.c9.c9.c9:before { - content: ''; - pointer-events: none; - position: absolute; - left: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to left,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); -} - -.c23.c23.c23.c23.c23 { - position: absolute; - right: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; - z-index: 1; + +.c13.c13.c13.c13.c13 { + border-width: 0; + border-left-style: solid; + border-left-width: 1px; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + height: 16px; } -.c23.c23.c23.c23.c23:before { - content: ''; - pointer-events: none; - position: absolute; - right: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to right,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); +.c25.c25.c25.c25.c25 { + color: hsla(211,33%,21%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.75rem; + font-weight: 600; + 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; + margin: 0; + padding: 0; } -.c20.c20.c20.c20.c20 { +.c11.c11.c11.c11.c11 { color: hsla(211,26%,34%,1); font-family: "Inter","Inter Fallback Arial",Arial; font-size: 0.875rem; @@ -477,13 +264,19 @@ exports[`TopNav should render 1`] = ` padding-left: 12px; padding-right: 12px; border-radius: 4px; + border: none; + background: none; } -.c20.c20.c20.c20.c20:hover { +.c11.c11.c11.c11.c11[aria-expanded="true"] { background-color: hsla(211,20%,52%,0.12); } -.c18.c18.c18.c18.c18 { +.c11.c11.c11.c11.c11:hover { + background-color: hsla(211,20%,52%,0.12); +} + +.c9.c9.c9.c9.c9 { position: relative; -webkit-flex-shrink: 0; -ms-flex-negative: 0; @@ -492,21 +285,17 @@ exports[`TopNav should render 1`] = ` background-color: transparent; border-color: transparent; border-style: solid; - border-bottom-width: 0; border-width: 1px; + border-bottom-width: 0; border-top-left-radius: 4px; border-top-right-radius: 4px; - -webkit-transform: none; - -ms-transform: none; - transform: none; -webkit-transition: 250ms cubic-bezier(0.3,0,0.2,1); transition: 250ms cubic-bezier(0.3,0,0.2,1); - -webkit-transition-property: background,-webkit-transform; - -webkit-transition-property: background,transform; - transition-property: background,transform; + -webkit-transition-property: background; + transition-property: background; } -.c19.c19.c19.c19.c19 { +.c10.c10.c10.c10.c10 { position: absolute; top: 0; left: 0; @@ -523,7 +312,7 @@ exports[`TopNav should render 1`] = ` transition-property: opacity; } -.c27.c27.c27.c27.c27 { +.c21.c21.c21.c21.c21 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -534,7 +323,7 @@ exports[`TopNav should render 1`] = ` outline: 0.5px solid hsla(214,28%,84%,1); } -.c29.c29.c29.c29.c29 { +.c23.c23.c23.c23.c23 { display: block; text-align: center; -webkit-text-decoration: none; @@ -548,7 +337,7 @@ exports[`TopNav should render 1`] = ` background-color: hsla(211,20%,52%,0.18); } -.c29.c29.c29.c29.c29 img { +.c23.c23.c23.c23.c23 img { display: block; height: 36px; width: 36px; @@ -556,130 +345,132 @@ exports[`TopNav should render 1`] = ` object-fit: cover; } -@media screen and (min-width:768px) { - .c1.c1.c1.c1.c1 { - grid-template-columns: auto minmax(0,1fr) auto; - } +.c15.c15.c15.c15.c15 { + min-height: 36px; + height: 36px; + width: 36px; + cursor: pointer; + background-color: hsla(211,20%,52%,0.12); + border-color: hsla(214,28%,84%,1); + border-width: 0px; + border-radius: 4px; + border-style: solid; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 0px; + padding-right: 0px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-transition-property: background-color,border-color,box-shadow; + transition-property: background-color,border-color,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: 150ms; + transition-duration: 150ms; + position: relative; } -@media screen and (min-width:768px) { - .c2.c2.c2.c2.c2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } +.c15.c15.c15.c15.c15:hover { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:1200px) { - .c2.c2.c2.c2.c2 { - width: 264px; - } +.c15.c15.c15.c15.c15:active { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:768px) { - .c4.c4.c4.c4.c4 { - display: block; - } +.c15.c15.c15.c15.c15:focus-visible { + background-color: hsla(211,20%,52%,0.18); + outline: 1px solid hsla(227,100%,59%,0.09); + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); } -@media screen and (min-width:320px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c15.c15.c15.c15.c15 * { + -webkit-transition-property: color,fill,opacity; + transition-property: color,fill,opacity; + -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); } -@media screen and (min-width:480px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c16.c16.c16.c16.c16 { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); + transition-duration: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: 150px; + transition-timing-function: 150px; } -@media screen and (min-width:768px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c18.c18.c18.c18.c18 { + opacity: 1; } -@media screen and (min-width:1024px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; +@media screen and (min-width:768px) { + .c1.c1.c1.c1.c1 { + padding-top: 0px; + padding-bottom: 0px; + padding-right: 8px; + padding-left: 8px; } } @media screen and (min-width:1200px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; + .c2.c2.c2.c2.c2 { + width: 264px; } } @media screen and (min-width:768px) { - .c7.c7.c7.c7.c7 { + .c4.c4.c4.c4.c4 { padding-right: 80px; - margin-left: 12px; } } @media screen and (min-width:320px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:480px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:768px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1024px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1200px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } -@media screen and (min-width:320px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:480px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:768px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1024px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1200px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - - - - - + + + + Home + + + + + + Payroll + + + + + + + Payments + + + + + - - - - + Magic Checkout + + - - - - + More - - - - - Payroll - - - - - - - Payments - - - - - - - Magic Checkout - - - - - - - - - - - - - - - AH @@ -1048,44 +768,6 @@ exports[`TopNav should render 1`] = ` - - - - - - - - - - - diff --git a/packages/blade/src/components/TopNav/docs/TabNav.stories.tsx b/packages/blade/src/components/TopNav/docs/TabNav.stories.tsx index e8cdebc32f4..7ee8d6e1cbb 100644 --- a/packages/blade/src/components/TopNav/docs/TabNav.stories.tsx +++ b/packages/blade/src/components/TopNav/docs/TabNav.stories.tsx @@ -1,19 +1,28 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import React from 'react'; import type { StoryFn, Meta } from '@storybook/react'; import type { TabNavItemProps } from '../TabNav'; -import { TabNav, TabNavItem } from '../TabNav'; +import { TabNavItems, TabNav, TabNavItem } from '../TabNav'; import { tabNavExample } from './code'; import { Box } from '~components/Box'; import iconMap from '~components/Icons/iconMap'; -import { ChevronDownIcon, ChevronRightIcon, HomeIcon } from '~components/Icons'; -import { Menu, MenuFooter, MenuHeader, MenuItem, MenuOverlay } from '~components/Menu'; +import { + AcceptPaymentsIcon, + AwardIcon, + ChevronDownIcon, + HomeIcon, + ShoppingBagIcon, +} from '~components/Icons'; +import { Menu, MenuItem, MenuOverlay } from '~components/Menu'; import { Badge } from '~components/Badge'; import { Link } from '~components/Link'; import { Code, Text } from '~components/Typography'; import { Sandbox } from '~utils/storybook/Sandbox'; import StoryPageWrapper from '~utils/storybook/StoryPageWrapper'; +import { List, ListItem, ListItemCode } from '~components/List'; +import { Alert } from '~components/Alert'; const DocsPage = (): React.ReactElement => { return ( @@ -31,38 +40,86 @@ const trailingMapping = { 'NEW': NEW, }; +const propsCategory = { + TAB_NAV_ITEM: 'TabNavItem Props', + ITEM_DATA: 'Extra props for "item" data', +}; + export default { title: 'Components/TopNav/TabNav', component: TabNavItem, argTypes: { + title: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + href: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + target: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + accessibilityLabel: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + as: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, icon: { name: 'icon', type: 'select', options: Object.keys(iconMap), - mapping: iconMap, + table: { category: propsCategory.TAB_NAV_ITEM }, } as unknown, trailing: { name: 'trailing', type: 'select', options: Object.keys(trailingMapping), - mapping: trailingMapping, + table: { category: propsCategory.TAB_NAV_ITEM }, } as unknown, + isAlwaysOverflowing: { + type: 'boolean', + table: { category: propsCategory.ITEM_DATA }, + }, + isActive: { + type: 'boolean', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + description: { + type: 'string', + table: { category: propsCategory.ITEM_DATA }, + }, onClick: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, onKeyDown: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, onKeyUp: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, onMouseDown: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, onPointerDown: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, }, + args: { + title: 'Payroll', + description: 'Manage payroll effortlessly.', + isAlwaysOverflowing: false, + isActive: false, + }, tags: ['autodocs'], parameters: { docs: { @@ -71,121 +128,149 @@ export default { }, } as Meta; -const TabNavTemplate: StoryFn = (args) => { - return ( - - - - - {args.children} - - Payments - Magic Checkout - - - ); -}; +const TabNavTemplate: StoryFn = ( + args: TabNavItemProps & { + isAlwaysOverflowing: boolean; + description: string; + }, +) => { + const icon = iconMap[(args.icon as unknown) as keyof typeof iconMap]; + const trailing = trailingMapping[(args.trailing as unknown) as keyof typeof trailingMapping]; -const TabNavTemplateWithMenu: StoryFn = (args) => { return ( - - - You can compose TabNav with Menu component to create a dropdown - menus within TabNav. - - - Each TabNavItem component can be wrapped with Menu component to - achieve this. - - - - - - {args.children} - - Payments - Magic Checkout - - }> - Explore - - - - Recommended - - } - /> - - - Payroll - - - - - Payout - - - - - View all products - - - - - - - ); -}; + + TabNav component provides a flexible way for you to build tabs which automatically handles + responsiveness and overflows as screen size reduces + -const TabNavTemplateOverFlowing: StoryFn = (args) => { - return ( - - - - If there are more TabNavItems than we can fit in the available space, the TabNav will - become horizontally scrollable with left/right arrow buttons. - - - - - - - {args.children} - - Payments - Magic Checkout - Item 1 - Item 2 - Item 3 - Item 4 - Item 5 - + + TabNav component takes in an array of items and gives you the flexibility of + the rendering via a render prop + + + + The render prop exposes two arrays: + + + items - an array of items that fit in the available space + + + overflowingItems - an array of items that overflow the + available space + + + + + You can map over these arrays and render the TabNavItem component for each item + or for the overflowing items you can render a Menu component to create a + dropdown "More" menu. + + + + + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + const Icon = item.icon; + return ( + { + console.log('clicked', item.title); + }} + > + + + {Icon && } + {item.title} + + + {item.description} + + + + ); + })} + + + ) : null} + > + ); + }} + ); }; export const TabNavExample = TabNavTemplate.bind({}); TabNavExample.args = { - children: 'Payroll', + title: 'Payroll', isActive: true, }; TabNavExample.storyName = 'TabNavExample'; - -export const WithMenu = TabNavTemplateWithMenu.bind({}); -WithMenu.args = { - children: 'Payroll', - isActive: true, -}; -WithMenu.storyName = 'With Menu'; - -export const OverFlowing = TabNavTemplateOverFlowing.bind({}); -OverFlowing.args = { - children: 'Payroll', - isActive: true, -}; -OverFlowing.storyName = 'Overflowing'; diff --git a/packages/blade/src/components/TopNav/docs/TopNav.stories.tsx b/packages/blade/src/components/TopNav/docs/TopNav.stories.tsx index 80ec9f96558..ac7bc589435 100644 --- a/packages/blade/src/components/TopNav/docs/TopNav.stories.tsx +++ b/packages/blade/src/components/TopNav/docs/TopNav.stories.tsx @@ -1,14 +1,14 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import React from 'react'; import type { StoryFn, Meta } from '@storybook/react'; import { Link, matchPath, useHistory, useLocation } from 'react-router-dom'; import storyRouterDecorator from 'storybook-react-router'; import { Title } from '@storybook/addon-docs'; +import styled from 'styled-components'; import type { TopNavProps } from '../TopNav'; import { TopNav, TopNavActions, TopNavContent, TopNavBrand } from '../TopNav'; import type { TabNavItemProps } from '../TabNav'; -import { TabNav, TabNavItem } from '../TabNav'; +import { TabNavItems, TabNav, TabNavItem } from '../TabNav'; import { topNavFullExample } from './code'; import { Box } from '~components/Box'; import type { SideNavLinkProps, SideNavProps } from '~components/SideNav'; @@ -21,14 +21,16 @@ import { } from '~components/SideNav'; import type { IconComponent } from '~components/Icons'; import { + SearchIcon, + AcceptPaymentsIcon, + AwardIcon, + ShoppingBagIcon, ChevronDownIcon, ActivityIcon, AnnouncementIcon, - BulkPayoutsIcon, ChevronRightIcon, HomeIcon, LayoutIcon, - MenuIcon, PaymentButtonIcon, PaymentGatewayIcon, PaymentLinkIcon, @@ -40,8 +42,7 @@ import { SearchInput } from '~components/Input/SearchInput'; import { Button } from '~components/Button'; import { Tooltip } from '~components/Tooltip'; import { Avatar } from '~components/Avatar'; -import { useIsMobile } from '~utils/useIsMobile'; -import { Text } from '~components/Typography'; +import { Heading, Text } from '~components/Typography'; import { Menu, MenuFooter, MenuHeader, MenuItem, MenuOverlay } from '~components/Menu'; import { Link as BladeLink } from '~components/Link'; import { Badge } from '~components/Badge'; @@ -50,7 +51,7 @@ import StoryPageWrapper from '~utils/storybook/StoryPageWrapper'; import { Alert } from '~components/Alert'; import { List, ListItem } from '~components/List'; -import { makeSize } from '~utils'; +import { makeSize, useBreakpoint, useTheme } from '~utils'; import { SIDE_NAV_EXPANDED_L1_WIDTH_XL, SIDE_NAV_EXPANDED_L1_WIDTH_BASE, @@ -190,7 +191,7 @@ const ExploreItem = ({ description, }: { icon: IconComponent; - title: string; + title?: string; description: string; }): React.ReactElement => { return ( @@ -214,134 +215,226 @@ const ExploreItem = ({ ); }; +const DashboardBackground = styled.div(() => { + return { + height: '100vh', + background: 'radial-gradient(94.74% 64.44% at 29.03% 15.17%, #FFFFFF 0%, #90A5BB 100%)', + }; +}); + const TopNavFullExample = () => { - const isMobile = useIsMobile(); const history = useHistory(); + const { theme } = useTheme(); + const { matchedBreakpoint, matchedDeviceType } = useBreakpoint({ + breakpoints: theme.breakpoints, + }); + const isTablet = matchedBreakpoint === 'm'; + const isMobile = matchedDeviceType === 'mobile'; const [isSideBarOpen, setIsSideBarOpen] = React.useState(false); const [selectedProduct, setSelectedProduct] = React.useState(null); + const activeUrl = useLocation().pathname; + React.useEffect(() => { + setSelectedProduct(activeUrl); + }, [activeUrl]); + return ( - + - {/* TopNavBrand gets hidden on mobile */} - - - - - {/* Desktop - render TabNav */} - - - Payroll - Payments - Magic Checkout - - }> - {selectedProduct ? `Explore: ${selectedProduct}` : 'Explore'} - + {isMobile ? ( + <> + + Home + + + Payments + + + - - Recommended - - } - /> - { - history.push('/explore/payroll'); - setSelectedProduct('Payroll'); - }} - > - + + + + + + John Doe + + + Razorpay Trusted Merchant + + + + + Settings - { - history.push('/explore/payouts'); - setSelectedProduct('Payout'); - }} - > - + + Logout - - - View all products - - - - {/* Mobile - render hamburger button */} - - { - setIsSideBarOpen(!isSideBarOpen); - }} - /> - Home - - - - {/* Remove searchbar on mobile */} - - - - - - - - - - - - - - - - - - John Doe - - - Razorpay Trusted Merchant - - - - - Settings - - - Logout - - - - + > + ) : ( + <> + + + + + + {({ items, overflowingItems }) => { + const activeProduct = overflowingItems.find( + (item) => item.href === selectedProduct, + ); + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } + isActive={Boolean(activeProduct)} + /> + + + Recommended + + } + /> + {overflowingItems.map((item) => { + return ( + { + history.push(item.href!); + setSelectedProduct(item.href!); + }} + > + + + ); + })} + + + View all products + + + + + ) : null} + > + ); + }} + + + + {isTablet ? ( + + + + ) : ( + + )} + + + + + + + + + + + + + + + John Doe + + + Razorpay Trusted Merchant + + + + + Settings + + + Logout + + + + + > + )} { backgroundColor="surface.background.gray.intense" > - This demo integrates: + Active URL: {activeUrl} + This demo integrates: SideNav Menu (Explore Tab) @@ -382,25 +476,115 @@ const TopNavFullExample = () => { - + ); }; const TopNavFullTemplate: StoryFn = () => ; const TopNavMinimalTemplate: StoryFn = () => { + const history = useHistory(); + const [selectedProduct, setSelectedProduct] = React.useState(null); + return ( - + - - - Payroll - Payments - Magic Checkout + + {({ items, overflowingItems }) => { + const activeProduct = overflowingItems.find( + (item) => item.href === selectedProduct, + ); + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } + isActive={Boolean(activeProduct)} + /> + + + Recommended + + } + /> + {overflowingItems.map((item) => { + return ( + { + history.push(item.href!); + setSelectedProduct(item.href!); + }} + > + + + ); + })} + + + View all products + + + + + ) : null} + > + ); + }} @@ -417,14 +601,15 @@ const TopNavMinimalTemplate: StoryFn = () => { - - - This is a minimal example usage of TopNav, checkout Full Dashboard Layout example for - other features & integration details. - - - + + + + This is a minimal example usage of TopNav, checkout Full Dashboard Layout example for + other features & integration details. + + + ); }; diff --git a/packages/blade/src/components/TopNav/docs/code.ts b/packages/blade/src/components/TopNav/docs/code.ts index 1fb305f150d..afe02c34914 100644 --- a/packages/blade/src/components/TopNav/docs/code.ts +++ b/packages/blade/src/components/TopNav/docs/code.ts @@ -22,6 +22,7 @@ export const topNavFullExample = { import { Box, Text, + Heading, TopNav, TopNavBrand, TopNavContent, @@ -80,7 +81,7 @@ export const topNavFullExample = { description, }: { icon: IconComponent; - title: string; + title?: string; description: string; }): React.ReactElement => { return ( @@ -104,6 +105,13 @@ export const topNavFullExample = { ); }; + const DashboardBackground = styled.div(() => { + return { + height: '100vh', + background: 'radial-gradient(94.74% 64.44% at 29.03% 15.17%, #FFFFFF 0%, #90A5BB 100%)', + }; + }); + const TopNavExample = (): React.ReactElement => { const { platform } = useTheme(); const history = useHistory(); @@ -111,145 +119,239 @@ export const topNavFullExample = { const [isSideBarOpen, setIsSideBarOpen] = React.useState(false); const [selectedProduct, setSelectedProduct] = React.useState(null); + const activeUrl = useLocation().pathname; + React.useEffect(() => { + setSelectedProduct(activeUrl); + }, [activeUrl]); + return ( - - - {/* TopNavBrand automatically gets hidden on mobile */} - - - - - {/* Desktop - render TabNav */} - - - Payroll - Payments - Magic Checkout - - - {selectedProduct ? \`Explore: \${selectedProduct}\` : 'Explore'} - - - - Recommended - - } - /> - { - history.push('/explore/payroll'); - setSelectedProduct('Payroll'); - }} + + + + {isMobile ? ( + <> + + Home + + + Payments + + + + + + + + + + John Doe + + + Razorpay Trusted Merchant + + + + + Settings + + + Logout + + + + > + ) : ( + <> + + + + + - - - { - history.push('/explore/payouts'); - setSelectedProduct('Payout'); + {({ items, overflowingItems }) => { + const activeProduct = overflowingItems.find( + (item) => item.href === selectedProduct, + ); + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } + isActive={Boolean(activeProduct)} + /> + + + Recommended + + } + /> + {overflowingItems.map((item) => { + return ( + { + history.push(item.href!); + setSelectedProduct(item.href!); + }} + > + + + ); + })} + + + View all products + + + + + ) : null} + > + ); }} - > - + + + + + - - - - View all products - - - - - - {/* Mobile - render hamburger button */} - - { - setIsSideBarOpen(!isSideBarOpen); - }} - /> - Home - - - - {/* Remove searchbar on mobile */} - - - - - - - - - - - - - - { - setIsSideBarOpen(false); - }} - /> + + + + + + + + + + + + + John Doe + + + Razorpay Trusted Merchant + + + + + Settings + + + Logout + + + + + > + )} + + { + setIsSideBarOpen(false); + }} + /> - - This demo integrates: - - SideNav - Menu (Explore Tab) - ReactRouter - Mobile Responsiveness - One Dashboard Layout - + + + This demo integrates: + + SideNav + Menu (Explore Tab) + ReactRouter + Mobile Responsiveness + One Dashboard Layout + + - + ); }; @@ -381,16 +483,107 @@ export const topNavFullExample = { export const tabNavExample = { 'App.tsx': dedent`import React from 'react'; - import { Box, TabNav, TabNavItem } from '@razorpay/blade/components'; + import { + Box, + TabNav, + TabNavItem, + Text, + HomeIcon, + RazorpayxPayrollIcon, + AcceptPaymentsIcon, + MagicCheckoutIcon, + AwardIcon, + ChevronDownIcon, + Menu, + MenuItem, + MenuOverlay, + } from '@razorpay/blade/components'; const App = () => { return ( - - - Payroll - Payments - Magic Checkout + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + const Icon = item.icon; + return ( + { + console.log('clicked', item.title); + }} + > + + + {Icon && } + {item.title} + + + {item.description} + + + + ); + })} + + + ) : null} + > + ); + }} ); diff --git a/packages/blade/src/tokens/global/size.ts b/packages/blade/src/tokens/global/size.ts index 241c5aa07c6..e8a4550bb31 100644 --- a/packages/blade/src/tokens/global/size.ts +++ b/packages/blade/src/tokens/global/size.ts @@ -76,6 +76,8 @@ export const size = { 176: 176, /** 200 px */ 200: 200, + /** 208 px */ + 208: 208, /** 240 px */ 240: 240, /** 245 px */ diff --git a/packages/blade/src/utils/componentZIndices.ts b/packages/blade/src/utils/componentZIndices.ts index 6afeb89cf8a..ca600d0019f 100644 --- a/packages/blade/src/utils/componentZIndices.ts +++ b/packages/blade/src/utils/componentZIndices.ts @@ -7,4 +7,5 @@ export const componentZIndices = { tourMask: 1100, popover: 1100, tooltip: 1100, + topnav: 100, };
+
+ Header Title +
- Header Title + Subtitle
- Subtitle -
Custom Slot
Account @@ -789,7 +798,7 @@ exports[`Menu renders a Menu 1`] = ` data-blade-component="box" >
Accounts @@ -797,23 +806,23 @@ exports[`Menu renders a Menu 1`] = `
Profile @@ -860,35 +869,35 @@ exports[`Menu renders a Menu 1`] = ` />
Settings @@ -900,11 +909,11 @@ exports[`Menu renders a Menu 1`] = ` />
Cmd + S @@ -915,30 +924,30 @@ exports[`Menu renders a Menu 1`] = ` Share @@ -950,7 +959,7 @@ exports[`Menu renders a Menu 1`] = ` /> Log Out @@ -1008,26 +1017,26 @@ exports[`Menu renders a Menu 1`] = ` /> Footer slot diff --git a/packages/blade/src/components/Menu/docs/Menu.stories.tsx b/packages/blade/src/components/Menu/docs/Menu.stories.tsx index 72e7964ab1f..bd83739f3f0 100644 --- a/packages/blade/src/components/Menu/docs/Menu.stories.tsx +++ b/packages/blade/src/components/Menu/docs/Menu.stories.tsx @@ -170,7 +170,7 @@ type TemplateProps = MenuProps & { trigger: React.ReactElement }; const accountsMenuOverlayContent = ( <> } /> - + Razorpay Pvt Ltd @@ -182,7 +182,7 @@ const accountsMenuOverlayContent = ( Switch Merchant - + } diff --git a/packages/blade/src/components/Menu/types.ts b/packages/blade/src/components/Menu/types.ts index 44b81a90c12..caf8a4ef12c 100644 --- a/packages/blade/src/components/Menu/types.ts +++ b/packages/blade/src/components/Menu/types.ts @@ -114,7 +114,7 @@ type MenuOverlayProps = { /** * JSX Slot for MenuItem or anything else */ - children: React.ReactElement[] | React.ReactElement; + children: React.ReactElement[] | React.ReactElement | React.ReactNode; /** * zIndex override diff --git a/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx b/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx index c9b1b93d265..6c89d280429 100644 --- a/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx +++ b/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx @@ -1,181 +1,116 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable consistent-return */ import React from 'react'; -import styled from 'styled-components'; -import { useTopNavContext } from '../TopNavContext'; -import { approximatelyEqual, MIXED_BG_COLOR, useHasOverflow } from './utils'; +import ReactDOM from 'react-dom'; import { TabNavContext } from './TabNavContext'; +import { useResize } from './utils'; +import type { TabNavItemData, TabNavProps } from './types'; import BaseBox from '~components/Box/BaseBox'; import type { StyledPropsBlade } from '~components/Box/styledProps'; import { getStyledProps } from '~components/Box/styledProps'; -import { Button } from '~components/Button'; import { Divider } from '~components/Divider'; -import { ChevronLeftIcon, ChevronRightIcon } from '~components/Icons'; -import { makeMotionTime, makeSize } from '~utils'; +import { makeSize } from '~utils'; import { size } from '~tokens/global'; -import getIn from '~utils/lodashButBetter/get'; -import type { BoxProps } from '~components/Box'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import type { BoxProps } from '~components/Box'; +import { Box } from '~components/Box'; -const GRADIENT_WIDTH = 54 as const; -const GRADIENT_OFFSET = -8 as const; -const OFFSET_BOTTOM = -12 as const; -const SCROLL_AMOUNT = 200; - -type TabNavProps = { - children: React.ReactNode; +const TabNavItems = ({ children, ...props }: BoxProps): React.ReactElement => { + return ( + + {React.Children.map(children, (child, index) => { + return ( + <> + {index > 0 ? ( + + ) : null} + {React.cloneElement(child as React.ReactElement, { + __isInsideTabNavItems: true, + __index: index, + })} + > + ); + })} + + + ); }; -const ScrollableArea = styled(BaseBox)(() => { - return { - '&::-webkit-scrollbar': { display: 'none' }, - }; -}); - -const GradientOverlay = styled(BaseBox)<{ - shouldShow?: boolean; - variant: 'left' | 'right'; - $color: BoxProps['backgroundColor']; -}>(({ theme, shouldShow, variant, $color }) => { - const color = getIn(theme.colors, $color as never, MIXED_BG_COLOR); - - return { - position: 'absolute', - [variant]: 0, - pointerEvents: shouldShow ? 'auto' : 'none', - transform: shouldShow ? 'scale(1)' : 'scale(0.5)', - opacity: shouldShow ? 1 : 0, - transitionTimingFunction: `${theme.motion.easing.standard.revealing}`, - transitionDuration: `${makeMotionTime(theme.motion.duration.xquick)}`, - transitionProperty: 'opacity, transform', - zIndex: 1, - ':before': { - content: "''", - pointerEvents: 'none', - position: 'absolute', - [variant]: 0, - top: makeSize(GRADIENT_OFFSET), - bottom: makeSize(GRADIENT_OFFSET), - width: makeSize(GRADIENT_WIDTH), - background: `linear-gradient(to ${variant}, transparent 0%, ${color} 30%, ${color} 100%);`, - }, - }; -}); - const TabNav = ({ children, + items, ...styledProps }: TabNavProps & StyledPropsBlade): React.ReactElement => { const ref = React.useRef(null); - const hasOverflow = useHasOverflow(ref); - const [scrollStatus, setScrollStatus] = React.useState<'start' | 'end' | 'middle'>('start'); - const { backgroundColor } = useTopNavContext(); + const [controlledItems, setControlledItems] = React.useState(items); - // Check if the scroll is at start, end or middle - const handleScrollStatus = React.useCallback( - (e: React.UIEvent): void => { - const target = e.target as HTMLDivElement; - const isAtStart = target.scrollLeft === 0; - const isAtEnd = approximatelyEqual( - target.scrollLeft, - target.scrollWidth - target.offsetWidth, - ); - - if (isAtStart) { - setScrollStatus('start'); - } else if (isAtEnd) { - setScrollStatus('end'); - } else { - setScrollStatus('middle'); - } - }, - [], + const overflowingItems = controlledItems.filter( + (item) => item.isAlwaysOverflowing ?? item.isOverflowing, ); + const _items = controlledItems.filter((item) => !item.isAlwaysOverflowing && !item.isOverflowing); - const scrollRight = (): void => { - if (!ref.current) return; - ref.current.scrollBy({ - behavior: 'smooth', - left: SCROLL_AMOUNT, - }); - }; + // We need to memoize this callback otherwise it will cause infinite re-renders + // Because the ResizeObserver callback will be a new reference on every render + // and it will trigger a re-render + const resizeCallback = React.useCallback((resizeInfo: ResizeObserverEntry): void => { + const target = resizeInfo.target as HTMLElement; + const updateItems = (): void => { + setControlledItems((items) => { + return items.map((item, index) => { + // never overflow the first item + if (index === 0) return { ...item, isOverflowing: false }; + // add padding to the offsetX to account the "More" menu's width changing due to the selection state (eg: More:ProdctName) + // Currently, hardcoding this to 150, we can make this dynamic too but that's causing layout thrashing + // because first we need to calculate the width of the "More" menu and then update the items + const padding = 150; + const offset = (item.offsetX! + padding)! - target.getBoundingClientRect().left; + if (offset > target.offsetWidth) { + return { ...item, isOverflowing: true }; + } else { + return { ...item, isOverflowing: false }; + } + }); + }); + }; + // https://github.com/webpack/webpack/issues/14814 + const flushSync = (ReactDOM as any)['flushSync'.toString()]; + // Using flushSync to avoid layout thrashing, + // this will force React to flush all pending updates and only then update the DOM + if (flushSync !== undefined) { + flushSync(updateItems); + } else { + updateItems(); + } + }, []); - const scrollLeft = (): void => { - if (!ref.current) return; - ref.current.scrollBy({ - behavior: 'smooth', - left: -SCROLL_AMOUNT, - }); - }; + useResize(ref, resizeCallback); return ( - + - - - - + - {React.Children.map(children, (child, index) => { - return ( - <> - {index > 0 ? ( - - ) : null} - {child} - > - ); - })} + {children({ items: _items, overflowingItems })} - - - - + ); }; -export { TabNav }; +export { TabNav, TabNavItems }; diff --git a/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx b/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx index 5cca5072449..a5a11a368a3 100644 --- a/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx +++ b/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx @@ -1,9 +1,11 @@ import React from 'react'; +import type { TabNavItemData } from './types'; import { throwBladeError } from '~utils/logger'; type TabNavContextProps = { containerRef: React.RefObject; - hasOverflow: boolean; + controlledItems: TabNavItemData[]; + setControlledItems: React.Dispatch>; }; const TabNavContext = React.createContext(null); diff --git a/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx b/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx index 966649f5859..f911864bc2a 100644 --- a/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx +++ b/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable consistent-return */ import React from 'react'; import styled from 'styled-components'; -import { useTopNavContext } from '../TopNavContext'; import type { TabNavItemProps } from './types'; import { useTabNavContext } from './TabNavContext'; import { MIXED_BG_COLOR } from './utils'; @@ -10,11 +12,8 @@ import { makeBorderSize, makeMotionTime, makeSize, makeSpace } from '~utils'; import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects'; import { makeAccessible } from '~utils/makeAccessible'; import { size } from '~tokens/global'; -import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; -import { mergeRefs } from '~utils/useMergeRefs'; -import type { BoxProps } from '~components/Box'; -import getIn from '~utils/lodashButBetter/get'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; const StyledTabNavItem = styled.a<{ $isActive?: boolean }>(({ theme, $isActive }) => { return { @@ -37,6 +36,14 @@ const StyledTabNavItem = styled.a<{ $isActive?: boolean }>(({ theme, $isActive } paddingLeft: makeSpace(theme.spacing[4]), paddingRight: makeSpace(theme.spacing[4]), borderRadius: makeBorderSize(theme.border.radius.medium), + // reset button styles + border: 'none', + background: 'none', + '&[aria-expanded="true"]': $isActive + ? {} + : { + backgroundColor: theme.colors.interactive.background.gray.default, + }, '&:hover': $isActive ? {} : { @@ -47,16 +54,15 @@ const StyledTabNavItem = styled.a<{ $isActive?: boolean }>(({ theme, $isActive } const StyledTabNavItemWrapper = styled(BaseBox)<{ isActive?: boolean; - dividerHiderColor: BoxProps['backgroundColor']; -}>(({ theme, isActive, dividerHiderColor }) => { +}>(({ theme, isActive }) => { const dividerHiderStyle = { content: '""', position: 'absolute', top: '50%', transform: 'translateY(-50%)', width: makeSize(size[1]), - height: '50%', - backgroundColor: getIn(theme.colors, dividerHiderColor as never, MIXED_BG_COLOR), + height: makeSize(size[16]), + backgroundColor: MIXED_BG_COLOR, } as const; return { @@ -66,16 +72,14 @@ const StyledTabNavItemWrapper = styled(BaseBox)<{ backgroundColor: isActive ? theme.colors.surface.background.gray.intense : 'transparent', borderColor: isActive ? theme.colors.surface.border.gray.muted : 'transparent', borderStyle: 'solid', - borderBottomWidth: 0, borderWidth: makeBorderSize(theme.border.width.thin), + borderBottomWidth: 0, borderTopLeftRadius: makeBorderSize(theme.border.radius.medium), borderTopRightRadius: makeBorderSize(theme.border.radius.medium), - // Animation - transform: isActive ? `translateY(${makeSize(size[2])})` : 'none', transition: `${makeMotionTime(theme.motion.duration.moderate)} ${ theme.motion.easing.standard.effective }`, - transitionProperty: 'background, transform', + transitionProperty: 'background', // Hide the left and right divider by overlaying them with a pseudo element as same color as the background ...(isActive @@ -113,47 +117,55 @@ const SelectedBar = styled(BaseBox)<{ isActive?: boolean }>(({ theme, isActive } }); const _TabNavItem: React.ForwardRefRenderFunction = ( - { as, children, isActive, icon: Icon, trailing, accessibilityLabel, href, target, ...props }, + { + as, + title, + isActive, + icon: Icon, + trailing, + accessibilityLabel, + href, + target, + // @ts-expect-error - This prop is only used internally + __isInsideTabNavItems, + // @ts-expect-error - This prop is only used internally + __index, + ...props + }, ref, ): React.ReactElement => { - const { containerRef, hasOverflow } = useTabNavContext(); - const { backgroundColor } = useTopNavContext(); - const linkRef = React.useRef(null); + const { setControlledItems } = useTabNavContext(); + const bodyRef = React.useRef(null); - // Scroll the active tab into view - // Only if the tab is very close to the edge - // Or if the tab is out of view + // Update the controlledItems with the tabWidth and offsetX useIsomorphicLayoutEffect(() => { - if (!isActive || !hasOverflow) return; - if (!('requestAnimationFrame' in window)) return; - - window.requestAnimationFrame(() => { - if (!linkRef.current || !containerRef.current) return; - - const buffer = 100; - const container = containerRef.current; - const linkElement = linkRef.current; - const containerRect = container.getBoundingClientRect(); - const linkRect = linkElement.getBoundingClientRect(); - const isCloseToStart = linkRect.left < containerRect.left + buffer; - const isCloseToEnd = linkRect.right > containerRect.right - buffer; - - if (isCloseToStart || isCloseToEnd) { - linkElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); - } + if (!bodyRef.current) return; + if (!__isInsideTabNavItems) return; + setControlledItems((prev) => { + return prev.map((item, index) => { + if (index !== __index) return item; + const bounds = bodyRef?.current?.getBoundingClientRect()!; + const tabWidth = bounds.width; + const offsetX = bounds.right; + return { + ...item, + tabWidth, + offsetX, + }; + }); }); - }, [hasOverflow, isActive]); + }, [__isInsideTabNavItems, __index, setControlledItems]); return ( ) : null} - {children} + {title} {trailing ? trailing : null} diff --git a/packages/blade/src/components/TopNav/TabNav/types.ts b/packages/blade/src/components/TopNav/TabNav/types.ts index e0d38fc59ab..646e28677df 100644 --- a/packages/blade/src/components/TopNav/TabNav/types.ts +++ b/packages/blade/src/components/TopNav/TabNav/types.ts @@ -32,7 +32,7 @@ type TabNavItemProps = { * ``` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - as?: React.ComponentType; + as?: React.ComponentType | 'a' | 'button'; /** * Selected state of the navigation item. * @@ -52,15 +52,30 @@ type TabNavItemProps = { */ trailing?: React.ReactElement; /** - * Element to render inside the navigation item. - * - * This can either be a string or JSX element (eg: Menu component) + * Title of the navigation item. */ - children?: React.ReactNode; + title?: string; /** * Accessibility label for the navigation item. */ accessibilityLabel?: string; } & MenuTriggerProps; -export type { TabNavItemProps }; +type Item = TabNavItemProps & { + description?: string; + isAlwaysOverflowing?: boolean; +}; +type TabNavItemData = Item & { + isOverflowing?: boolean; + tabWidth?: number; + offsetX?: number; +}; +type TabNavProps = { + items: Item[]; + children: (props: { + items: TabNavItemData[]; + overflowingItems: TabNavItemData[]; + }) => React.ReactElement; +}; + +export type { TabNavItemProps, TabNavItemData, TabNavProps }; diff --git a/packages/blade/src/components/TopNav/TabNav/utils.ts b/packages/blade/src/components/TopNav/TabNav/utils.ts index dd25928377c..5a39973c858 100644 --- a/packages/blade/src/components/TopNav/TabNav/utils.ts +++ b/packages/blade/src/components/TopNav/TabNav/utils.ts @@ -1,51 +1,38 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable consistent-return */ -import React from 'react'; +import type React from 'react'; import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; /** - * Check if an element has scroll overflow + * Hook to observe resize events on a given element */ -const useHasOverflow = ( +const useResize = ( ref: React.RefObject, - callback?: (hasOverflow: boolean) => void, -): boolean => { - const observer = React.useRef(null); - const [hasOverflow, setHasOverflow] = React.useState(false); - + callback?: (entry: ResizeObserverEntry) => void, +) => { useIsomorphicLayoutEffect(() => { if (!ref.current) return; const element = ref.current; - const trigger = (): void => { - const hasOverflow = element.scrollWidth > element.clientWidth; - setHasOverflow(hasOverflow); - - if (callback) callback(hasOverflow); - }; + if (!('ResizeObserver' in window)) return; - trigger(); - if ('ResizeObserver' in window) { - observer.current = new ResizeObserver(trigger); - observer.current.observe(element); - } + const observer = new ResizeObserver((entries) => { + entries.forEach((entry) => { + callback?.(entry); + }); + }); + observer.observe(element); // destroy the observer return (): void => { - if ('ResizeObserver' in window) { - observer.current?.disconnect(); - } + if (!('ResizeObserver' in window)) return; + observer?.disconnect(); }; - }, [callback, ref]); - - return hasOverflow; -}; - -const approximatelyEqual = (v1: number, v2: number, tolerance = 1): boolean => { - return Math.abs(v1 - v2) < tolerance; + }, [callback]); }; // Overlapping color of surface.background.gray.subtle + interactive.background.gray.default // TODO(future): design will tokenize or check if this is needed or not const MIXED_BG_COLOR = '#e1e7ef'; -export { useHasOverflow, approximatelyEqual, MIXED_BG_COLOR }; +export { useResize, MIXED_BG_COLOR }; diff --git a/packages/blade/src/components/TopNav/TopNav.web.tsx b/packages/blade/src/components/TopNav/TopNav.web.tsx index 11ee8dfa4c6..2ca8cf962ce 100644 --- a/packages/blade/src/components/TopNav/TopNav.web.tsx +++ b/packages/blade/src/components/TopNav/TopNav.web.tsx @@ -1,9 +1,7 @@ import React from 'react'; -import { TopNavContext } from './TopNavContext'; import type { BoxProps } from '~components/Box'; import { Box } from '~components/Box'; import BaseBox from '~components/Box/BaseBox'; -import { Divider } from '~components/Divider'; import { SIDE_NAV_EXPANDED_L1_WIDTH_XL, SIDE_NAV_EXPANDED_L1_WIDTH_BASE, @@ -11,42 +9,12 @@ import { import { size } from '~tokens/global'; import { makeSize } from '~utils'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import type { StyledPropsBlade } from '~components/Box/styledProps'; +import { componentZIndices } from '~utils/componentZIndices'; const TOP_NAV_HEIGHT = size[56]; const CONTENT_RIGHT_GAP = size[80]; -const RazorpayLinesSvg = (): React.ReactElement => { - return ( - - - - - - - - - - ); -}; - type TopNavProps = { children: React.ReactNode; } & Pick< @@ -66,40 +34,33 @@ type TopNavProps = { | 'right' | 'width' | 'zIndex' ->; +> & + StyledPropsBlade; -const TopNav = ({ children, ...styledProps }: TopNavProps): React.ReactElement => { +const TopNav = ({ children, ...boxProps }: TopNavProps): React.ReactElement => { return ( - - - {children} - - - - - + + {children} + ); }; const TopNavBrand = ({ children }: { children: React.ReactNode }): React.ReactElement => { return ( {children} - - - ); }; @@ -128,7 +81,7 @@ const TopNavContent = ({ children }: { children: React.ReactNode }): React.React @@ -140,10 +93,15 @@ const TopNavContent = ({ children }: { children: React.ReactNode }): React.React const TopNavActions = ({ children }: { children: React.ReactNode }): React.ReactElement => { return ( {children} diff --git a/packages/blade/src/components/TopNav/TopNavContext.tsx b/packages/blade/src/components/TopNav/TopNavContext.tsx deleted file mode 100644 index c9e01ae14e6..00000000000 --- a/packages/blade/src/components/TopNav/TopNavContext.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { MIXED_BG_COLOR } from './TabNav/utils'; -import type { BoxProps } from '~components/Box'; - -type TopNavContextProps = { - backgroundColor: BoxProps['backgroundColor']; -}; -const TopNavContext = React.createContext({ - backgroundColor: MIXED_BG_COLOR as never, -}); - -const useTopNavContext = (): TopNavContextProps => { - const context = React.useContext(TopNavContext); - return context!; -}; - -export { TopNavContext, useTopNavContext }; diff --git a/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx b/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx index 7a8be71ae64..4d56947df54 100644 --- a/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx +++ b/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx @@ -3,22 +3,102 @@ import type { StoryFn } from '@storybook/react'; import { within, userEvent } from '@storybook/testing-library'; import { expect } from '@storybook/jest'; import React from 'react'; -import { TabNav, TabNavItem } from '../TabNav'; -import { Box } from '~components/Box'; -import { HomeIcon } from '~components/Icons'; +import type { TabNavProps } from '../TabNav'; +import { TabNav, TabNavItem, TabNavItems } from '../TabNav'; +import { + AcceptPaymentsIcon, + AwardIcon, + ChevronDownIcon, + HomeIcon, + MagicCheckoutIcon, + RazorpayxPayrollIcon, +} from '~components/Icons'; +import { Menu, MenuItem, MenuOverlay } from '~components/Menu'; +import { Text } from '~components/Typography'; const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); -export const TestBasicTabNav: StoryFn = (): React.ReactElement => { +const TabNavExample = ({ items }: { items?: TabNavProps['items'] }): React.ReactElement => { return ( - - - Payroll - Payments + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + return ( + { + console.log('clicked', item.title); + }} + > + {item.title} + + ); + })} + + + ) : null} + > + ); + }} ); }; +export const TestBasicTabNav: StoryFn = (): React.ReactElement => { + return ; +}; + TestBasicTabNav.play = async ({ canvasElement }) => { const { getByRole } = within(canvasElement); const homeTab = getByRole('link', { name: 'Home' }); @@ -30,45 +110,90 @@ TestBasicTabNav.play = async ({ canvasElement }) => { }; export const TestOverflow: StoryFn = (): React.ReactElement => { + return ; +}; + +TestOverflow.play = async ({ canvasElement }) => { + const { getByRole, queryByRole } = within(document.body); + canvasElement.style.width = '100%'; + + await sleep(500); + const homeTab = getByRole('link', { name: 'Home' }); + const payrollTab = getByRole('link', { name: 'Payroll' }); + const paymentsTab = getByRole('link', { name: 'Payments' }); + await expect(homeTab).toBeVisible(); + await expect(payrollTab).toBeVisible(); + await expect(paymentsTab).toBeVisible(); + + await sleep(500); + + // reduce the width of the canvas to make the tabs overflow + canvasElement.style.width = '600px'; + await sleep(500); + + const moreTab = getByRole('button', { name: 'More' }); + await userEvent.hover(moreTab); + await sleep(500); + await expect(queryByRole('menu', { name: 'More' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Rize' })).toBeVisible(); + await expect(queryByRole('link', { name: 'Magic Checkout' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Magic Checkout' })).toBeVisible(); + + canvasElement.style.width = '300px'; + await sleep(500); + await expect(queryByRole('link', { name: 'Payroll' })).toBeNull(); + await expect(queryByRole('link', { name: 'Payments' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Payroll' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payments' })).toBeVisible(); + + canvasElement.style.width = '100%'; + await sleep(500); + await expect(queryByRole('menuitem', { name: 'Rize' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payroll' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Payments' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Magic Checkout' })).toBeNull(); +}; + +export const ShouldNotShowMore: StoryFn = (): React.ReactElement => { return ( - - - Item 1 - Item 2 - Item 3 - Item 4 - Item 5 - Item 6 - Item 7 - Item 8 - Item 9 - - + ); }; -TestOverflow.play = async ({ canvasElement }) => { - const { getByRole } = within(canvasElement); +ShouldNotShowMore.play = async ({ canvasElement }) => { + const { getByRole, queryByRole } = within(document.body); + + await sleep(500); + const homeTab = getByRole('link', { name: 'Home' }); + const payrollTab = getByRole('link', { name: 'Payroll' }); + const paymentsTab = getByRole('link', { name: 'Payments' }); + await expect(homeTab).toBeVisible(); + await expect(payrollTab).toBeVisible(); + await expect(paymentsTab).toBeVisible(); + await expect(queryByRole('button', { name: 'More' })).toBeNull(); + await sleep(500); - const item1 = getByRole('link', { name: 'Item 1' }); - const scrollLeftButton = getByRole('button', { name: /Scroll Left/ }); - const scrollRightButton = getByRole('button', { name: /Scroll Right/ }); - await expect(scrollLeftButton).not.toBeVisible(); - - await expect(item1).toBeVisible(); - await expect(scrollRightButton).toBeVisible(); - // scroll - await userEvent.click(scrollRightButton); + + // reduce the width of the canvas to make the tabs overflow + canvasElement.style.width = '200px'; await sleep(500); - await expect(scrollLeftButton).toBeVisible(); - await expect(scrollRightButton).toBeVisible(); + const moreTab = getByRole('button', { name: 'More' }); + await expect(moreTab).toBeVisible(); - // scroll to end - await userEvent.click(scrollRightButton); + // hover over the more tab + await userEvent.hover(moreTab); await sleep(500); - await expect(scrollLeftButton).toBeVisible(); - await expect(scrollRightButton).not.toBeVisible(); + + await expect(queryByRole('menu', { name: 'More' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payments' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payroll' })).toBeVisible(); }; export default { diff --git a/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx b/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx index f30336d1bb6..bf5e19371a7 100644 --- a/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx +++ b/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx @@ -1,11 +1,13 @@ -import { TabNav, TabNavItem } from '../TabNav'; +import { TabNav, TabNavItem, TabNavItems } from '../TabNav'; import { TopNav, TopNavActions, TopNavBrand, TopNavContent } from '../TopNav'; import { Avatar } from '~components/Avatar'; import { Box } from '~components/Box'; import { Button } from '~components/Button'; -import { ActivityIcon, AnnouncementIcon, HomeIcon } from '~components/Icons'; +import { ActivityIcon, AnnouncementIcon, ChevronDownIcon } from '~components/Icons'; import { RazorpayLogo } from '~components/SideNav/docs/RazorpayLogo'; import { Tooltip } from '~components/Tooltip'; +import { Menu, MenuItem, MenuOverlay } from '~components/Menu'; +import { Text } from '~components/Typography'; const TopNavExample = (): React.ReactElement => { return ( @@ -15,11 +17,67 @@ const TopNavExample = (): React.ReactElement => { - - - Payroll - Payments - Magic Checkout + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + return ( + { + console.log('clicked', item.title); + }} + > + {item.title} + + ); + })} + + + ) : null} + > + ); + }} diff --git a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap index e393fa4a3e2..0d44ec52691 100644 --- a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap +++ b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should render TopNav ssr 1`] = `"PayrollPaymentsMagic CheckoutAH"`; +exports[` should render TopNav ssr 1`] = `"HomePayrollPaymentsMagic CheckoutMoreAH"`; exports[` should render TopNav ssr 2`] = ` .c0.c0.c0.c0.c0 { @@ -16,10 +16,12 @@ exports[` should render TopNav ssr 2`] = ` align-items: center; position: -webkit-sticky; position: sticky; - z-index: 1; - grid-template-columns: minmax(0,1fr) auto; - padding-right: 8px; - padding-left: 8px; + z-index: 100; + grid-template-columns: auto minmax(0,1fr) auto; + padding-top: 8px; + padding-bottom: 8px; + padding-right: 12px; + padding-left: 12px; height: 56px; width: 100%; top: 0px; @@ -27,7 +29,6 @@ exports[` should render TopNav ssr 2`] = ` } .c2.c2.c2.c2.c2 { - display: none; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -41,23 +42,6 @@ exports[` should render TopNav ssr 2`] = ` } .c4.c4.c4.c4.c4 { - display: none; - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; -} - -.c5.c5.c5.c5.c5 { - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; - margin-right: -1px; - height: 20px; - border-left-color: hsla(211,20%,52%,0.18); - border-left-style: solid; -} - -.c7.c7.c7.c7.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -66,11 +50,13 @@ exports[` should render TopNav ssr 2`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; padding-right: 0px; - margin-left: 0px; } -.c8.c8.c8.c8.c8 { +.c5.c5.c5.c5.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -79,82 +65,54 @@ exports[` should render TopNav ssr 2`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; position: relative; - margin-bottom: -12px; width: 100%; } -.c12.c12.c12.c12.c12 { +.c6.c6.c6.c6.c6 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -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; - z-index: 1; + position: relative; + width: 100%; } -.c14.c14.c14.c14.c14 { +.c7.c7.c7.c7.c7 { 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-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; } -.c15.c15.c15.c15.c15 { +.c8.c8.c8.c8.c8 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - overflow-x: auto; - overflow-y: hidden; - white-space: nowrap; position: relative; width: 100%; gap: 0px; + left: -1px; } -.c17.c17.c17.c17.c17 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - width: -webkit-max-content; - width: -moz-max-content; - width: max-content; -} - -.c21.c21.c21.c21.c21 { +.c12.c12.c12.c12.c12 { margin: auto; height: 16px; border-left-color: hsla(211,20%,52%,0.18); border-left-style: solid; } -.c24.c24.c24.c24.c24 { +.c14.c14.c14.c14.c14 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -163,25 +121,25 @@ exports[` should render TopNav ssr 2`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; + padding: 8px; margin-top: 2px; gap: 8px; -} - -.c26.c26.c26.c26.c26 { background-color: hsla(0,0%,100%,1); + border-top-left-radius: 4px; + border-top-right-radius: 4px; } -.c28.c28.c28.c28.c28 { - position: relative; - height: 100%; - width: 100%; -} - -.c30.c30.c30.c30.c30 { +.c17.c17.c17.c17.c17 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -194,252 +152,81 @@ exports[` should render TopNav ssr 2`] = ` -ms-flex-pack: center; justify-content: center; z-index: 1; - height: 100%; -} - -.c32.c32.c32.c32.c32 { - position: absolute; - top: 0px; - left: 0px; - pointer-events: none; } -.c10.c10.c10.c10.c10 { - min-height: 28px; - height: 28px; - width: 28px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; +.c19.c19.c19.c19.c19 { + 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-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c10.c10.c10.c10.c10:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c10.c10.c10.c10.c10 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c25.c25.c25.c25.c25 { - min-height: 36px; - height: 36px; - width: 36px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c25.c25.c25.c25.c25:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c25.c25.c25.c25.c25 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c11.c11.c11.c11.c11 { - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); - -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); - transition-duration: cubic-bezier(0.3,0,0.2,1); - -webkit-transition-timing-function: 150px; - transition-timing-function: 150px; -} - -.c31.c31.c31.c31.c31 { - color: hsla(211,33%,21%,1); - font-family: "Inter","Inter Fallback Arial",Arial; - font-size: 0.75rem; - font-weight: 600; - 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; - margin: 0; - padding: 0; -} - -.c13.c13.c13.c13.c13 { - opacity: 1; } -.c6.c6.c6.c6.c6 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 20px; +.c20.c20.c20.c20.c20 { + background-color: hsla(0,0%,100%,1); } .c22.c22.c22.c22.c22 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 16px; -} - -.c16.c16.c16.c16.c16::-webkit-scrollbar { - display: none; + position: relative; + height: 100%; + width: 100%; } -.c9.c9.c9.c9.c9 { - position: absolute; - left: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; +.c24.c24.c24.c24.c24 { + 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; z-index: 1; + height: 100%; } - -.c9.c9.c9.c9.c9:before { - content: ''; - pointer-events: none; - position: absolute; - left: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to left,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); -} - -.c23.c23.c23.c23.c23 { - position: absolute; - right: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; - z-index: 1; + +.c13.c13.c13.c13.c13 { + border-width: 0; + border-left-style: solid; + border-left-width: 1px; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + height: 16px; } -.c23.c23.c23.c23.c23:before { - content: ''; - pointer-events: none; - position: absolute; - right: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to right,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); +.c25.c25.c25.c25.c25 { + color: hsla(211,33%,21%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.75rem; + font-weight: 600; + 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; + margin: 0; + padding: 0; } -.c20.c20.c20.c20.c20 { +.c11.c11.c11.c11.c11 { color: hsla(211,26%,34%,1); font-family: "Inter","Inter Fallback Arial",Arial; font-size: 0.875rem; @@ -479,13 +266,19 @@ exports[` should render TopNav ssr 2`] = ` padding-left: 12px; padding-right: 12px; border-radius: 4px; + border: none; + background: none; } -.c20.c20.c20.c20.c20:hover { +.c11.c11.c11.c11.c11[aria-expanded="true"] { background-color: hsla(211,20%,52%,0.12); } -.c18.c18.c18.c18.c18 { +.c11.c11.c11.c11.c11:hover { + background-color: hsla(211,20%,52%,0.12); +} + +.c9.c9.c9.c9.c9 { position: relative; -webkit-flex-shrink: 0; -ms-flex-negative: 0; @@ -494,21 +287,17 @@ exports[` should render TopNav ssr 2`] = ` background-color: transparent; border-color: transparent; border-style: solid; - border-bottom-width: 0; border-width: 1px; + border-bottom-width: 0; border-top-left-radius: 4px; border-top-right-radius: 4px; - -webkit-transform: none; - -ms-transform: none; - transform: none; -webkit-transition: 250ms cubic-bezier(0.3,0,0.2,1); transition: 250ms cubic-bezier(0.3,0,0.2,1); - -webkit-transition-property: background,-webkit-transform; - -webkit-transition-property: background,transform; - transition-property: background,transform; + -webkit-transition-property: background; + transition-property: background; } -.c19.c19.c19.c19.c19 { +.c10.c10.c10.c10.c10 { position: absolute; top: 0; left: 0; @@ -525,7 +314,7 @@ exports[` should render TopNav ssr 2`] = ` transition-property: opacity; } -.c27.c27.c27.c27.c27 { +.c21.c21.c21.c21.c21 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -536,7 +325,7 @@ exports[` should render TopNav ssr 2`] = ` outline: 0.5px solid hsla(214,28%,84%,1); } -.c29.c29.c29.c29.c29 { +.c23.c23.c23.c23.c23 { display: block; text-align: center; -webkit-text-decoration: none; @@ -550,7 +339,7 @@ exports[` should render TopNav ssr 2`] = ` background-color: hsla(211,20%,52%,0.18); } -.c29.c29.c29.c29.c29 img { +.c23.c23.c23.c23.c23 img { display: block; height: 36px; width: 36px; @@ -558,130 +347,132 @@ exports[` should render TopNav ssr 2`] = ` object-fit: cover; } -@media screen and (min-width:768px) { - .c1.c1.c1.c1.c1 { - grid-template-columns: auto minmax(0,1fr) auto; - } +.c15.c15.c15.c15.c15 { + min-height: 36px; + height: 36px; + width: 36px; + cursor: pointer; + background-color: hsla(211,20%,52%,0.12); + border-color: hsla(214,28%,84%,1); + border-width: 0px; + border-radius: 4px; + border-style: solid; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 0px; + padding-right: 0px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-transition-property: background-color,border-color,box-shadow; + transition-property: background-color,border-color,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: 150ms; + transition-duration: 150ms; + position: relative; } -@media screen and (min-width:768px) { - .c2.c2.c2.c2.c2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } +.c15.c15.c15.c15.c15:hover { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:1200px) { - .c2.c2.c2.c2.c2 { - width: 264px; - } +.c15.c15.c15.c15.c15:active { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:768px) { - .c4.c4.c4.c4.c4 { - display: block; - } +.c15.c15.c15.c15.c15:focus-visible { + background-color: hsla(211,20%,52%,0.18); + outline: 1px solid hsla(227,100%,59%,0.09); + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); } -@media screen and (min-width:320px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c15.c15.c15.c15.c15 * { + -webkit-transition-property: color,fill,opacity; + transition-property: color,fill,opacity; + -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); } -@media screen and (min-width:480px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c16.c16.c16.c16.c16 { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); + transition-duration: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: 150px; + transition-timing-function: 150px; } -@media screen and (min-width:768px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c18.c18.c18.c18.c18 { + opacity: 1; } -@media screen and (min-width:1024px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; +@media screen and (min-width:768px) { + .c1.c1.c1.c1.c1 { + padding-top: 0px; + padding-bottom: 0px; + padding-right: 8px; + padding-left: 8px; } } @media screen and (min-width:1200px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; + .c2.c2.c2.c2.c2 { + width: 264px; } } @media screen and (min-width:768px) { - .c7.c7.c7.c7.c7 { + .c4.c4.c4.c4.c4 { padding-right: 80px; - margin-left: 12px; } } @media screen and (min-width:320px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:480px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:768px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1024px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1200px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } -@media screen and (min-width:320px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:480px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:768px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1024px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1200px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - @@ -724,244 +515,173 @@ exports[` should render TopNav ssr 2`] = ` - - - - + + + + Home + + + + + + Payroll + + + + + + + Payments + + + + + - - - - + Magic Checkout + + - - - - + More - - - - - Payroll - - - - - - - Payments - - - - - - - Magic Checkout - - - - - - - - - - - - - - - should render TopNav ssr 2`] = ` should render TopNav ssr 2`] = ` AH @@ -1052,44 +772,6 @@ exports[` should render TopNav ssr 2`] = ` - - - - - - - - - - - diff --git a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.web.test.tsx.snap b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.web.test.tsx.snap index 42e0731dfa6..9fe67227bfd 100644 --- a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.web.test.tsx.snap +++ b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.web.test.tsx.snap @@ -14,10 +14,12 @@ exports[`TopNav should render 1`] = ` align-items: center; position: -webkit-sticky; position: sticky; - z-index: 1; - grid-template-columns: minmax(0,1fr) auto; - padding-right: 8px; - padding-left: 8px; + z-index: 100; + grid-template-columns: auto minmax(0,1fr) auto; + padding-top: 8px; + padding-bottom: 8px; + padding-right: 12px; + padding-left: 12px; height: 56px; width: 100%; top: 0px; @@ -25,7 +27,6 @@ exports[`TopNav should render 1`] = ` } .c2.c2.c2.c2.c2 { - display: none; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -39,23 +40,6 @@ exports[`TopNav should render 1`] = ` } .c4.c4.c4.c4.c4 { - display: none; - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; -} - -.c5.c5.c5.c5.c5 { - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; - margin-right: -1px; - height: 20px; - border-left-color: hsla(211,20%,52%,0.18); - border-left-style: solid; -} - -.c7.c7.c7.c7.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -64,11 +48,13 @@ exports[`TopNav should render 1`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; padding-right: 0px; - margin-left: 0px; } -.c8.c8.c8.c8.c8 { +.c5.c5.c5.c5.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -77,82 +63,54 @@ exports[`TopNav should render 1`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; position: relative; - margin-bottom: -12px; width: 100%; } -.c12.c12.c12.c12.c12 { +.c6.c6.c6.c6.c6 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -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; - z-index: 1; + position: relative; + width: 100%; } -.c14.c14.c14.c14.c14 { +.c7.c7.c7.c7.c7 { 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-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; } -.c15.c15.c15.c15.c15 { +.c8.c8.c8.c8.c8 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - overflow-x: auto; - overflow-y: hidden; - white-space: nowrap; position: relative; width: 100%; gap: 0px; + left: -1px; } -.c17.c17.c17.c17.c17 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - width: -webkit-max-content; - width: -moz-max-content; - width: max-content; -} - -.c21.c21.c21.c21.c21 { +.c12.c12.c12.c12.c12 { margin: auto; height: 16px; border-left-color: hsla(211,20%,52%,0.18); border-left-style: solid; } -.c24.c24.c24.c24.c24 { +.c14.c14.c14.c14.c14 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -161,25 +119,25 @@ exports[`TopNav should render 1`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; + padding: 8px; margin-top: 2px; gap: 8px; -} - -.c26.c26.c26.c26.c26 { background-color: hsla(0,0%,100%,1); + border-top-left-radius: 4px; + border-top-right-radius: 4px; } -.c28.c28.c28.c28.c28 { - position: relative; - height: 100%; - width: 100%; -} - -.c30.c30.c30.c30.c30 { +.c17.c17.c17.c17.c17 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -192,252 +150,81 @@ exports[`TopNav should render 1`] = ` -ms-flex-pack: center; justify-content: center; z-index: 1; - height: 100%; -} - -.c32.c32.c32.c32.c32 { - position: absolute; - top: 0px; - left: 0px; - pointer-events: none; } -.c10.c10.c10.c10.c10 { - min-height: 28px; - height: 28px; - width: 28px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; +.c19.c19.c19.c19.c19 { + 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-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c10.c10.c10.c10.c10:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c10.c10.c10.c10.c10 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c25.c25.c25.c25.c25 { - min-height: 36px; - height: 36px; - width: 36px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c25.c25.c25.c25.c25:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c25.c25.c25.c25.c25 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c11.c11.c11.c11.c11 { - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); - -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); - transition-duration: cubic-bezier(0.3,0,0.2,1); - -webkit-transition-timing-function: 150px; - transition-timing-function: 150px; -} - -.c31.c31.c31.c31.c31 { - color: hsla(211,33%,21%,1); - font-family: "Inter","Inter Fallback Arial",Arial; - font-size: 0.75rem; - font-weight: 600; - 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; - margin: 0; - padding: 0; -} - -.c13.c13.c13.c13.c13 { - opacity: 1; } -.c6.c6.c6.c6.c6 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 20px; +.c20.c20.c20.c20.c20 { + background-color: hsla(0,0%,100%,1); } .c22.c22.c22.c22.c22 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 16px; -} - -.c16.c16.c16.c16.c16::-webkit-scrollbar { - display: none; + position: relative; + height: 100%; + width: 100%; } -.c9.c9.c9.c9.c9 { - position: absolute; - left: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; +.c24.c24.c24.c24.c24 { + 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; z-index: 1; + height: 100%; } - -.c9.c9.c9.c9.c9:before { - content: ''; - pointer-events: none; - position: absolute; - left: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to left,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); -} - -.c23.c23.c23.c23.c23 { - position: absolute; - right: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; - z-index: 1; + +.c13.c13.c13.c13.c13 { + border-width: 0; + border-left-style: solid; + border-left-width: 1px; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + height: 16px; } -.c23.c23.c23.c23.c23:before { - content: ''; - pointer-events: none; - position: absolute; - right: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to right,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); +.c25.c25.c25.c25.c25 { + color: hsla(211,33%,21%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.75rem; + font-weight: 600; + 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; + margin: 0; + padding: 0; } -.c20.c20.c20.c20.c20 { +.c11.c11.c11.c11.c11 { color: hsla(211,26%,34%,1); font-family: "Inter","Inter Fallback Arial",Arial; font-size: 0.875rem; @@ -477,13 +264,19 @@ exports[`TopNav should render 1`] = ` padding-left: 12px; padding-right: 12px; border-radius: 4px; + border: none; + background: none; } -.c20.c20.c20.c20.c20:hover { +.c11.c11.c11.c11.c11[aria-expanded="true"] { background-color: hsla(211,20%,52%,0.12); } -.c18.c18.c18.c18.c18 { +.c11.c11.c11.c11.c11:hover { + background-color: hsla(211,20%,52%,0.12); +} + +.c9.c9.c9.c9.c9 { position: relative; -webkit-flex-shrink: 0; -ms-flex-negative: 0; @@ -492,21 +285,17 @@ exports[`TopNav should render 1`] = ` background-color: transparent; border-color: transparent; border-style: solid; - border-bottom-width: 0; border-width: 1px; + border-bottom-width: 0; border-top-left-radius: 4px; border-top-right-radius: 4px; - -webkit-transform: none; - -ms-transform: none; - transform: none; -webkit-transition: 250ms cubic-bezier(0.3,0,0.2,1); transition: 250ms cubic-bezier(0.3,0,0.2,1); - -webkit-transition-property: background,-webkit-transform; - -webkit-transition-property: background,transform; - transition-property: background,transform; + -webkit-transition-property: background; + transition-property: background; } -.c19.c19.c19.c19.c19 { +.c10.c10.c10.c10.c10 { position: absolute; top: 0; left: 0; @@ -523,7 +312,7 @@ exports[`TopNav should render 1`] = ` transition-property: opacity; } -.c27.c27.c27.c27.c27 { +.c21.c21.c21.c21.c21 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -534,7 +323,7 @@ exports[`TopNav should render 1`] = ` outline: 0.5px solid hsla(214,28%,84%,1); } -.c29.c29.c29.c29.c29 { +.c23.c23.c23.c23.c23 { display: block; text-align: center; -webkit-text-decoration: none; @@ -548,7 +337,7 @@ exports[`TopNav should render 1`] = ` background-color: hsla(211,20%,52%,0.18); } -.c29.c29.c29.c29.c29 img { +.c23.c23.c23.c23.c23 img { display: block; height: 36px; width: 36px; @@ -556,130 +345,132 @@ exports[`TopNav should render 1`] = ` object-fit: cover; } -@media screen and (min-width:768px) { - .c1.c1.c1.c1.c1 { - grid-template-columns: auto minmax(0,1fr) auto; - } +.c15.c15.c15.c15.c15 { + min-height: 36px; + height: 36px; + width: 36px; + cursor: pointer; + background-color: hsla(211,20%,52%,0.12); + border-color: hsla(214,28%,84%,1); + border-width: 0px; + border-radius: 4px; + border-style: solid; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 0px; + padding-right: 0px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-transition-property: background-color,border-color,box-shadow; + transition-property: background-color,border-color,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: 150ms; + transition-duration: 150ms; + position: relative; } -@media screen and (min-width:768px) { - .c2.c2.c2.c2.c2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } +.c15.c15.c15.c15.c15:hover { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:1200px) { - .c2.c2.c2.c2.c2 { - width: 264px; - } +.c15.c15.c15.c15.c15:active { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:768px) { - .c4.c4.c4.c4.c4 { - display: block; - } +.c15.c15.c15.c15.c15:focus-visible { + background-color: hsla(211,20%,52%,0.18); + outline: 1px solid hsla(227,100%,59%,0.09); + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); } -@media screen and (min-width:320px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c15.c15.c15.c15.c15 * { + -webkit-transition-property: color,fill,opacity; + transition-property: color,fill,opacity; + -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); } -@media screen and (min-width:480px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c16.c16.c16.c16.c16 { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); + transition-duration: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: 150px; + transition-timing-function: 150px; } -@media screen and (min-width:768px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c18.c18.c18.c18.c18 { + opacity: 1; } -@media screen and (min-width:1024px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; +@media screen and (min-width:768px) { + .c1.c1.c1.c1.c1 { + padding-top: 0px; + padding-bottom: 0px; + padding-right: 8px; + padding-left: 8px; } } @media screen and (min-width:1200px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; + .c2.c2.c2.c2.c2 { + width: 264px; } } @media screen and (min-width:768px) { - .c7.c7.c7.c7.c7 { + .c4.c4.c4.c4.c4 { padding-right: 80px; - margin-left: 12px; } } @media screen and (min-width:320px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:480px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:768px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1024px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1200px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } -@media screen and (min-width:320px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:480px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:768px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1024px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1200px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - - - - - + + + + Home + + + + + + Payroll + + + + + + + Payments + + + + + - - - - + Magic Checkout + + - - - - + More - - - - - Payroll - - - - - - - Payments - - - - - - - Magic Checkout - - - - - - - - - - - - - - - AH @@ -1048,44 +768,6 @@ exports[`TopNav should render 1`] = ` - - - - - - - - - - - diff --git a/packages/blade/src/components/TopNav/docs/TabNav.stories.tsx b/packages/blade/src/components/TopNav/docs/TabNav.stories.tsx index e8cdebc32f4..7ee8d6e1cbb 100644 --- a/packages/blade/src/components/TopNav/docs/TabNav.stories.tsx +++ b/packages/blade/src/components/TopNav/docs/TabNav.stories.tsx @@ -1,19 +1,28 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import React from 'react'; import type { StoryFn, Meta } from '@storybook/react'; import type { TabNavItemProps } from '../TabNav'; -import { TabNav, TabNavItem } from '../TabNav'; +import { TabNavItems, TabNav, TabNavItem } from '../TabNav'; import { tabNavExample } from './code'; import { Box } from '~components/Box'; import iconMap from '~components/Icons/iconMap'; -import { ChevronDownIcon, ChevronRightIcon, HomeIcon } from '~components/Icons'; -import { Menu, MenuFooter, MenuHeader, MenuItem, MenuOverlay } from '~components/Menu'; +import { + AcceptPaymentsIcon, + AwardIcon, + ChevronDownIcon, + HomeIcon, + ShoppingBagIcon, +} from '~components/Icons'; +import { Menu, MenuItem, MenuOverlay } from '~components/Menu'; import { Badge } from '~components/Badge'; import { Link } from '~components/Link'; import { Code, Text } from '~components/Typography'; import { Sandbox } from '~utils/storybook/Sandbox'; import StoryPageWrapper from '~utils/storybook/StoryPageWrapper'; +import { List, ListItem, ListItemCode } from '~components/List'; +import { Alert } from '~components/Alert'; const DocsPage = (): React.ReactElement => { return ( @@ -31,38 +40,86 @@ const trailingMapping = { 'NEW': NEW, }; +const propsCategory = { + TAB_NAV_ITEM: 'TabNavItem Props', + ITEM_DATA: 'Extra props for "item" data', +}; + export default { title: 'Components/TopNav/TabNav', component: TabNavItem, argTypes: { + title: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + href: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + target: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + accessibilityLabel: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + as: { + type: 'string', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, icon: { name: 'icon', type: 'select', options: Object.keys(iconMap), - mapping: iconMap, + table: { category: propsCategory.TAB_NAV_ITEM }, } as unknown, trailing: { name: 'trailing', type: 'select', options: Object.keys(trailingMapping), - mapping: trailingMapping, + table: { category: propsCategory.TAB_NAV_ITEM }, } as unknown, + isAlwaysOverflowing: { + type: 'boolean', + table: { category: propsCategory.ITEM_DATA }, + }, + isActive: { + type: 'boolean', + table: { category: propsCategory.TAB_NAV_ITEM }, + }, + description: { + type: 'string', + table: { category: propsCategory.ITEM_DATA }, + }, onClick: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, onKeyDown: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, onKeyUp: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, onMouseDown: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, onPointerDown: { type: 'function', + table: { category: propsCategory.TAB_NAV_ITEM }, }, }, + args: { + title: 'Payroll', + description: 'Manage payroll effortlessly.', + isAlwaysOverflowing: false, + isActive: false, + }, tags: ['autodocs'], parameters: { docs: { @@ -71,121 +128,149 @@ export default { }, } as Meta; -const TabNavTemplate: StoryFn = (args) => { - return ( - - - - - {args.children} - - Payments - Magic Checkout - - - ); -}; +const TabNavTemplate: StoryFn = ( + args: TabNavItemProps & { + isAlwaysOverflowing: boolean; + description: string; + }, +) => { + const icon = iconMap[(args.icon as unknown) as keyof typeof iconMap]; + const trailing = trailingMapping[(args.trailing as unknown) as keyof typeof trailingMapping]; -const TabNavTemplateWithMenu: StoryFn = (args) => { return ( - - - You can compose TabNav with Menu component to create a dropdown - menus within TabNav. - - - Each TabNavItem component can be wrapped with Menu component to - achieve this. - - - - - - {args.children} - - Payments - Magic Checkout - - }> - Explore - - - - Recommended - - } - /> - - - Payroll - - - - - Payout - - - - - View all products - - - - - - - ); -}; + + TabNav component provides a flexible way for you to build tabs which automatically handles + responsiveness and overflows as screen size reduces + -const TabNavTemplateOverFlowing: StoryFn = (args) => { - return ( - - - - If there are more TabNavItems than we can fit in the available space, the TabNav will - become horizontally scrollable with left/right arrow buttons. - - - - - - - {args.children} - - Payments - Magic Checkout - Item 1 - Item 2 - Item 3 - Item 4 - Item 5 - + + TabNav component takes in an array of items and gives you the flexibility of + the rendering via a render prop + + + + The render prop exposes two arrays: + + + items - an array of items that fit in the available space + + + overflowingItems - an array of items that overflow the + available space + + + + + You can map over these arrays and render the TabNavItem component for each item + or for the overflowing items you can render a Menu component to create a + dropdown "More" menu. + + + + + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + const Icon = item.icon; + return ( + { + console.log('clicked', item.title); + }} + > + + + {Icon && } + {item.title} + + + {item.description} + + + + ); + })} + + + ) : null} + > + ); + }} + ); }; export const TabNavExample = TabNavTemplate.bind({}); TabNavExample.args = { - children: 'Payroll', + title: 'Payroll', isActive: true, }; TabNavExample.storyName = 'TabNavExample'; - -export const WithMenu = TabNavTemplateWithMenu.bind({}); -WithMenu.args = { - children: 'Payroll', - isActive: true, -}; -WithMenu.storyName = 'With Menu'; - -export const OverFlowing = TabNavTemplateOverFlowing.bind({}); -OverFlowing.args = { - children: 'Payroll', - isActive: true, -}; -OverFlowing.storyName = 'Overflowing'; diff --git a/packages/blade/src/components/TopNav/docs/TopNav.stories.tsx b/packages/blade/src/components/TopNav/docs/TopNav.stories.tsx index 80ec9f96558..ac7bc589435 100644 --- a/packages/blade/src/components/TopNav/docs/TopNav.stories.tsx +++ b/packages/blade/src/components/TopNav/docs/TopNav.stories.tsx @@ -1,14 +1,14 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import React from 'react'; import type { StoryFn, Meta } from '@storybook/react'; import { Link, matchPath, useHistory, useLocation } from 'react-router-dom'; import storyRouterDecorator from 'storybook-react-router'; import { Title } from '@storybook/addon-docs'; +import styled from 'styled-components'; import type { TopNavProps } from '../TopNav'; import { TopNav, TopNavActions, TopNavContent, TopNavBrand } from '../TopNav'; import type { TabNavItemProps } from '../TabNav'; -import { TabNav, TabNavItem } from '../TabNav'; +import { TabNavItems, TabNav, TabNavItem } from '../TabNav'; import { topNavFullExample } from './code'; import { Box } from '~components/Box'; import type { SideNavLinkProps, SideNavProps } from '~components/SideNav'; @@ -21,14 +21,16 @@ import { } from '~components/SideNav'; import type { IconComponent } from '~components/Icons'; import { + SearchIcon, + AcceptPaymentsIcon, + AwardIcon, + ShoppingBagIcon, ChevronDownIcon, ActivityIcon, AnnouncementIcon, - BulkPayoutsIcon, ChevronRightIcon, HomeIcon, LayoutIcon, - MenuIcon, PaymentButtonIcon, PaymentGatewayIcon, PaymentLinkIcon, @@ -40,8 +42,7 @@ import { SearchInput } from '~components/Input/SearchInput'; import { Button } from '~components/Button'; import { Tooltip } from '~components/Tooltip'; import { Avatar } from '~components/Avatar'; -import { useIsMobile } from '~utils/useIsMobile'; -import { Text } from '~components/Typography'; +import { Heading, Text } from '~components/Typography'; import { Menu, MenuFooter, MenuHeader, MenuItem, MenuOverlay } from '~components/Menu'; import { Link as BladeLink } from '~components/Link'; import { Badge } from '~components/Badge'; @@ -50,7 +51,7 @@ import StoryPageWrapper from '~utils/storybook/StoryPageWrapper'; import { Alert } from '~components/Alert'; import { List, ListItem } from '~components/List'; -import { makeSize } from '~utils'; +import { makeSize, useBreakpoint, useTheme } from '~utils'; import { SIDE_NAV_EXPANDED_L1_WIDTH_XL, SIDE_NAV_EXPANDED_L1_WIDTH_BASE, @@ -190,7 +191,7 @@ const ExploreItem = ({ description, }: { icon: IconComponent; - title: string; + title?: string; description: string; }): React.ReactElement => { return ( @@ -214,134 +215,226 @@ const ExploreItem = ({ ); }; +const DashboardBackground = styled.div(() => { + return { + height: '100vh', + background: 'radial-gradient(94.74% 64.44% at 29.03% 15.17%, #FFFFFF 0%, #90A5BB 100%)', + }; +}); + const TopNavFullExample = () => { - const isMobile = useIsMobile(); const history = useHistory(); + const { theme } = useTheme(); + const { matchedBreakpoint, matchedDeviceType } = useBreakpoint({ + breakpoints: theme.breakpoints, + }); + const isTablet = matchedBreakpoint === 'm'; + const isMobile = matchedDeviceType === 'mobile'; const [isSideBarOpen, setIsSideBarOpen] = React.useState(false); const [selectedProduct, setSelectedProduct] = React.useState(null); + const activeUrl = useLocation().pathname; + React.useEffect(() => { + setSelectedProduct(activeUrl); + }, [activeUrl]); + return ( - + - {/* TopNavBrand gets hidden on mobile */} - - - - - {/* Desktop - render TabNav */} - - - Payroll - Payments - Magic Checkout - - }> - {selectedProduct ? `Explore: ${selectedProduct}` : 'Explore'} - + {isMobile ? ( + <> + + Home + + + Payments + + + - - Recommended - - } - /> - { - history.push('/explore/payroll'); - setSelectedProduct('Payroll'); - }} - > - + + + + + + John Doe + + + Razorpay Trusted Merchant + + + + + Settings - { - history.push('/explore/payouts'); - setSelectedProduct('Payout'); - }} - > - + + Logout - - - View all products - - - - {/* Mobile - render hamburger button */} - - { - setIsSideBarOpen(!isSideBarOpen); - }} - /> - Home - - - - {/* Remove searchbar on mobile */} - - - - - - - - - - - - - - - - - - John Doe - - - Razorpay Trusted Merchant - - - - - Settings - - - Logout - - - - + > + ) : ( + <> + + + + + + {({ items, overflowingItems }) => { + const activeProduct = overflowingItems.find( + (item) => item.href === selectedProduct, + ); + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } + isActive={Boolean(activeProduct)} + /> + + + Recommended + + } + /> + {overflowingItems.map((item) => { + return ( + { + history.push(item.href!); + setSelectedProduct(item.href!); + }} + > + + + ); + })} + + + View all products + + + + + ) : null} + > + ); + }} + + + + {isTablet ? ( + + + + ) : ( + + )} + + + + + + + + + + + + + + + John Doe + + + Razorpay Trusted Merchant + + + + + Settings + + + Logout + + + + + > + )} { backgroundColor="surface.background.gray.intense" > - This demo integrates: + Active URL: {activeUrl} + This demo integrates: SideNav Menu (Explore Tab) @@ -382,25 +476,115 @@ const TopNavFullExample = () => { - + ); }; const TopNavFullTemplate: StoryFn = () => ; const TopNavMinimalTemplate: StoryFn = () => { + const history = useHistory(); + const [selectedProduct, setSelectedProduct] = React.useState(null); + return ( - + - - - Payroll - Payments - Magic Checkout + + {({ items, overflowingItems }) => { + const activeProduct = overflowingItems.find( + (item) => item.href === selectedProduct, + ); + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } + isActive={Boolean(activeProduct)} + /> + + + Recommended + + } + /> + {overflowingItems.map((item) => { + return ( + { + history.push(item.href!); + setSelectedProduct(item.href!); + }} + > + + + ); + })} + + + View all products + + + + + ) : null} + > + ); + }} @@ -417,14 +601,15 @@ const TopNavMinimalTemplate: StoryFn = () => { - - - This is a minimal example usage of TopNav, checkout Full Dashboard Layout example for - other features & integration details. - - - + + + + This is a minimal example usage of TopNav, checkout Full Dashboard Layout example for + other features & integration details. + + + ); }; diff --git a/packages/blade/src/components/TopNav/docs/code.ts b/packages/blade/src/components/TopNav/docs/code.ts index 1fb305f150d..afe02c34914 100644 --- a/packages/blade/src/components/TopNav/docs/code.ts +++ b/packages/blade/src/components/TopNav/docs/code.ts @@ -22,6 +22,7 @@ export const topNavFullExample = { import { Box, Text, + Heading, TopNav, TopNavBrand, TopNavContent, @@ -80,7 +81,7 @@ export const topNavFullExample = { description, }: { icon: IconComponent; - title: string; + title?: string; description: string; }): React.ReactElement => { return ( @@ -104,6 +105,13 @@ export const topNavFullExample = { ); }; + const DashboardBackground = styled.div(() => { + return { + height: '100vh', + background: 'radial-gradient(94.74% 64.44% at 29.03% 15.17%, #FFFFFF 0%, #90A5BB 100%)', + }; + }); + const TopNavExample = (): React.ReactElement => { const { platform } = useTheme(); const history = useHistory(); @@ -111,145 +119,239 @@ export const topNavFullExample = { const [isSideBarOpen, setIsSideBarOpen] = React.useState(false); const [selectedProduct, setSelectedProduct] = React.useState(null); + const activeUrl = useLocation().pathname; + React.useEffect(() => { + setSelectedProduct(activeUrl); + }, [activeUrl]); + return ( - - - {/* TopNavBrand automatically gets hidden on mobile */} - - - - - {/* Desktop - render TabNav */} - - - Payroll - Payments - Magic Checkout - - - {selectedProduct ? \`Explore: \${selectedProduct}\` : 'Explore'} - - - - Recommended - - } - /> - { - history.push('/explore/payroll'); - setSelectedProduct('Payroll'); - }} + + + + {isMobile ? ( + <> + + Home + + + Payments + + + + + + + + + + John Doe + + + Razorpay Trusted Merchant + + + + + Settings + + + Logout + + + + > + ) : ( + <> + + + + + - - - { - history.push('/explore/payouts'); - setSelectedProduct('Payout'); + {({ items, overflowingItems }) => { + const activeProduct = overflowingItems.find( + (item) => item.href === selectedProduct, + ); + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } + isActive={Boolean(activeProduct)} + /> + + + Recommended + + } + /> + {overflowingItems.map((item) => { + return ( + { + history.push(item.href!); + setSelectedProduct(item.href!); + }} + > + + + ); + })} + + + View all products + + + + + ) : null} + > + ); }} - > - + + + + + - - - - View all products - - - - - - {/* Mobile - render hamburger button */} - - { - setIsSideBarOpen(!isSideBarOpen); - }} - /> - Home - - - - {/* Remove searchbar on mobile */} - - - - - - - - - - - - - - { - setIsSideBarOpen(false); - }} - /> + + + + + + + + + + + + + John Doe + + + Razorpay Trusted Merchant + + + + + Settings + + + Logout + + + + + > + )} + + { + setIsSideBarOpen(false); + }} + /> - - This demo integrates: - - SideNav - Menu (Explore Tab) - ReactRouter - Mobile Responsiveness - One Dashboard Layout - + + + This demo integrates: + + SideNav + Menu (Explore Tab) + ReactRouter + Mobile Responsiveness + One Dashboard Layout + + - + ); }; @@ -381,16 +483,107 @@ export const topNavFullExample = { export const tabNavExample = { 'App.tsx': dedent`import React from 'react'; - import { Box, TabNav, TabNavItem } from '@razorpay/blade/components'; + import { + Box, + TabNav, + TabNavItem, + Text, + HomeIcon, + RazorpayxPayrollIcon, + AcceptPaymentsIcon, + MagicCheckoutIcon, + AwardIcon, + ChevronDownIcon, + Menu, + MenuItem, + MenuOverlay, + } from '@razorpay/blade/components'; const App = () => { return ( - - - Payroll - Payments - Magic Checkout + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + const Icon = item.icon; + return ( + { + console.log('clicked', item.title); + }} + > + + + {Icon && } + {item.title} + + + {item.description} + + + + ); + })} + + + ) : null} + > + ); + }} ); diff --git a/packages/blade/src/tokens/global/size.ts b/packages/blade/src/tokens/global/size.ts index 241c5aa07c6..e8a4550bb31 100644 --- a/packages/blade/src/tokens/global/size.ts +++ b/packages/blade/src/tokens/global/size.ts @@ -76,6 +76,8 @@ export const size = { 176: 176, /** 200 px */ 200: 200, + /** 208 px */ + 208: 208, /** 240 px */ 240: 240, /** 245 px */ diff --git a/packages/blade/src/utils/componentZIndices.ts b/packages/blade/src/utils/componentZIndices.ts index 6afeb89cf8a..ca600d0019f 100644 --- a/packages/blade/src/utils/componentZIndices.ts +++ b/packages/blade/src/utils/componentZIndices.ts @@ -7,4 +7,5 @@ export const componentZIndices = { tourMask: 1100, popover: 1100, tooltip: 1100, + topnav: 100, };
Share @@ -950,7 +959,7 @@ exports[`Menu renders a Menu 1`] = ` />
Log Out @@ -1008,26 +1017,26 @@ exports[`Menu renders a Menu 1`] = ` />
Footer slot diff --git a/packages/blade/src/components/Menu/docs/Menu.stories.tsx b/packages/blade/src/components/Menu/docs/Menu.stories.tsx index 72e7964ab1f..bd83739f3f0 100644 --- a/packages/blade/src/components/Menu/docs/Menu.stories.tsx +++ b/packages/blade/src/components/Menu/docs/Menu.stories.tsx @@ -170,7 +170,7 @@ type TemplateProps = MenuProps & { trigger: React.ReactElement }; const accountsMenuOverlayContent = ( <> } /> - + Razorpay Pvt Ltd @@ -182,7 +182,7 @@ const accountsMenuOverlayContent = ( Switch Merchant - + } diff --git a/packages/blade/src/components/Menu/types.ts b/packages/blade/src/components/Menu/types.ts index 44b81a90c12..caf8a4ef12c 100644 --- a/packages/blade/src/components/Menu/types.ts +++ b/packages/blade/src/components/Menu/types.ts @@ -114,7 +114,7 @@ type MenuOverlayProps = { /** * JSX Slot for MenuItem or anything else */ - children: React.ReactElement[] | React.ReactElement; + children: React.ReactElement[] | React.ReactElement | React.ReactNode; /** * zIndex override diff --git a/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx b/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx index c9b1b93d265..6c89d280429 100644 --- a/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx +++ b/packages/blade/src/components/TopNav/TabNav/TabNav.web.tsx @@ -1,181 +1,116 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable consistent-return */ import React from 'react'; -import styled from 'styled-components'; -import { useTopNavContext } from '../TopNavContext'; -import { approximatelyEqual, MIXED_BG_COLOR, useHasOverflow } from './utils'; +import ReactDOM from 'react-dom'; import { TabNavContext } from './TabNavContext'; +import { useResize } from './utils'; +import type { TabNavItemData, TabNavProps } from './types'; import BaseBox from '~components/Box/BaseBox'; import type { StyledPropsBlade } from '~components/Box/styledProps'; import { getStyledProps } from '~components/Box/styledProps'; -import { Button } from '~components/Button'; import { Divider } from '~components/Divider'; -import { ChevronLeftIcon, ChevronRightIcon } from '~components/Icons'; -import { makeMotionTime, makeSize } from '~utils'; +import { makeSize } from '~utils'; import { size } from '~tokens/global'; -import getIn from '~utils/lodashButBetter/get'; -import type { BoxProps } from '~components/Box'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import type { BoxProps } from '~components/Box'; +import { Box } from '~components/Box'; -const GRADIENT_WIDTH = 54 as const; -const GRADIENT_OFFSET = -8 as const; -const OFFSET_BOTTOM = -12 as const; -const SCROLL_AMOUNT = 200; - -type TabNavProps = { - children: React.ReactNode; +const TabNavItems = ({ children, ...props }: BoxProps): React.ReactElement => { + return ( + + {React.Children.map(children, (child, index) => { + return ( + <> + {index > 0 ? ( + + ) : null} + {React.cloneElement(child as React.ReactElement, { + __isInsideTabNavItems: true, + __index: index, + })} + > + ); + })} + + + ); }; -const ScrollableArea = styled(BaseBox)(() => { - return { - '&::-webkit-scrollbar': { display: 'none' }, - }; -}); - -const GradientOverlay = styled(BaseBox)<{ - shouldShow?: boolean; - variant: 'left' | 'right'; - $color: BoxProps['backgroundColor']; -}>(({ theme, shouldShow, variant, $color }) => { - const color = getIn(theme.colors, $color as never, MIXED_BG_COLOR); - - return { - position: 'absolute', - [variant]: 0, - pointerEvents: shouldShow ? 'auto' : 'none', - transform: shouldShow ? 'scale(1)' : 'scale(0.5)', - opacity: shouldShow ? 1 : 0, - transitionTimingFunction: `${theme.motion.easing.standard.revealing}`, - transitionDuration: `${makeMotionTime(theme.motion.duration.xquick)}`, - transitionProperty: 'opacity, transform', - zIndex: 1, - ':before': { - content: "''", - pointerEvents: 'none', - position: 'absolute', - [variant]: 0, - top: makeSize(GRADIENT_OFFSET), - bottom: makeSize(GRADIENT_OFFSET), - width: makeSize(GRADIENT_WIDTH), - background: `linear-gradient(to ${variant}, transparent 0%, ${color} 30%, ${color} 100%);`, - }, - }; -}); - const TabNav = ({ children, + items, ...styledProps }: TabNavProps & StyledPropsBlade): React.ReactElement => { const ref = React.useRef(null); - const hasOverflow = useHasOverflow(ref); - const [scrollStatus, setScrollStatus] = React.useState<'start' | 'end' | 'middle'>('start'); - const { backgroundColor } = useTopNavContext(); + const [controlledItems, setControlledItems] = React.useState(items); - // Check if the scroll is at start, end or middle - const handleScrollStatus = React.useCallback( - (e: React.UIEvent): void => { - const target = e.target as HTMLDivElement; - const isAtStart = target.scrollLeft === 0; - const isAtEnd = approximatelyEqual( - target.scrollLeft, - target.scrollWidth - target.offsetWidth, - ); - - if (isAtStart) { - setScrollStatus('start'); - } else if (isAtEnd) { - setScrollStatus('end'); - } else { - setScrollStatus('middle'); - } - }, - [], + const overflowingItems = controlledItems.filter( + (item) => item.isAlwaysOverflowing ?? item.isOverflowing, ); + const _items = controlledItems.filter((item) => !item.isAlwaysOverflowing && !item.isOverflowing); - const scrollRight = (): void => { - if (!ref.current) return; - ref.current.scrollBy({ - behavior: 'smooth', - left: SCROLL_AMOUNT, - }); - }; + // We need to memoize this callback otherwise it will cause infinite re-renders + // Because the ResizeObserver callback will be a new reference on every render + // and it will trigger a re-render + const resizeCallback = React.useCallback((resizeInfo: ResizeObserverEntry): void => { + const target = resizeInfo.target as HTMLElement; + const updateItems = (): void => { + setControlledItems((items) => { + return items.map((item, index) => { + // never overflow the first item + if (index === 0) return { ...item, isOverflowing: false }; + // add padding to the offsetX to account the "More" menu's width changing due to the selection state (eg: More:ProdctName) + // Currently, hardcoding this to 150, we can make this dynamic too but that's causing layout thrashing + // because first we need to calculate the width of the "More" menu and then update the items + const padding = 150; + const offset = (item.offsetX! + padding)! - target.getBoundingClientRect().left; + if (offset > target.offsetWidth) { + return { ...item, isOverflowing: true }; + } else { + return { ...item, isOverflowing: false }; + } + }); + }); + }; + // https://github.com/webpack/webpack/issues/14814 + const flushSync = (ReactDOM as any)['flushSync'.toString()]; + // Using flushSync to avoid layout thrashing, + // this will force React to flush all pending updates and only then update the DOM + if (flushSync !== undefined) { + flushSync(updateItems); + } else { + updateItems(); + } + }, []); - const scrollLeft = (): void => { - if (!ref.current) return; - ref.current.scrollBy({ - behavior: 'smooth', - left: -SCROLL_AMOUNT, - }); - }; + useResize(ref, resizeCallback); return ( - + - - - - + - {React.Children.map(children, (child, index) => { - return ( - <> - {index > 0 ? ( - - ) : null} - {child} - > - ); - })} + {children({ items: _items, overflowingItems })} - - - - + ); }; -export { TabNav }; +export { TabNav, TabNavItems }; diff --git a/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx b/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx index 5cca5072449..a5a11a368a3 100644 --- a/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx +++ b/packages/blade/src/components/TopNav/TabNav/TabNavContext.tsx @@ -1,9 +1,11 @@ import React from 'react'; +import type { TabNavItemData } from './types'; import { throwBladeError } from '~utils/logger'; type TabNavContextProps = { containerRef: React.RefObject; - hasOverflow: boolean; + controlledItems: TabNavItemData[]; + setControlledItems: React.Dispatch>; }; const TabNavContext = React.createContext(null); diff --git a/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx b/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx index 966649f5859..f911864bc2a 100644 --- a/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx +++ b/packages/blade/src/components/TopNav/TabNav/TabNavItem.web.tsx @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable consistent-return */ import React from 'react'; import styled from 'styled-components'; -import { useTopNavContext } from '../TopNavContext'; import type { TabNavItemProps } from './types'; import { useTabNavContext } from './TabNavContext'; import { MIXED_BG_COLOR } from './utils'; @@ -10,11 +12,8 @@ import { makeBorderSize, makeMotionTime, makeSize, makeSpace } from '~utils'; import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects'; import { makeAccessible } from '~utils/makeAccessible'; import { size } from '~tokens/global'; -import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; -import { mergeRefs } from '~utils/useMergeRefs'; -import type { BoxProps } from '~components/Box'; -import getIn from '~utils/lodashButBetter/get'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; const StyledTabNavItem = styled.a<{ $isActive?: boolean }>(({ theme, $isActive }) => { return { @@ -37,6 +36,14 @@ const StyledTabNavItem = styled.a<{ $isActive?: boolean }>(({ theme, $isActive } paddingLeft: makeSpace(theme.spacing[4]), paddingRight: makeSpace(theme.spacing[4]), borderRadius: makeBorderSize(theme.border.radius.medium), + // reset button styles + border: 'none', + background: 'none', + '&[aria-expanded="true"]': $isActive + ? {} + : { + backgroundColor: theme.colors.interactive.background.gray.default, + }, '&:hover': $isActive ? {} : { @@ -47,16 +54,15 @@ const StyledTabNavItem = styled.a<{ $isActive?: boolean }>(({ theme, $isActive } const StyledTabNavItemWrapper = styled(BaseBox)<{ isActive?: boolean; - dividerHiderColor: BoxProps['backgroundColor']; -}>(({ theme, isActive, dividerHiderColor }) => { +}>(({ theme, isActive }) => { const dividerHiderStyle = { content: '""', position: 'absolute', top: '50%', transform: 'translateY(-50%)', width: makeSize(size[1]), - height: '50%', - backgroundColor: getIn(theme.colors, dividerHiderColor as never, MIXED_BG_COLOR), + height: makeSize(size[16]), + backgroundColor: MIXED_BG_COLOR, } as const; return { @@ -66,16 +72,14 @@ const StyledTabNavItemWrapper = styled(BaseBox)<{ backgroundColor: isActive ? theme.colors.surface.background.gray.intense : 'transparent', borderColor: isActive ? theme.colors.surface.border.gray.muted : 'transparent', borderStyle: 'solid', - borderBottomWidth: 0, borderWidth: makeBorderSize(theme.border.width.thin), + borderBottomWidth: 0, borderTopLeftRadius: makeBorderSize(theme.border.radius.medium), borderTopRightRadius: makeBorderSize(theme.border.radius.medium), - // Animation - transform: isActive ? `translateY(${makeSize(size[2])})` : 'none', transition: `${makeMotionTime(theme.motion.duration.moderate)} ${ theme.motion.easing.standard.effective }`, - transitionProperty: 'background, transform', + transitionProperty: 'background', // Hide the left and right divider by overlaying them with a pseudo element as same color as the background ...(isActive @@ -113,47 +117,55 @@ const SelectedBar = styled(BaseBox)<{ isActive?: boolean }>(({ theme, isActive } }); const _TabNavItem: React.ForwardRefRenderFunction = ( - { as, children, isActive, icon: Icon, trailing, accessibilityLabel, href, target, ...props }, + { + as, + title, + isActive, + icon: Icon, + trailing, + accessibilityLabel, + href, + target, + // @ts-expect-error - This prop is only used internally + __isInsideTabNavItems, + // @ts-expect-error - This prop is only used internally + __index, + ...props + }, ref, ): React.ReactElement => { - const { containerRef, hasOverflow } = useTabNavContext(); - const { backgroundColor } = useTopNavContext(); - const linkRef = React.useRef(null); + const { setControlledItems } = useTabNavContext(); + const bodyRef = React.useRef(null); - // Scroll the active tab into view - // Only if the tab is very close to the edge - // Or if the tab is out of view + // Update the controlledItems with the tabWidth and offsetX useIsomorphicLayoutEffect(() => { - if (!isActive || !hasOverflow) return; - if (!('requestAnimationFrame' in window)) return; - - window.requestAnimationFrame(() => { - if (!linkRef.current || !containerRef.current) return; - - const buffer = 100; - const container = containerRef.current; - const linkElement = linkRef.current; - const containerRect = container.getBoundingClientRect(); - const linkRect = linkElement.getBoundingClientRect(); - const isCloseToStart = linkRect.left < containerRect.left + buffer; - const isCloseToEnd = linkRect.right > containerRect.right - buffer; - - if (isCloseToStart || isCloseToEnd) { - linkElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); - } + if (!bodyRef.current) return; + if (!__isInsideTabNavItems) return; + setControlledItems((prev) => { + return prev.map((item, index) => { + if (index !== __index) return item; + const bounds = bodyRef?.current?.getBoundingClientRect()!; + const tabWidth = bounds.width; + const offsetX = bounds.right; + return { + ...item, + tabWidth, + offsetX, + }; + }); }); - }, [hasOverflow, isActive]); + }, [__isInsideTabNavItems, __index, setControlledItems]); return ( ) : null} - {children} + {title} {trailing ? trailing : null} diff --git a/packages/blade/src/components/TopNav/TabNav/types.ts b/packages/blade/src/components/TopNav/TabNav/types.ts index e0d38fc59ab..646e28677df 100644 --- a/packages/blade/src/components/TopNav/TabNav/types.ts +++ b/packages/blade/src/components/TopNav/TabNav/types.ts @@ -32,7 +32,7 @@ type TabNavItemProps = { * ``` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - as?: React.ComponentType; + as?: React.ComponentType | 'a' | 'button'; /** * Selected state of the navigation item. * @@ -52,15 +52,30 @@ type TabNavItemProps = { */ trailing?: React.ReactElement; /** - * Element to render inside the navigation item. - * - * This can either be a string or JSX element (eg: Menu component) + * Title of the navigation item. */ - children?: React.ReactNode; + title?: string; /** * Accessibility label for the navigation item. */ accessibilityLabel?: string; } & MenuTriggerProps; -export type { TabNavItemProps }; +type Item = TabNavItemProps & { + description?: string; + isAlwaysOverflowing?: boolean; +}; +type TabNavItemData = Item & { + isOverflowing?: boolean; + tabWidth?: number; + offsetX?: number; +}; +type TabNavProps = { + items: Item[]; + children: (props: { + items: TabNavItemData[]; + overflowingItems: TabNavItemData[]; + }) => React.ReactElement; +}; + +export type { TabNavItemProps, TabNavItemData, TabNavProps }; diff --git a/packages/blade/src/components/TopNav/TabNav/utils.ts b/packages/blade/src/components/TopNav/TabNav/utils.ts index dd25928377c..5a39973c858 100644 --- a/packages/blade/src/components/TopNav/TabNav/utils.ts +++ b/packages/blade/src/components/TopNav/TabNav/utils.ts @@ -1,51 +1,38 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable consistent-return */ -import React from 'react'; +import type React from 'react'; import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; /** - * Check if an element has scroll overflow + * Hook to observe resize events on a given element */ -const useHasOverflow = ( +const useResize = ( ref: React.RefObject, - callback?: (hasOverflow: boolean) => void, -): boolean => { - const observer = React.useRef(null); - const [hasOverflow, setHasOverflow] = React.useState(false); - + callback?: (entry: ResizeObserverEntry) => void, +) => { useIsomorphicLayoutEffect(() => { if (!ref.current) return; const element = ref.current; - const trigger = (): void => { - const hasOverflow = element.scrollWidth > element.clientWidth; - setHasOverflow(hasOverflow); - - if (callback) callback(hasOverflow); - }; + if (!('ResizeObserver' in window)) return; - trigger(); - if ('ResizeObserver' in window) { - observer.current = new ResizeObserver(trigger); - observer.current.observe(element); - } + const observer = new ResizeObserver((entries) => { + entries.forEach((entry) => { + callback?.(entry); + }); + }); + observer.observe(element); // destroy the observer return (): void => { - if ('ResizeObserver' in window) { - observer.current?.disconnect(); - } + if (!('ResizeObserver' in window)) return; + observer?.disconnect(); }; - }, [callback, ref]); - - return hasOverflow; -}; - -const approximatelyEqual = (v1: number, v2: number, tolerance = 1): boolean => { - return Math.abs(v1 - v2) < tolerance; + }, [callback]); }; // Overlapping color of surface.background.gray.subtle + interactive.background.gray.default // TODO(future): design will tokenize or check if this is needed or not const MIXED_BG_COLOR = '#e1e7ef'; -export { useHasOverflow, approximatelyEqual, MIXED_BG_COLOR }; +export { useResize, MIXED_BG_COLOR }; diff --git a/packages/blade/src/components/TopNav/TopNav.web.tsx b/packages/blade/src/components/TopNav/TopNav.web.tsx index 11ee8dfa4c6..2ca8cf962ce 100644 --- a/packages/blade/src/components/TopNav/TopNav.web.tsx +++ b/packages/blade/src/components/TopNav/TopNav.web.tsx @@ -1,9 +1,7 @@ import React from 'react'; -import { TopNavContext } from './TopNavContext'; import type { BoxProps } from '~components/Box'; import { Box } from '~components/Box'; import BaseBox from '~components/Box/BaseBox'; -import { Divider } from '~components/Divider'; import { SIDE_NAV_EXPANDED_L1_WIDTH_XL, SIDE_NAV_EXPANDED_L1_WIDTH_BASE, @@ -11,42 +9,12 @@ import { import { size } from '~tokens/global'; import { makeSize } from '~utils'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import type { StyledPropsBlade } from '~components/Box/styledProps'; +import { componentZIndices } from '~utils/componentZIndices'; const TOP_NAV_HEIGHT = size[56]; const CONTENT_RIGHT_GAP = size[80]; -const RazorpayLinesSvg = (): React.ReactElement => { - return ( - - - - - - - - - - ); -}; - type TopNavProps = { children: React.ReactNode; } & Pick< @@ -66,40 +34,33 @@ type TopNavProps = { | 'right' | 'width' | 'zIndex' ->; +> & + StyledPropsBlade; -const TopNav = ({ children, ...styledProps }: TopNavProps): React.ReactElement => { +const TopNav = ({ children, ...boxProps }: TopNavProps): React.ReactElement => { return ( - - - {children} - - - - - + + {children} + ); }; const TopNavBrand = ({ children }: { children: React.ReactNode }): React.ReactElement => { return ( {children} - - - ); }; @@ -128,7 +81,7 @@ const TopNavContent = ({ children }: { children: React.ReactNode }): React.React @@ -140,10 +93,15 @@ const TopNavContent = ({ children }: { children: React.ReactNode }): React.React const TopNavActions = ({ children }: { children: React.ReactNode }): React.ReactElement => { return ( {children} diff --git a/packages/blade/src/components/TopNav/TopNavContext.tsx b/packages/blade/src/components/TopNav/TopNavContext.tsx deleted file mode 100644 index c9e01ae14e6..00000000000 --- a/packages/blade/src/components/TopNav/TopNavContext.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { MIXED_BG_COLOR } from './TabNav/utils'; -import type { BoxProps } from '~components/Box'; - -type TopNavContextProps = { - backgroundColor: BoxProps['backgroundColor']; -}; -const TopNavContext = React.createContext({ - backgroundColor: MIXED_BG_COLOR as never, -}); - -const useTopNavContext = (): TopNavContextProps => { - const context = React.useContext(TopNavContext); - return context!; -}; - -export { TopNavContext, useTopNavContext }; diff --git a/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx b/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx index 7a8be71ae64..4d56947df54 100644 --- a/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx +++ b/packages/blade/src/components/TopNav/__tests__/TabNav.test.stories.tsx @@ -3,22 +3,102 @@ import type { StoryFn } from '@storybook/react'; import { within, userEvent } from '@storybook/testing-library'; import { expect } from '@storybook/jest'; import React from 'react'; -import { TabNav, TabNavItem } from '../TabNav'; -import { Box } from '~components/Box'; -import { HomeIcon } from '~components/Icons'; +import type { TabNavProps } from '../TabNav'; +import { TabNav, TabNavItem, TabNavItems } from '../TabNav'; +import { + AcceptPaymentsIcon, + AwardIcon, + ChevronDownIcon, + HomeIcon, + MagicCheckoutIcon, + RazorpayxPayrollIcon, +} from '~components/Icons'; +import { Menu, MenuItem, MenuOverlay } from '~components/Menu'; +import { Text } from '~components/Typography'; const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); -export const TestBasicTabNav: StoryFn = (): React.ReactElement => { +const TabNavExample = ({ items }: { items?: TabNavProps['items'] }): React.ReactElement => { return ( - - - Payroll - Payments + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + return ( + { + console.log('clicked', item.title); + }} + > + {item.title} + + ); + })} + + + ) : null} + > + ); + }} ); }; +export const TestBasicTabNav: StoryFn = (): React.ReactElement => { + return ; +}; + TestBasicTabNav.play = async ({ canvasElement }) => { const { getByRole } = within(canvasElement); const homeTab = getByRole('link', { name: 'Home' }); @@ -30,45 +110,90 @@ TestBasicTabNav.play = async ({ canvasElement }) => { }; export const TestOverflow: StoryFn = (): React.ReactElement => { + return ; +}; + +TestOverflow.play = async ({ canvasElement }) => { + const { getByRole, queryByRole } = within(document.body); + canvasElement.style.width = '100%'; + + await sleep(500); + const homeTab = getByRole('link', { name: 'Home' }); + const payrollTab = getByRole('link', { name: 'Payroll' }); + const paymentsTab = getByRole('link', { name: 'Payments' }); + await expect(homeTab).toBeVisible(); + await expect(payrollTab).toBeVisible(); + await expect(paymentsTab).toBeVisible(); + + await sleep(500); + + // reduce the width of the canvas to make the tabs overflow + canvasElement.style.width = '600px'; + await sleep(500); + + const moreTab = getByRole('button', { name: 'More' }); + await userEvent.hover(moreTab); + await sleep(500); + await expect(queryByRole('menu', { name: 'More' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Rize' })).toBeVisible(); + await expect(queryByRole('link', { name: 'Magic Checkout' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Magic Checkout' })).toBeVisible(); + + canvasElement.style.width = '300px'; + await sleep(500); + await expect(queryByRole('link', { name: 'Payroll' })).toBeNull(); + await expect(queryByRole('link', { name: 'Payments' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Payroll' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payments' })).toBeVisible(); + + canvasElement.style.width = '100%'; + await sleep(500); + await expect(queryByRole('menuitem', { name: 'Rize' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payroll' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Payments' })).toBeNull(); + await expect(queryByRole('menuitem', { name: 'Magic Checkout' })).toBeNull(); +}; + +export const ShouldNotShowMore: StoryFn = (): React.ReactElement => { return ( - - - Item 1 - Item 2 - Item 3 - Item 4 - Item 5 - Item 6 - Item 7 - Item 8 - Item 9 - - + ); }; -TestOverflow.play = async ({ canvasElement }) => { - const { getByRole } = within(canvasElement); +ShouldNotShowMore.play = async ({ canvasElement }) => { + const { getByRole, queryByRole } = within(document.body); + + await sleep(500); + const homeTab = getByRole('link', { name: 'Home' }); + const payrollTab = getByRole('link', { name: 'Payroll' }); + const paymentsTab = getByRole('link', { name: 'Payments' }); + await expect(homeTab).toBeVisible(); + await expect(payrollTab).toBeVisible(); + await expect(paymentsTab).toBeVisible(); + await expect(queryByRole('button', { name: 'More' })).toBeNull(); + await sleep(500); - const item1 = getByRole('link', { name: 'Item 1' }); - const scrollLeftButton = getByRole('button', { name: /Scroll Left/ }); - const scrollRightButton = getByRole('button', { name: /Scroll Right/ }); - await expect(scrollLeftButton).not.toBeVisible(); - - await expect(item1).toBeVisible(); - await expect(scrollRightButton).toBeVisible(); - // scroll - await userEvent.click(scrollRightButton); + + // reduce the width of the canvas to make the tabs overflow + canvasElement.style.width = '200px'; await sleep(500); - await expect(scrollLeftButton).toBeVisible(); - await expect(scrollRightButton).toBeVisible(); + const moreTab = getByRole('button', { name: 'More' }); + await expect(moreTab).toBeVisible(); - // scroll to end - await userEvent.click(scrollRightButton); + // hover over the more tab + await userEvent.hover(moreTab); await sleep(500); - await expect(scrollLeftButton).toBeVisible(); - await expect(scrollRightButton).not.toBeVisible(); + + await expect(queryByRole('menu', { name: 'More' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payments' })).toBeVisible(); + await expect(queryByRole('menuitem', { name: 'Payroll' })).toBeVisible(); }; export default { diff --git a/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx b/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx index f30336d1bb6..bf5e19371a7 100644 --- a/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx +++ b/packages/blade/src/components/TopNav/__tests__/TopNavExample.web.tsx @@ -1,11 +1,13 @@ -import { TabNav, TabNavItem } from '../TabNav'; +import { TabNav, TabNavItem, TabNavItems } from '../TabNav'; import { TopNav, TopNavActions, TopNavBrand, TopNavContent } from '../TopNav'; import { Avatar } from '~components/Avatar'; import { Box } from '~components/Box'; import { Button } from '~components/Button'; -import { ActivityIcon, AnnouncementIcon, HomeIcon } from '~components/Icons'; +import { ActivityIcon, AnnouncementIcon, ChevronDownIcon } from '~components/Icons'; import { RazorpayLogo } from '~components/SideNav/docs/RazorpayLogo'; import { Tooltip } from '~components/Tooltip'; +import { Menu, MenuItem, MenuOverlay } from '~components/Menu'; +import { Text } from '~components/Typography'; const TopNavExample = (): React.ReactElement => { return ( @@ -15,11 +17,67 @@ const TopNavExample = (): React.ReactElement => { - - - Payroll - Payments - Magic Checkout + + {({ items, overflowingItems }) => { + return ( + <> + + {items.map((item) => { + return ( + + ); + })} + + {overflowingItems.length ? ( + + } /> + + {overflowingItems.map((item) => { + return ( + { + console.log('clicked', item.title); + }} + > + {item.title} + + ); + })} + + + ) : null} + > + ); + }} diff --git a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap index e393fa4a3e2..0d44ec52691 100644 --- a/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap +++ b/packages/blade/src/components/TopNav/__tests__/__snapshots__/TopNav.ssr.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should render TopNav ssr 1`] = `"PayrollPaymentsMagic CheckoutAH"`; +exports[` should render TopNav ssr 1`] = `"HomePayrollPaymentsMagic CheckoutMoreAH"`; exports[` should render TopNav ssr 2`] = ` .c0.c0.c0.c0.c0 { @@ -16,10 +16,12 @@ exports[` should render TopNav ssr 2`] = ` align-items: center; position: -webkit-sticky; position: sticky; - z-index: 1; - grid-template-columns: minmax(0,1fr) auto; - padding-right: 8px; - padding-left: 8px; + z-index: 100; + grid-template-columns: auto minmax(0,1fr) auto; + padding-top: 8px; + padding-bottom: 8px; + padding-right: 12px; + padding-left: 12px; height: 56px; width: 100%; top: 0px; @@ -27,7 +29,6 @@ exports[` should render TopNav ssr 2`] = ` } .c2.c2.c2.c2.c2 { - display: none; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -41,23 +42,6 @@ exports[` should render TopNav ssr 2`] = ` } .c4.c4.c4.c4.c4 { - display: none; - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; -} - -.c5.c5.c5.c5.c5 { - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; - margin-right: -1px; - height: 20px; - border-left-color: hsla(211,20%,52%,0.18); - border-left-style: solid; -} - -.c7.c7.c7.c7.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -66,11 +50,13 @@ exports[` should render TopNav ssr 2`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; padding-right: 0px; - margin-left: 0px; } -.c8.c8.c8.c8.c8 { +.c5.c5.c5.c5.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -79,82 +65,54 @@ exports[` should render TopNav ssr 2`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; position: relative; - margin-bottom: -12px; width: 100%; } -.c12.c12.c12.c12.c12 { +.c6.c6.c6.c6.c6 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -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; - z-index: 1; + position: relative; + width: 100%; } -.c14.c14.c14.c14.c14 { +.c7.c7.c7.c7.c7 { 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-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; } -.c15.c15.c15.c15.c15 { +.c8.c8.c8.c8.c8 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - overflow-x: auto; - overflow-y: hidden; - white-space: nowrap; position: relative; width: 100%; gap: 0px; + left: -1px; } -.c17.c17.c17.c17.c17 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - width: -webkit-max-content; - width: -moz-max-content; - width: max-content; -} - -.c21.c21.c21.c21.c21 { +.c12.c12.c12.c12.c12 { margin: auto; height: 16px; border-left-color: hsla(211,20%,52%,0.18); border-left-style: solid; } -.c24.c24.c24.c24.c24 { +.c14.c14.c14.c14.c14 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -163,25 +121,25 @@ exports[` should render TopNav ssr 2`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-align-self: end; + -ms-flex-item-align: end; + align-self: end; + padding: 8px; margin-top: 2px; gap: 8px; -} - -.c26.c26.c26.c26.c26 { background-color: hsla(0,0%,100%,1); + border-top-left-radius: 4px; + border-top-right-radius: 4px; } -.c28.c28.c28.c28.c28 { - position: relative; - height: 100%; - width: 100%; -} - -.c30.c30.c30.c30.c30 { +.c17.c17.c17.c17.c17 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -194,252 +152,81 @@ exports[` should render TopNav ssr 2`] = ` -ms-flex-pack: center; justify-content: center; z-index: 1; - height: 100%; -} - -.c32.c32.c32.c32.c32 { - position: absolute; - top: 0px; - left: 0px; - pointer-events: none; } -.c10.c10.c10.c10.c10 { - min-height: 28px; - height: 28px; - width: 28px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; +.c19.c19.c19.c19.c19 { + 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-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c10.c10.c10.c10.c10:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c10.c10.c10.c10.c10:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c10.c10.c10.c10.c10 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c25.c25.c25.c25.c25 { - min-height: 36px; - height: 36px; - width: 36px; - cursor: pointer; - background-color: hsla(211,20%,52%,0.12); - border-color: hsla(214,28%,84%,1); - border-width: 0px; - border-radius: 4px; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-text-decoration: none; - text-decoration: none; - overflow: hidden; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-transition-property: background-color,border-color,box-shadow; - transition-property: background-color,border-color,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: 150ms; - transition-duration: 150ms; - position: relative; -} - -.c25.c25.c25.c25.c25:hover { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:active { - background-color: hsla(211,20%,52%,0.18); -} - -.c25.c25.c25.c25.c25:focus-visible { - background-color: hsla(211,20%,52%,0.18); - outline: 1px solid hsla(227,100%,59%,0.09); - box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); -} - -.c25.c25.c25.c25.c25 * { - -webkit-transition-property: color,fill,opacity; - transition-property: color,fill,opacity; - -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); -} - -.c11.c11.c11.c11.c11 { - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); - -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); - transition-duration: cubic-bezier(0.3,0,0.2,1); - -webkit-transition-timing-function: 150px; - transition-timing-function: 150px; -} - -.c31.c31.c31.c31.c31 { - color: hsla(211,33%,21%,1); - font-family: "Inter","Inter Fallback Arial",Arial; - font-size: 0.75rem; - font-weight: 600; - 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; - margin: 0; - padding: 0; -} - -.c13.c13.c13.c13.c13 { - opacity: 1; } -.c6.c6.c6.c6.c6 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 20px; +.c20.c20.c20.c20.c20 { + background-color: hsla(0,0%,100%,1); } .c22.c22.c22.c22.c22 { - border-width: 0; - border-left-style: solid; - border-left-width: 1px; - -webkit-align-self: stretch; - -ms-flex-item-align: stretch; - align-self: stretch; - height: 16px; -} - -.c16.c16.c16.c16.c16::-webkit-scrollbar { - display: none; + position: relative; + height: 100%; + width: 100%; } -.c9.c9.c9.c9.c9 { - position: absolute; - left: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; +.c24.c24.c24.c24.c24 { + 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; z-index: 1; + height: 100%; } - -.c9.c9.c9.c9.c9:before { - content: ''; - pointer-events: none; - position: absolute; - left: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to left,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); -} - -.c23.c23.c23.c23.c23 { - position: absolute; - right: 0; - pointer-events: none; - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - transform: scale(0.5); - opacity: 0; - -webkit-transition-timing-function: cubic-bezier(0.5,0,0,1); - transition-timing-function: cubic-bezier(0.5,0,0,1); - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-property: opacity,-webkit-transform; - -webkit-transition-property: opacity,transform; - transition-property: opacity,transform; - z-index: 1; + +.c13.c13.c13.c13.c13 { + border-width: 0; + border-left-style: solid; + border-left-width: 1px; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + height: 16px; } -.c23.c23.c23.c23.c23:before { - content: ''; - pointer-events: none; - position: absolute; - right: 0; - top: -8px; - bottom: -8px; - width: 54px; - background: linear-gradient(to right,transparent 0%,hsla(0,0%,100%,1) 30%,hsla(0,0%,100%,1) 100%); +.c25.c25.c25.c25.c25 { + color: hsla(211,33%,21%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.75rem; + font-weight: 600; + 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; + margin: 0; + padding: 0; } -.c20.c20.c20.c20.c20 { +.c11.c11.c11.c11.c11 { color: hsla(211,26%,34%,1); font-family: "Inter","Inter Fallback Arial",Arial; font-size: 0.875rem; @@ -479,13 +266,19 @@ exports[` should render TopNav ssr 2`] = ` padding-left: 12px; padding-right: 12px; border-radius: 4px; + border: none; + background: none; } -.c20.c20.c20.c20.c20:hover { +.c11.c11.c11.c11.c11[aria-expanded="true"] { background-color: hsla(211,20%,52%,0.12); } -.c18.c18.c18.c18.c18 { +.c11.c11.c11.c11.c11:hover { + background-color: hsla(211,20%,52%,0.12); +} + +.c9.c9.c9.c9.c9 { position: relative; -webkit-flex-shrink: 0; -ms-flex-negative: 0; @@ -494,21 +287,17 @@ exports[` should render TopNav ssr 2`] = ` background-color: transparent; border-color: transparent; border-style: solid; - border-bottom-width: 0; border-width: 1px; + border-bottom-width: 0; border-top-left-radius: 4px; border-top-right-radius: 4px; - -webkit-transform: none; - -ms-transform: none; - transform: none; -webkit-transition: 250ms cubic-bezier(0.3,0,0.2,1); transition: 250ms cubic-bezier(0.3,0,0.2,1); - -webkit-transition-property: background,-webkit-transform; - -webkit-transition-property: background,transform; - transition-property: background,transform; + -webkit-transition-property: background; + transition-property: background; } -.c19.c19.c19.c19.c19 { +.c10.c10.c10.c10.c10 { position: absolute; top: 0; left: 0; @@ -525,7 +314,7 @@ exports[` should render TopNav ssr 2`] = ` transition-property: opacity; } -.c27.c27.c27.c27.c27 { +.c21.c21.c21.c21.c21 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -536,7 +325,7 @@ exports[` should render TopNav ssr 2`] = ` outline: 0.5px solid hsla(214,28%,84%,1); } -.c29.c29.c29.c29.c29 { +.c23.c23.c23.c23.c23 { display: block; text-align: center; -webkit-text-decoration: none; @@ -550,7 +339,7 @@ exports[` should render TopNav ssr 2`] = ` background-color: hsla(211,20%,52%,0.18); } -.c29.c29.c29.c29.c29 img { +.c23.c23.c23.c23.c23 img { display: block; height: 36px; width: 36px; @@ -558,130 +347,132 @@ exports[` should render TopNav ssr 2`] = ` object-fit: cover; } -@media screen and (min-width:768px) { - .c1.c1.c1.c1.c1 { - grid-template-columns: auto minmax(0,1fr) auto; - } +.c15.c15.c15.c15.c15 { + min-height: 36px; + height: 36px; + width: 36px; + cursor: pointer; + background-color: hsla(211,20%,52%,0.12); + border-color: hsla(214,28%,84%,1); + border-width: 0px; + border-radius: 4px; + border-style: solid; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 0px; + padding-right: 0px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-transition-property: background-color,border-color,box-shadow; + transition-property: background-color,border-color,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: 150ms; + transition-duration: 150ms; + position: relative; } -@media screen and (min-width:768px) { - .c2.c2.c2.c2.c2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } +.c15.c15.c15.c15.c15:hover { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:1200px) { - .c2.c2.c2.c2.c2 { - width: 264px; - } +.c15.c15.c15.c15.c15:active { + background-color: hsla(211,20%,52%,0.18); } -@media screen and (min-width:768px) { - .c4.c4.c4.c4.c4 { - display: block; - } +.c15.c15.c15.c15.c15:focus-visible { + background-color: hsla(211,20%,52%,0.18); + outline: 1px solid hsla(227,100%,59%,0.09); + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); } -@media screen and (min-width:320px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c15.c15.c15.c15.c15 * { + -webkit-transition-property: color,fill,opacity; + transition-property: color,fill,opacity; + -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); } -@media screen and (min-width:480px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c16.c16.c16.c16.c16 { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); + transition-duration: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: 150px; + transition-timing-function: 150px; } -@media screen and (min-width:768px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; - } +.c18.c18.c18.c18.c18 { + opacity: 1; } -@media screen and (min-width:1024px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; +@media screen and (min-width:768px) { + .c1.c1.c1.c1.c1 { + padding-top: 0px; + padding-bottom: 0px; + padding-right: 8px; + padding-left: 8px; } } @media screen and (min-width:1200px) { - .c5.c5.c5.c5.c5 { - border-left-style: solid; + .c2.c2.c2.c2.c2 { + width: 264px; } } @media screen and (min-width:768px) { - .c7.c7.c7.c7.c7 { + .c4.c4.c4.c4.c4 { padding-right: 80px; - margin-left: 12px; } } @media screen and (min-width:320px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:480px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:768px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1024px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } @media screen and (min-width:1200px) { - .c21.c21.c21.c21.c21 { + .c12.c12.c12.c12.c12 { border-left-style: solid; } } -@media screen and (min-width:320px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:480px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:768px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1024px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - -@media screen and (min-width:1200px) { - .c32.c32.c32.c32.c32 { - pointer-events: none; - } -} - @@ -724,244 +515,173 @@ exports[` should render TopNav ssr 2`] = ` - - -
AH
AH @@ -1052,44 +772,6 @@ exports[` should render TopNav ssr 2`] = `
AH @@ -1048,44 +768,6 @@ exports[`TopNav should render 1`] = `
TabNav
TabNavItem
items
Menu