diff --git a/packages/drawer/README.md b/packages/drawer/README.md index 41103a972a..08fc43b39f 100644 --- a/packages/drawer/README.md +++ b/packages/drawer/README.md @@ -26,7 +26,9 @@ npm install @leafygreen-ui/drawer ## Example -### Single Overlay Drawer +### Single Overlay Drawer without Toolbar + +#### Without `OverlayDrawerLayout` ```tsx import React, { useState } from 'react'; @@ -59,7 +61,35 @@ function ExampleComponent() { } ``` -### Multiple Overlay Drawers +or + +#### With `OverlayDrawerLayout` + +```tsx +function ExampleComponent() { + const [open, setOpen] = useState(false); + + return ( + + + + setOpen(false)} + open={open} + title="Drawer Title" + > + content + + + + ); +} +``` + +### Multiple Overlay Drawers without Toolbar ```tsx import React, { useState } from 'react'; @@ -104,7 +134,11 @@ function ExampleComponent() { } ``` -### Embedded Drawer +### Overlay Drawer with Toolbar + +// TODO: in separate PR + +### Embedded Drawer without Toolbar ```tsx import React, { useState } from 'react'; @@ -142,6 +176,10 @@ function ExampleComponent() { } ``` +### Embedded Drawer with Toolbar + +// TODO: in separate PR + ## Properties ### Drawer @@ -156,9 +194,24 @@ function ExampleComponent() { ### EmbeddedDrawerLayout +Use `EmbeddedDrawerLayout` when you need an embedded `Drawer` within a specific container. + | Prop | Type | Description | Default | | -------------- | --------- | ----------------------------------------------------- | ------- | | `isDrawerOpen` | `boolean` | Determines if the `Drawer` instance is open or closed | | +| `hasToolbar` | `boolean` | Determines if the `Toolbar` is present in the layout | `false` | + +### OverlayDrawerLayout + +Use `OverlayDrawerLayout` when you need an overlay `Drawer` that is positioned relative to a specific container rather than the entire app. The `OverlayDrawerLayout` adds `position: relative` to its container, ensuring the `Drawer` overlays only its parent element. If the `Drawer` should overlay the entire application, this layout is not necessary. + +| Prop | Type | Description | Default | +| ------------ | --------- | ---------------------------------------------------- | ------- | +| `hasToolbar` | `boolean` | Determines if the `Toolbar` is present in the layout | `false` | + +## DrawerToolbarLayout + +// TODO: Add in separate PR # Test Harnesses @@ -240,10 +293,11 @@ const { findDrawer, getCloseButtonUtils, getDrawer, isOpen, queryDrawer } = getTestUtils(); ``` -| Util | Description | Returns | +// TODO: toolbar utils in a separate PR +| Util | Description | Returns | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| `findDrawer` | Returns a promise that resolves to the drawer element. The promise is rejected if no elements match or if more than one match is found. | `Promise` \| `Promise` | -| `getCloseButtonUtils` | Returns the button test utils for the close button | [Button test utils return type](https://github.com/mongodb/leafygreen-ui/blob/main/packages/button/README.md#test-utils) | -| `getDrawer` | Returns the drawer element and throws if no elements match or if more than one match is found. | `HTMLDivElement` | -| `isOpen` | Checks the `aria-hidden` attribute and that the drawer element is visible based on CSS properties for `display`, `opacity`, `transform`, and `visibility` | `boolean` | -| `queryDrawer` | Returns the drawer element or `null` if no elements match and throws if more than one match is found. | `HTMLDivElement` | +| `findDrawer` | Returns a promise that resolves to the drawer element. The promise is rejected if no elements match or if more than one match is found. | `Promise` \| `Promise` | +| `getCloseButtonUtils` | Returns the button test utils for the close button | [Button test utils return type](https://github.com/mongodb/leafygreen-ui/blob/main/packages/button/README.md#test-utils) | +| `getDrawer` | Returns the drawer element and throws if no elements match or if more than one match is found. | `HTMLDivElement` | +| `isOpen` | Checks the `aria-hidden` attribute and that the drawer element is visible based on CSS properties for `display`, `opacity`, `transform`, and `visibility` | `boolean` | +| `queryDrawer` | Returns the drawer element or `null` if no elements match and throws if more than one match is found. | `HTMLDivElement` | diff --git a/packages/drawer/package.json b/packages/drawer/package.json index 883b3d659f..43138ee384 100644 --- a/packages/drawer/package.json +++ b/packages/drawer/package.json @@ -25,6 +25,7 @@ "@leafygreen-ui/polymorphic": "workspace:^", "@leafygreen-ui/tabs": "workspace:^", "@leafygreen-ui/tokens": "workspace:^", + "@leafygreen-ui/toolbar": "workspace:^", "@leafygreen-ui/typography": "workspace:^", "@lg-tools/test-harnesses": "workspace:^", "polished": "^4.2.2", diff --git a/packages/drawer/src/Drawer.stories.tsx b/packages/drawer/src/Drawer.stories.tsx index dcb1458513..e43dae4bdb 100644 --- a/packages/drawer/src/Drawer.stories.tsx +++ b/packages/drawer/src/Drawer.stories.tsx @@ -9,12 +9,14 @@ import { StoryFn, StoryObj } from '@storybook/react'; import Button from '@leafygreen-ui/button'; import { css } from '@leafygreen-ui/emotion'; +import { palette } from '@leafygreen-ui/palette'; import { spacing } from '@leafygreen-ui/tokens'; import { Body } from '@leafygreen-ui/typography'; import { DisplayMode, Drawer, DrawerProps } from './Drawer'; import { DrawerStackProvider } from './DrawerStackContext'; import { EmbeddedDrawerLayout } from './EmbeddedDrawerLayout'; +import { OverlayDrawerLayout } from './OverlayDrawerLayout'; const SEED = 0; faker.seed(SEED); @@ -25,6 +27,13 @@ const defaultExcludedControls = [ 'open', ]; +const snapshotStoryExcludedControlParams = [ + ...defaultExcludedControls, + 'darkMode', + 'displayMode', + 'title', +]; + export default { title: 'Components/Drawer', component: Drawer, @@ -33,9 +42,11 @@ export default {
@@ -69,7 +80,7 @@ export default { const LongContent = () => { const paragraphs = useMemo(() => { return faker.lorem - .paragraphs(20, '\n') + .paragraphs(30, '\n') .split('\n') .map((p, i) => {p}); }, []); @@ -115,7 +126,7 @@ const TemplateComponent: StoryFn = ({ @@ -137,9 +148,29 @@ const TemplateComponent: StoryFn = ({ ) : ( -
- {renderTrigger()} - {renderDrawer()} +
+ +
+ {renderTrigger()} + +
+ {renderDrawer()} +
); @@ -149,7 +180,6 @@ export const LiveExample: StoryObj = { render: TemplateComponent, args: { children: , - initialOpen: false, }, parameters: { chromatic: { @@ -228,13 +258,6 @@ export const MultipleDrawers: StoryObj = { }, }; -const snapshotStoryExcludedControlParams = [ - ...defaultExcludedControls, - 'darkMode', - 'displayMode', - 'title', -]; - export const LightModeOverlay: StoryObj = { render: TemplateComponent, args: { diff --git a/packages/drawer/src/Drawer/Drawer.constants.ts b/packages/drawer/src/Drawer/Drawer.constants.ts index 7b4b80c9ee..2240190c59 100644 --- a/packages/drawer/src/Drawer/Drawer.constants.ts +++ b/packages/drawer/src/Drawer/Drawer.constants.ts @@ -1,3 +1,4 @@ export const HEADER_HEIGHT = 48; export const MOBILE_BREAKPOINT = 390; export const PANEL_WIDTH = 432; +export const TOOLBAR_WIDTH = 48; diff --git a/packages/drawer/src/Drawer/Drawer.styles.ts b/packages/drawer/src/Drawer/Drawer.styles.ts index 2d751f00fc..ca10fd014c 100644 --- a/packages/drawer/src/Drawer/Drawer.styles.ts +++ b/packages/drawer/src/Drawer/Drawer.styles.ts @@ -1,5 +1,5 @@ -import { css, cx } from '@leafygreen-ui/emotion'; -import { Theme } from '@leafygreen-ui/lib'; +import { css, cx, keyframes } from '@leafygreen-ui/emotion'; +import { createUniqueClassName, Theme } from '@leafygreen-ui/lib'; import { addOverflowShadow, color, @@ -17,15 +17,50 @@ import { DisplayMode } from './Drawer.types'; export const drawerTransitionDuration = transitionDuration.slower; -const getBaseStyles = ({ open, theme }: { open: boolean; theme: Theme }) => css` +export const drawerClassName = createUniqueClassName('lg-drawer'); + +// Because of .show() and .close() in the drawer component, transitioning from 0px to (x)px does not transition correctly. Having the drawer start at the open position while hidden, moving to the closed position, and then animating to the open position is a workaround to get the animation to work. +// These styles are used for a standalone drawer in overlay mode since it is not part of a grid layout. +const drawerIn = keyframes` + 0% { + transform: translate3d(0%, 0, 0); + opacity: 0; + visibility: visible; // Adding visibility: hidden break autoFocus of the close button when using the dialog component + } + 1% { + transform: translate3d(100%, 0, 0); + opacity: 1; + } + 100% { + transform: translate3d(0%, 0, 0); + } +`; + +// Keep the drawer opacity at 1 until the end of the animation. The inner container opacity is transitioned separately. +const drawerOut = keyframes` + 0% { + transform: translate3d(0%, 0, 0); + } + 99% { + transform: translate3d(100%, 0, 0); + opacity: 1; + } + 100% { + opacity: 0; + visibility: hidden; + } +`; + +const getBaseStyles = ({ theme }: { theme: Theme }) => css` all: unset; + padding: 0; background-color: ${color[theme].background.primary.default}; - border: ${open - ? `1px solid ${color[theme].border.secondary.default}` - : 'none'}; + border: 1px solid ${color[theme].border.secondary.default}; width: 100%; max-width: ${PANEL_WIDTH}px; height: 100%; + overflow: hidden; + box-sizing: border-box; @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { max-width: 100%; @@ -35,113 +70,126 @@ const getBaseStyles = ({ open, theme }: { open: boolean; theme: Theme }) => css` const overlayOpenStyles = css` opacity: 1; - transform: none; + animation-name: ${drawerIn}; + // On mobile, the drawer should be positioned at the bottom of the screen when closed, and slide up to the top when opened. @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { transform: none; } `; const overlayClosedStyles = css` - opacity: 0; - transform: translate3d(100%, 0, 0); pointer-events: none; + animation-name: ${drawerOut}; + // On mobile, the drawer should be positioned at the bottom of the screen when closed, and slide up to the top when opened. @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { transform: translate3d(0, 100%, 0); + opacity: 0; } `; const getOverlayStyles = ({ open, + shouldAnimate, zIndex, }: { open: boolean; + shouldAnimate: boolean; zIndex: number; }) => cx( css` - position: fixed; + position: absolute; z-index: ${zIndex}; top: 0; bottom: 0; right: 0; overflow: visible; - transition: transform ${drawerTransitionDuration}ms ease-in-out, - opacity ${drawerTransitionDuration}ms ease-in-out - ${open ? '0ms' : `${drawerTransitionDuration}ms`}; + + // By default, the drawer is positioned off-screen to the right. + transform: translate3d(100%, 0, 0); + animation-timing-function: ease-in-out; + animation-duration: ${drawerTransitionDuration}ms; + animation-fill-mode: forwards; @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { top: unset; left: 0; + // Since the drawer has position: fixed, we can use normal transitions + animation: none; + position: fixed; + transform: translate3d(0, 100%, 0); + transition: transform ${drawerTransitionDuration}ms ease-in-out, + opacity ${drawerTransitionDuration}ms ease-in-out + ${open ? '0ms' : `${drawerTransitionDuration}ms`}; } `, { [overlayOpenStyles]: open, - [overlayClosedStyles]: !open, - }, - ); - -const embeddedOpenStyles = css` - width: 100%; - - @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { - height: 50vh; - } -`; - -const embeddedClosedStyles = css` - width: 0; - - @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { - width: 100%; - height: 0; - } -`; - -const getEmbeddedStyles = ({ open }: { open: boolean }) => - cx( - css` - position: relative; - overflow: auto; - `, - { - [embeddedOpenStyles]: open, - [embeddedClosedStyles]: !open, + [overlayClosedStyles]: !open && shouldAnimate, // This ensures that the drawer does not animate closed on initial render }, ); const getDisplayModeStyles = ({ displayMode, open, + shouldAnimate, zIndex, }: { displayMode: DisplayMode; open: boolean; + shouldAnimate: boolean; zIndex: number; }) => cx({ - [getOverlayStyles({ open, zIndex })]: displayMode === DisplayMode.Overlay, - [getEmbeddedStyles({ open })]: displayMode === DisplayMode.Embedded, + [getOverlayStyles({ open, shouldAnimate, zIndex })]: + displayMode === DisplayMode.Overlay, }); export const getDrawerStyles = ({ className, displayMode, open, + shouldAnimate, theme, zIndex, }: { className?: string; displayMode: DisplayMode; open: boolean; + shouldAnimate: boolean; theme: Theme; zIndex: number; }) => cx( - getBaseStyles({ open, theme }), - getDisplayModeStyles({ displayMode, open, zIndex }), + getBaseStyles({ theme }), + getDisplayModeStyles({ displayMode, open, shouldAnimate, zIndex }), className, + drawerClassName, + ); + +export const getDrawerShadowStyles = ({ + theme, + displayMode, +}: { + theme: Theme; + displayMode: DisplayMode; +}) => + cx( + css` + height: 100%; + background-color: ${color[theme].background.primary.default}; + `, + { + [css` + ${addOverflowShadow({ isInside: false, side: Side.Left, theme })}; + + @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { + ${addOverflowShadow({ isInside: false, side: Side.Top, theme })}; + } + `]: displayMode === DisplayMode.Overlay, + }, ); const getBaseInnerContainerStyles = ({ theme }: { theme: Theme }) => css` @@ -150,43 +198,40 @@ const getBaseInnerContainerStyles = ({ theme }: { theme: Theme }) => css` display: flex; flex-direction: column; background-color: ${color[theme].background.primary.default}; + opacity: 0; + transition-property: opacity; + transition-duration: ${transitionDuration.faster}ms; + transition-timing-function: linear; `; -const getDrawerShadowStyles = ({ theme }: { theme: Theme }) => css` - ${addOverflowShadow({ isInside: false, side: Side.Left, theme })}; - - @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { - ${addOverflowShadow({ isInside: false, side: Side.Top, theme })}; - } +const getInnerOpenContainerStyles = css` + transition-property: opacity; + transition-duration: ${transitionDuration.slowest}ms; + transition-timing-function: linear; + opacity: 1; `; export const getInnerContainerStyles = ({ - displayMode, theme, + open, }: { - displayMode: DisplayMode; theme: Theme; + open: boolean; }) => cx(getBaseInnerContainerStyles({ theme }), { - [getDrawerShadowStyles({ theme })]: displayMode === DisplayMode.Overlay, + [getInnerOpenContainerStyles]: open, }); -export const getHeaderStyles = ({ - hasTabs, - theme, -}: { - hasTabs: boolean; - theme: Theme; -}) => css` +export const getHeaderStyles = ({ theme }: { theme: Theme }) => css` height: ${HEADER_HEIGHT}px; padding: ${spacing[400]}px; display: flex; justify-content: space-between; align-items: center; - border-bottom: ${hasTabs - ? 'none' - : `1px solid ${color[theme].border.secondary.default}`}; - transition: box-shadow ${transitionDuration.faster}ms ease-in-out; + border-bottom: 1px solid ${color[theme].border.secondary.default}; + transition-property: box-shadow; + transition-duration: ${transitionDuration.faster}ms; + transition-timing-function: ease-in-out; `; const baseChildrenContainerStyles = css` @@ -216,11 +261,7 @@ const scrollContainerStyles = css` overscroll-behavior: contain; `; -export const getInnerChildrenContainerStyles = ({ - hasTabs, -}: { - hasTabs: boolean; -}) => - cx(baseInnerChildrenContainerStyles, { - [scrollContainerStyles]: !hasTabs, - }); +export const innerChildrenContainerStyles = cx( + baseInnerChildrenContainerStyles, + scrollContainerStyles, +); diff --git a/packages/drawer/src/Drawer/Drawer.tsx b/packages/drawer/src/Drawer/Drawer.tsx index 27785b8f74..87ce6c223b 100644 --- a/packages/drawer/src/Drawer/Drawer.tsx +++ b/packages/drawer/src/Drawer/Drawer.tsx @@ -15,17 +15,17 @@ import { usePolymorphic } from '@leafygreen-ui/polymorphic'; import { BaseFontSize } from '@leafygreen-ui/tokens'; import { Body } from '@leafygreen-ui/typography'; -import { DrawerContext } from '../DrawerContext'; import { useDrawerStackContext } from '../DrawerStackContext'; import { DEFAULT_LGID_ROOT, getLgIds } from '../utils'; import { drawerTransitionDuration, getChildrenContainerStyles, + getDrawerShadowStyles, getDrawerStyles, getHeaderStyles, - getInnerChildrenContainerStyles, getInnerContainerStyles, + innerChildrenContainerStyles, } from './Drawer.styles'; import { DisplayMode, DrawerProps } from './Drawer.types'; @@ -50,12 +50,10 @@ export const Drawer = forwardRef( ); const { getDrawerIndex, registerDrawer, unregisterDrawer } = useDrawerStackContext(); - + const [shouldAnimate, setShouldAnimate] = useState(false); const ref = useRef(null); const drawerRef = useMergeRefs([fwdRef, ref]); - const [hasTabs, setHasTabs] = useState(false); - const lgIds = getLgIds(dataLgId); const id = useIdAllocator({ prefix: 'drawer', id: idProp }); const titleId = useIdAllocator({ prefix: 'drawer' }); @@ -78,6 +76,7 @@ export const Drawer = forwardRef( if (open) { drawerElement.show(); + setShouldAnimate(true); } else { drawerElement.close(); } @@ -93,33 +92,31 @@ export const Drawer = forwardRef( return ( - setHasTabs(true) }} + - +
@@ -127,9 +124,8 @@ export const Drawer = forwardRef( as={typeof title === 'string' ? 'h2' : 'div'} baseFontSize={BaseFontSize.Body2} id={titleId} - weight="medium" > - {title} + {title} {showCloseButton && ( (
-
+
{/* Empty span element used to track if children container has scrolled down */} - {!hasTabs && } + {} {children}
- - +
+ ); }, diff --git a/packages/drawer/src/Drawer/index.ts b/packages/drawer/src/Drawer/index.ts index c91c8a0efe..a5262d99ba 100644 --- a/packages/drawer/src/Drawer/index.ts +++ b/packages/drawer/src/Drawer/index.ts @@ -1,3 +1,7 @@ export { Drawer } from './Drawer'; -export { MOBILE_BREAKPOINT, PANEL_WIDTH } from './Drawer.constants'; +export { + MOBILE_BREAKPOINT, + PANEL_WIDTH, + TOOLBAR_WIDTH, +} from './Drawer.constants'; export { DisplayMode, type DrawerProps } from './Drawer.types'; diff --git a/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout.stories.tsx b/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout.stories.tsx new file mode 100644 index 0000000000..5e024feee7 --- /dev/null +++ b/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout.stories.tsx @@ -0,0 +1,321 @@ +/* eslint-disable no-console */ +import React, { useMemo } from 'react'; +import { faker } from '@faker-js/faker'; +import { + storybookArgTypes, + storybookExcludedControlParams, + StoryMetaType, +} from '@lg-tools/storybook-utils'; +import { StoryFn, StoryObj } from '@storybook/react'; + +import { css } from '@leafygreen-ui/emotion'; +import { palette } from '@leafygreen-ui/palette'; +import { spacing } from '@leafygreen-ui/tokens'; +import { Body } from '@leafygreen-ui/typography'; + +import { DisplayMode, Drawer, DrawerProps } from '../Drawer'; +import { + DrawerToolbarLayout, + DrawerToolbarLayoutProps, +} from '../DrawerToolbarLayout'; + +const SEED = 0; +faker.seed(SEED); + +const defaultExcludedControls = [ + ...storybookExcludedControlParams, + 'children', + 'open', +]; + +const toolbarExcludedControls = [ + ...defaultExcludedControls, + 'displayMode', + 'title', +]; + +export default { + title: 'Components/Drawer/Toolbar', + component: Drawer, + decorators: [ + StoryFn => ( +
+ +
+ ), + ], + parameters: { + default: null, + controls: { + exclude: defaultExcludedControls, + }, + }, + args: { + displayMode: DisplayMode.Overlay, + title: 'Drawer Title', + }, + argTypes: { + darkMode: storybookArgTypes.darkMode, + displayMode: { + control: 'radio', + description: 'Options to control how the drawer element is displayed', + options: Object.values(DisplayMode), + }, + title: { + control: 'text', + description: 'Title of the Drawer', + }, + }, +} satisfies StoryMetaType; + +const LongContent = () => { + const paragraphs = useMemo(() => { + const text = faker.lorem.paragraphs(30, '\n'); + return text.split('\n').map((p, i) => {p}); + }, []); + + return ( +
+ {paragraphs} +
+ ); +}; + +const DrawerContent = () => { + // Generate a unique seed based on timestamp for different content each time + React.useEffect(() => { + faker.seed(Date.now()); + }, []); + + // Generate paragraphs without memoization so they're different each render + const paragraphs = faker.lorem + .paragraphs(30, '\n') + .split('\n') + .map((p, i) => {p}); + + return ( +
+ {paragraphs} +
+ ); +}; + +const CloudNavLayoutMock: React.FC<{ children?: React.ReactNode }> = ({ + children, +}) => ( +
+
+
+
+
+ {children} +
+
+); + +const DRAWER_TOOLBAR_DATA: DrawerToolbarLayoutProps['data'] = [ + { + id: 'Code', + label: 'Code', + content: , + title: 'Code Title', + glyph: 'Code', + onClick: () => { + console.log('Code clicked'); + }, + }, + { + id: 'Dashboard', + label: 'Dashboard', + content: , + title: 'Dashboard Title', + glyph: 'Dashboard', + onClick: () => { + console.log('Dashboard clicked'); + }, + }, +]; + +const EmbeddedComponent: StoryFn = (args: DrawerProps) => { + return ( +
+ +
+ +
+
+
+ ); +}; + +const OverlayComponent: StoryFn = (args: DrawerProps) => { + return ( +
+ +
+ + +
+
+
+ ); +}; + +const OverlayCloudNavComponent: StoryFn = (args: DrawerProps) => { + return ( + + +
+ + +
+
+
+ ); +}; + +export const OverlayCloudNav: StoryObj = { + render: OverlayCloudNavComponent, + parameters: { + controls: { + exclude: toolbarExcludedControls, + }, + }, +}; + +export const Overlay: StoryObj = { + render: OverlayComponent, + parameters: { + controls: { + exclude: toolbarExcludedControls, + }, + }, +}; + +const EmbeddedCloudNavComponent: StoryFn = (args: DrawerProps) => { + return ( + + +
+ + +
+
+
+ ); +}; + +export const EmbeddedCloudNav: StoryObj = { + render: EmbeddedCloudNavComponent, + parameters: { + controls: { + exclude: toolbarExcludedControls, + }, + }, +}; + +export const Embedded: StoryObj = { + render: EmbeddedComponent, + parameters: { + controls: { + exclude: toolbarExcludedControls, + }, + }, +}; diff --git a/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout.tsx b/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout.tsx new file mode 100644 index 0000000000..b0e5b1f804 --- /dev/null +++ b/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import { DrawerToolbarLayoutProps } from './DrawerToolbarLayout.types'; +import { DrawerToolbarLayoutContainer } from './DrawerToolbarLayoutContainer'; + +export const DrawerToolbarLayout = ({ + children, + ...rest +}: DrawerToolbarLayoutProps) => { + return ( + + {children} + + ); +}; + +DrawerToolbarLayout.displayName = 'DrawerToolbarLayout'; diff --git a/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout.types.ts b/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout.types.ts new file mode 100644 index 0000000000..49383b8850 --- /dev/null +++ b/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout.types.ts @@ -0,0 +1,53 @@ +import React, { PropsWithChildren } from 'react'; + +import { GlyphName } from '@leafygreen-ui/icon'; +import { DarkModeProps } from '@leafygreen-ui/lib'; + +import { DrawerProps } from '../Drawer/Drawer.types'; + +type PickedRequiredDrawerProps = Required>; +type PickedOptionalDrawerProps = Pick; + +interface LayoutData { + /** + * The id of the layout. This is used to open the drawer. + */ + id: string; + + /** + * The LG Icon that will render in the ToolbarIcon. + */ + glyph: GlyphName; + + /** + * The content of the drawer. + */ + content: React.ReactNode; + + /** + * The text that will render in the tooltip on hover. + */ + label: React.ReactNode; + + /** + * Callback function that is called when the toolbar item is clicked. + * @param event + * @returns void + */ + onClick?: (event: React.MouseEvent) => void; + + /** + * The title of the drawer. + */ + title: string; +} + +export type DrawerToolbarLayoutProps = PickedOptionalDrawerProps & + PickedRequiredDrawerProps & + DarkModeProps & + PropsWithChildren<{ + data?: Array; + className?: string; + }>; + +export type DrawerToolbarLayoutContainerProps = DrawerToolbarLayoutProps; diff --git a/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayoutContainer.tsx b/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayoutContainer.tsx new file mode 100644 index 0000000000..76193d00f8 --- /dev/null +++ b/packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayoutContainer.tsx @@ -0,0 +1,140 @@ +import React, { useState } from 'react'; + +import { css } from '@leafygreen-ui/emotion'; +import LeafyGreenProvider, { + useDarkMode, +} from '@leafygreen-ui/leafygreen-provider'; +import { spacing } from '@leafygreen-ui/tokens'; +import { Toolbar, ToolbarIconButton } from '@leafygreen-ui/toolbar'; +import { Body } from '@leafygreen-ui/typography'; + +import { GRID_AREA } from '../constants'; +import { Drawer } from '../Drawer/Drawer'; +import { DisplayMode } from '../Drawer/Drawer.types'; +import { DrawerStackProvider } from '../DrawerStackContext'; +import { DrawerWithToolbarWrapper } from '../DrawerWithToolbarWrapper'; +import { EmbeddedDrawerLayout } from '../EmbeddedDrawerLayout'; +import { OverlayDrawerLayout } from '../OverlayDrawerLayout'; + +import { DrawerToolbarLayoutContainerProps } from './DrawerToolbarLayout.types'; + +// Dummy content for now +const LongContent = () => { + return ( +
+ + Ipsam aspernatur sequi quo esse recusandae aperiam distinctio quod + dignissimos. Nihil totam accusamus. Odio occaecati commodi sapiente + dolores. + + + Molestiae accusantium porro eum quas praesentium consequuntur deleniti. + Fuga mollitia incidunt atque. Minima nisi fugit sapiente. + + + Ratione explicabo saepe occaecati. Et esse eveniet accusamus veritatis + esse quod. Vero aliquid quasi saepe vel harum molestiae rerum. + + + Minima distinctio eligendi sit culpa tempore adipisci. Consequuntur + consequatur minus quaerat sapiente consectetur esse blanditiis + provident. Nulla quas esse quasi a error sint pariatur possimus quia. + +
+ ); +}; + +export const DrawerToolbarLayoutContainer = ({ + children, + displayMode = DisplayMode.Embedded, + data, + onClose, + darkMode: darkModeProp, +}: DrawerToolbarLayoutContainerProps) => { + const { darkMode } = useDarkMode(darkModeProp); + const [open, setOpen] = useState(false); + + const handleOnClose = (event: React.MouseEvent) => { + onClose?.(event); + setOpen(false); + + // TODO: add call to context to update the drawer content + }; + + const LayoutComponent = + displayMode === DisplayMode.Overlay + ? OverlayDrawerLayout + : EmbeddedDrawerLayout; + + const layoutProps = { + hasToolbar: true, + isDrawerOpen: displayMode === DisplayMode.Embedded ? open : false, + }; + + const handleIconClick = ( + event: React.MouseEvent, + onClick?: (event: React.MouseEvent) => void, + ) => { + onClick?.(event); + setOpen(true); + }; + + return ( + + + +
+ {children} +
+ + + {data?.map(toolbar => ( + ) => + handleIconClick(e, toolbar.onClick) + } + /> + ))} + + + {/* TODO: get content from context */} + {/* Filler for now */} + + + + + + + {/* Filler for now */} + + +
+
+
+ ); +}; + +DrawerToolbarLayoutContainer.displayName = 'DrawerToolbarLayoutContainer'; diff --git a/packages/drawer/src/DrawerToolbarLayout/index.ts b/packages/drawer/src/DrawerToolbarLayout/index.ts new file mode 100644 index 0000000000..03dbc6fceb --- /dev/null +++ b/packages/drawer/src/DrawerToolbarLayout/index.ts @@ -0,0 +1,2 @@ +export { DrawerToolbarLayout } from './DrawerToolbarLayout'; +export type { DrawerToolbarLayoutProps } from './DrawerToolbarLayout.types'; diff --git a/packages/drawer/src/DrawerWithToolbarWrapper/DrawerWithToolbarWrapper.styles.ts b/packages/drawer/src/DrawerWithToolbarWrapper/DrawerWithToolbarWrapper.styles.ts new file mode 100644 index 0000000000..2df1821623 --- /dev/null +++ b/packages/drawer/src/DrawerWithToolbarWrapper/DrawerWithToolbarWrapper.styles.ts @@ -0,0 +1,169 @@ +import { css, cx, keyframes } from '@leafygreen-ui/emotion'; +import { Theme } from '@leafygreen-ui/lib'; +import { addOverflowShadow, breakpoints, Side } from '@leafygreen-ui/tokens'; + +import { GRID_AREA } from '../constants'; +import { PANEL_WIDTH, TOOLBAR_WIDTH } from '../Drawer/Drawer.constants'; +import { + drawerClassName, + drawerTransitionDuration, +} from '../Drawer/Drawer.styles'; +import { DisplayMode } from '../Drawer/Drawer.types'; + +const MOBILE_BREAKPOINT = breakpoints.Tablet; +const SHADOW_WIDTH = 36; // Width of the shadow padding on the left side + +const drawerIn = keyframes` + from { + // Because of .show() and .close() in the drawer component, transitioning from 0px to (x)px does not transition correctly. Using 1px along with css animations is a workaround to get the animation to work. + grid-template-columns: ${TOOLBAR_WIDTH}px 1px; + }, + to { + grid-template-columns: ${TOOLBAR_WIDTH}px ${PANEL_WIDTH}px; + } +`; + +const drawerOut = keyframes` + from { + grid-template-columns: ${TOOLBAR_WIDTH}px ${PANEL_WIDTH}px; + }, + to { + grid-template-columns: ${TOOLBAR_WIDTH}px 0px; + } +`; + +const drawerOutMobile = keyframes` + from { + grid-template-columns: ${TOOLBAR_WIDTH}px calc(100vw - ${ + TOOLBAR_WIDTH * 2 +}px); + }, + to { + grid-template-columns: ${TOOLBAR_WIDTH}px 0px; + } +`; + +const drawerInMobile = keyframes` + from { + grid-template-columns: ${TOOLBAR_WIDTH}px 1px; + }, + to { + grid-template-columns: ${TOOLBAR_WIDTH}px calc(100vw - ${ + TOOLBAR_WIDTH * 2 +}px); + } +`; + +const drawerPaddingOut = keyframes` + 0% { + padding-left: ${SHADOW_WIDTH}px; + } + 99% { + padding-left: ${SHADOW_WIDTH}px; + } + 100% { + padding-left: 0px; + } +`; + +const openStyles = css` + animation-name: ${drawerIn}; + animation-fill-mode: forwards; + + @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { + animation-name: ${drawerInMobile}; + } +`; + +const closedStyles = css` + animation-name: ${drawerOut}; + + @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { + animation-name: ${drawerOutMobile}; + } +`; + +const getDrawerShadowStyles = ({ theme }: { theme: Theme }) => css` + ${addOverflowShadow({ isInside: false, side: Side.Left, theme })}; + + // Need this to show the box shadow since we are using overflow: hidden + padding-left: ${SHADOW_WIDTH}px; + + &::before { + transition-property: opacity; + transition-duration: ${drawerTransitionDuration}ms; + transition-timing-function: ease-in-out; + opacity: 1; + left: ${SHADOW_WIDTH}px; + } +`; + +const baseStyles = css` + display: grid; + grid-template-columns: ${TOOLBAR_WIDTH}px 0px; + grid-template-areas: '${GRID_AREA.toolbar} ${GRID_AREA.innerDrawer}'; + grid-area: ${GRID_AREA.drawer}; + justify-self: end; + animation-timing-function: ease-in-out; + animation-duration: ${drawerTransitionDuration}ms; + z-index: 0; + height: 100%; + overflow: hidden; + + & > :first-child { + grid-area: ${GRID_AREA.toolbar}; + } + + .${drawerClassName} { + grid-area: ${GRID_AREA.innerDrawer}; + position: unset; + transition: none; + transform: unset; + overflow: hidden; + opacity: 1; + border-left: 0; + border-right: 0; + height: 100%; + animation: none; + + > div::before { + box-shadow: unset; + } + } +`; + +const closedDrawerShadowStyles = css` + padding-left: 0; + animation-name: ${drawerPaddingOut}; + animation-timing-function: ease-in-out; + animation-duration: ${drawerTransitionDuration}ms; + + ::before { + opacity: 0; + } +`; + +export const getDrawerWithToolbarWrapperStyles = ({ + className, + isDrawerOpen, + shouldAnimate, + displayMode, + theme, +}: { + className?: string; + isDrawerOpen: boolean; + shouldAnimate?: boolean; + displayMode: DisplayMode; + theme: Theme; +}) => + cx( + baseStyles, + { + [getDrawerShadowStyles({ theme })]: displayMode === DisplayMode.Overlay, + [closedDrawerShadowStyles]: + displayMode === DisplayMode.Overlay && !isDrawerOpen, + [openStyles]: isDrawerOpen, + [closedStyles]: !isDrawerOpen && shouldAnimate, // This ensures that the drawer does not animate closed on initial render + }, + className, + ); diff --git a/packages/drawer/src/DrawerWithToolbarWrapper/DrawerWithToolbarWrapper.tsx b/packages/drawer/src/DrawerWithToolbarWrapper/DrawerWithToolbarWrapper.tsx new file mode 100644 index 0000000000..5c2766257a --- /dev/null +++ b/packages/drawer/src/DrawerWithToolbarWrapper/DrawerWithToolbarWrapper.tsx @@ -0,0 +1,50 @@ +import React, { forwardRef, useEffect, useState } from 'react'; + +import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; + +import { getDrawerWithToolbarWrapperStyles } from './DrawerWithToolbarWrapper.styles'; +import { DrawerWithToolbarWrapperProps } from './DrawerWithToolbarWrapper.types'; + +/** + * This layout wrapper is used to position the toolbar and drawer together. When the drawer is open, the toolbar and drawer will shift to the right. + * + * If the drawer is overlay, a box shadow will be applied to the left side of this component. + */ +export const DrawerWithToolbarWrapper = forwardRef< + HTMLDivElement, + DrawerWithToolbarWrapperProps +>( + ( + { + children, + className, + isDrawerOpen, + displayMode, + }: DrawerWithToolbarWrapperProps, + forwardedRef, + ) => { + const { theme } = useDarkMode(); + const [shouldAnimate, setShouldAnimate] = useState(false); + + useEffect(() => { + if (isDrawerOpen) setShouldAnimate(true); + }, [isDrawerOpen]); + + return ( +
+ {children} +
+ ); + }, +); + +DrawerWithToolbarWrapper.displayName = 'DrawerWithToolbarWrapper'; diff --git a/packages/drawer/src/DrawerWithToolbarWrapper/DrawerWithToolbarWrapper.types.ts b/packages/drawer/src/DrawerWithToolbarWrapper/DrawerWithToolbarWrapper.types.ts new file mode 100644 index 0000000000..c3ecea654c --- /dev/null +++ b/packages/drawer/src/DrawerWithToolbarWrapper/DrawerWithToolbarWrapper.types.ts @@ -0,0 +1,14 @@ +import { HTMLElementProps } from '@leafygreen-ui/lib'; + +import { DrawerProps } from '../Drawer/Drawer.types'; + +type PickedDrawerProps = Required>; + +export interface DrawerWithToolbarWrapperProps + extends HTMLElementProps<'div'>, + PickedDrawerProps { + /** + * Determines if the Drawer instance is open or closed + */ + isDrawerOpen: boolean; +} diff --git a/packages/drawer/src/DrawerWithToolbarWrapper/index.ts b/packages/drawer/src/DrawerWithToolbarWrapper/index.ts new file mode 100644 index 0000000000..f7179c70b1 --- /dev/null +++ b/packages/drawer/src/DrawerWithToolbarWrapper/index.ts @@ -0,0 +1 @@ +export { DrawerWithToolbarWrapper } from './DrawerWithToolbarWrapper'; diff --git a/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.styles.ts b/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.styles.ts index 9f156cad94..8ff937d094 100644 --- a/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.styles.ts +++ b/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.styles.ts @@ -1,38 +1,66 @@ import { css, cx } from '@leafygreen-ui/emotion'; +import { breakpoints } from '@leafygreen-ui/tokens'; -import { MOBILE_BREAKPOINT, PANEL_WIDTH } from '../Drawer'; +import { GRID_AREA } from '../constants'; +import { MOBILE_BREAKPOINT, PANEL_WIDTH, TOOLBAR_WIDTH } from '../Drawer'; +import { drawerTransitionDuration } from '../Drawer/Drawer.styles'; const baseStyles = css` width: 100%; display: grid; grid-template-columns: auto 0; + transition: all ${drawerTransitionDuration}ms ease-in-out; + overflow: hidden; + position: relative; + height: 100%; +`; +const drawerBaseStyles = css` @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { grid-template-columns: unset; - grid-template-rows: auto 0; + grid-template-rows: 100% 0; } `; +// If there is no toolbar and the drawer is open, we need to shift the layout by the panel width; const drawerOpenStyles = css` grid-template-columns: auto ${PANEL_WIDTH}px; @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { - grid-template-columns: unset; grid-template-rows: 50% 50%; } `; +const withToolbarBaseStyles = css` + grid-template-columns: auto ${TOOLBAR_WIDTH}px; + grid-template-areas: '${GRID_AREA.content} ${GRID_AREA.drawer}'; +`; + +// If there is a toolbar and the drawer is open, we need to shift the layout by toolbar width + panel width; +const withToolbarOpenStyles = css` + grid-template-columns: auto ${PANEL_WIDTH + TOOLBAR_WIDTH}px; + + @media only screen and (max-width: ${breakpoints.Tablet}px) { + grid-template-columns: auto ${TOOLBAR_WIDTH}px; + } +`; + export const getEmbeddedDrawerLayoutStyles = ({ className, isDrawerOpen, + hasToolbar = false, }: { className?: string; isDrawerOpen: boolean; + hasToolbar?: boolean; }) => cx( baseStyles, { - [drawerOpenStyles]: isDrawerOpen, + [withToolbarBaseStyles]: hasToolbar, + [withToolbarOpenStyles]: isDrawerOpen && hasToolbar, + [drawerBaseStyles]: !hasToolbar, + [drawerOpenStyles]: isDrawerOpen && !hasToolbar, }, className, ); diff --git a/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.tsx b/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.tsx index f2971f3373..caecfca9e4 100644 --- a/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.tsx +++ b/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.tsx @@ -3,18 +3,33 @@ import React, { forwardRef } from 'react'; import { getEmbeddedDrawerLayoutStyles } from './EmbeddedDrawerLayout.styles'; import { EmbeddedDrawerLayoutProps } from './EmbeddedDrawerLayout.types'; +/** + * This layout wrapper is used to create a layout that has 2 grid columns. The main content is on the left and the drawer is on the right. + * + * Since this layout is used for embedded drawers, when the drawer is open, the layout will shift to the right by the width of the drawer + toolbar if it exists. + * + */ export const EmbeddedDrawerLayout = forwardRef< HTMLDivElement, EmbeddedDrawerLayoutProps >( ( - { children, className, isDrawerOpen }: EmbeddedDrawerLayoutProps, + { + children, + className, + isDrawerOpen, + hasToolbar = false, + }: EmbeddedDrawerLayoutProps, forwardedRef, ) => { return (
{children}
diff --git a/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.types.ts b/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.types.ts index c80bc00d6e..08e2c4ca36 100644 --- a/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.types.ts +++ b/packages/drawer/src/EmbeddedDrawerLayout/EmbeddedDrawerLayout.types.ts @@ -5,4 +5,9 @@ export interface EmbeddedDrawerLayoutProps extends HTMLElementProps<'div'> { * Determines if the Drawer instance is open or closed */ isDrawerOpen: boolean; + + /** + * Determines if the Toolbar is present in the layout + */ + hasToolbar?: boolean; } diff --git a/packages/drawer/src/OverlayDrawerLayout/OverlayDrawerLayout.styles.ts b/packages/drawer/src/OverlayDrawerLayout/OverlayDrawerLayout.styles.ts new file mode 100644 index 0000000000..2e201e5a2c --- /dev/null +++ b/packages/drawer/src/OverlayDrawerLayout/OverlayDrawerLayout.styles.ts @@ -0,0 +1,39 @@ +import { css, cx } from '@leafygreen-ui/emotion'; + +import { GRID_AREA } from '../constants'; +import { TOOLBAR_WIDTH } from '../Drawer'; + +const baseStyles = css` + width: 100%; + position: relative; + height: inherit; +`; + +const drawerBaseStyles = css` + display: grid; + grid-template-columns: auto 0px; + overflow: hidden; +`; + +const toolbarBaseStyles = css` + display: grid; + grid-template-columns: auto ${TOOLBAR_WIDTH}px; + grid-template-areas: '${GRID_AREA.content} ${GRID_AREA.drawer}'; + height: 100%; +`; + +export const getOverlayDrawerLayoutStyles = ({ + className, + hasToolbar = false, +}: { + className?: string; + hasToolbar?: boolean; +}) => + cx( + baseStyles, + { + [toolbarBaseStyles]: hasToolbar, + [drawerBaseStyles]: !hasToolbar, + }, + className, + ); diff --git a/packages/drawer/src/OverlayDrawerLayout/OverlayDrawerLayout.tsx b/packages/drawer/src/OverlayDrawerLayout/OverlayDrawerLayout.tsx new file mode 100644 index 0000000000..b04dff0310 --- /dev/null +++ b/packages/drawer/src/OverlayDrawerLayout/OverlayDrawerLayout.tsx @@ -0,0 +1,34 @@ +import React, { forwardRef } from 'react'; + +import { getOverlayDrawerLayoutStyles } from './OverlayDrawerLayout.styles'; +import { OverlayDrawerLayoutProps } from './OverlayDrawerLayout.types'; + +/** + * This layout wrapper is used to create a layout that has 2 grid columns. The main content is on the left and the drawer is on the right. + * + * Since this layout is used for overlay drawers, when the drawer is open, the layout will not shift. Instead the shifting is handled by the children of this component. + * + */ +export const OverlayDrawerLayout = forwardRef< + HTMLDivElement, + OverlayDrawerLayoutProps +>( + ( + { children, className, hasToolbar = false }: OverlayDrawerLayoutProps, + forwardedRef, + ) => { + return ( +
+ {children} +
+ ); + }, +); + +OverlayDrawerLayout.displayName = 'OverlayDrawerLayout'; diff --git a/packages/drawer/src/OverlayDrawerLayout/OverlayDrawerLayout.types.ts b/packages/drawer/src/OverlayDrawerLayout/OverlayDrawerLayout.types.ts new file mode 100644 index 0000000000..f9cb8af096 --- /dev/null +++ b/packages/drawer/src/OverlayDrawerLayout/OverlayDrawerLayout.types.ts @@ -0,0 +1,8 @@ +import { HTMLElementProps } from '@leafygreen-ui/lib'; + +export interface OverlayDrawerLayoutProps extends HTMLElementProps<'div'> { + /** + * Determines if the Toolbar is present in the layout + */ + hasToolbar?: boolean; +} diff --git a/packages/drawer/src/OverlayDrawerLayout/index.ts b/packages/drawer/src/OverlayDrawerLayout/index.ts new file mode 100644 index 0000000000..27b11ec524 --- /dev/null +++ b/packages/drawer/src/OverlayDrawerLayout/index.ts @@ -0,0 +1,2 @@ +export { OverlayDrawerLayout } from './OverlayDrawerLayout'; +export { OverlayDrawerLayoutProps } from './OverlayDrawerLayout.types'; diff --git a/packages/drawer/src/constants.ts b/packages/drawer/src/constants.ts new file mode 100644 index 0000000000..b276f2d844 --- /dev/null +++ b/packages/drawer/src/constants.ts @@ -0,0 +1,6 @@ +export const GRID_AREA = { + drawer: 'drawer', + content: 'content', + toolbar: 'toolbar', + innerDrawer: 'inner-drawer', +}; diff --git a/packages/drawer/src/utils/getTestUtils.tsx b/packages/drawer/src/utils/getTestUtils.tsx index f99ec029c1..f41e705922 100644 --- a/packages/drawer/src/utils/getTestUtils.tsx +++ b/packages/drawer/src/utils/getTestUtils.tsx @@ -42,14 +42,10 @@ export const getTestUtils = ( const element = getDrawer(); const isAriaVisible = element.getAttribute('aria-hidden') === 'false'; - const { display, opacity, transform, visibility } = - window.getComputedStyle(element); + const { display, opacity, visibility } = window.getComputedStyle(element); const isCSSVisible = - display !== 'none' && - opacity === '1' && - transform === 'none' && - visibility !== 'hidden'; + display !== 'none' && opacity === '1' && visibility !== 'hidden'; return isAriaVisible && isCSSVisible; }; diff --git a/packages/drawer/tsconfig.json b/packages/drawer/tsconfig.json index 8a6470d710..22ee4ba0d6 100644 --- a/packages/drawer/tsconfig.json +++ b/packages/drawer/tsconfig.json @@ -1,19 +1,26 @@ { "extends": "@lg-tools/build/config/package.tsconfig.json", - "compilerOptions": { + "compilerOptions": { "declarationDir": "dist", "outDir": "dist", "rootDir": "src", "baseUrl": ".", "paths": { - "@leafygreen-ui/icon/dist/*": ["../icon/src/generated/*"], - "@leafygreen-ui/*": ["../*/src"] + "@leafygreen-ui/icon/dist/*": [ + "../icon/src/generated/*" + ], + "@leafygreen-ui/*": [ + "../*/src" + ] } }, "include": [ "src/**/*" ], - "exclude": ["**/*.spec.*", "**/*.stories.*"], + "exclude": [ + "**/*.spec.*", + "**/*.stories.*" + ], "references": [ { "path": "../button" @@ -42,6 +49,9 @@ { "path": "../tabs" }, + { + "path": "../toolbar" + }, { "path": "../tokens" }, @@ -49,4 +59,4 @@ "path": "../typography" } ] -} +} \ No newline at end of file diff --git a/packages/toolbar/src/Toolbar/Toolbar.styles.ts b/packages/toolbar/src/Toolbar/Toolbar.styles.ts index ea7dd5f33f..12f7789830 100644 --- a/packages/toolbar/src/Toolbar/Toolbar.styles.ts +++ b/packages/toolbar/src/Toolbar/Toolbar.styles.ts @@ -24,6 +24,8 @@ export const getBaseStyles = ({ // Show the focus ring on the toolbar itself when a child element is focused and only when navigating with a keyboard &:has(:focus-visible) { box-shadow: ${focusRing[theme].default}; + // So the focus ring overlaps sibling elements + z-index: 1; } `, className, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ac44bc9a9..f01929e71b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1355,6 +1355,9 @@ importers: '@leafygreen-ui/tokens': specifier: workspace:^ version: link:../tokens + '@leafygreen-ui/toolbar': + specifier: workspace:^ + version: link:../toolbar '@leafygreen-ui/typography': specifier: workspace:^ version: link:../typography