Skip to content

Commit 71c6daa

Browse files
authored
[LG-5460] feat(drawer): add toolbar item visibility control (#3067)
* wip * feat(DrawerToolbar): enhance toolbar visibility management and add toggle functionality * wip, not complete * remove resolvedInitialState * actually remove resolvedInitialState * maybe a fix? * refactor(Drawer): remove unused animations and update layout styles for mobile responsiveness * refactor(Drawer): streamline styles by removing unused animations * refactor(Drawer): update context and layout components to use the same styles, wip * refactor(Drawer): improve context handling and layout structure for better drawer integration * refactor(Drawer): consolidate layout rendering and improve content styling * feat(Drawer): introduce LayoutGrid component for improved drawer layout management * refactor(Drawer): clean up unused code and lint * feat(Drawer): add toolbar visibility toggle functionality and update test utils * refactor(Drawer): update constants and styles * refactor(Drawer): adjust mobile layout styles for toolbar visibility * refactor(Drawer): rename story exports for clarity and enhance toolbar item tests * refactor(Drawer): enhance layout styles and logic for drawer visibility handling * refactor(Drawer): update drawer width calculation and enhance toolbar flicker handling * test(Drawer): add unit test for setHasToolbar functionality in DrawerLayoutContext * refactor(Drawer): improve embedded styles and overlay handling for drawer layout * docs(Drawer): update README * refactor(Drawer): remove empty media query from embedded drawer styles * docs(Drawer): add changesets * feat(Drawer): replace layoutGird with PanelGrid component and update story data * refactor(Drawer): remove unused contentStyles from DrawerToolbarLayout and PanelGrid * fix(Drawer): update initial width calculation to consider resizable state for embedded drawers * feat(Drawer): refactor styling logic for embedded drawer layout with toolbar and panel content * fix(Drawer): simplify props handling in DrawerToolbarLayout story
1 parent 0e77720 commit 71c6daa

33 files changed

+1030
-460
lines changed

.changeset/dull-bikes-train.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
'@leafygreen-ui/drawer': minor
3+
---
4+
5+
# What's New
6+
7+
- **New `visible` property**: Toolbar items can now be conditionally shown/hidden using a `visible` boolean property (defaults to true). [LG-5460](https://jira.mongodb.org/browse/LG-5460)
8+
```tsx
9+
export const DRAWER_TOOLBAR_DATA: DrawerToolbarLayoutProps['toolbarData'] = [
10+
{
11+
id: 'Code',
12+
label: 'Code',
13+
content: <DrawerContent />,
14+
title: 'Code Title',
15+
glyph: 'Code',
16+
},
17+
{
18+
id: 'Dashboard',
19+
label: 'Dashboard',
20+
content: <DrawerContent />,
21+
title: 'Dashboard Title',
22+
glyph: 'Dashboard',
23+
visible: false, // This item will be hidden
24+
},
25+
{
26+
id: 'Plus',
27+
label: "Perform some action, doesn't open a drawer",
28+
glyph: 'Plus',
29+
},
30+
{
31+
id: 'Sparkle',
32+
label: 'Disabled item',
33+
glyph: 'Sparkle',
34+
disabled: true,
35+
},
36+
];
37+
```
38+
39+
40+
- **Dynamic toolbar rendering**: When all toolbar items have `visible: false`, the entire toolbar element is removed from the DOM. [LG-5460](https://jira.mongodb.org/browse/LG-5460)
41+
42+
```tsx
43+
{
44+
id: 'Code',
45+
label: 'Code',
46+
content: <DrawerContent />,
47+
title: 'Code Title',
48+
glyph: 'Code',
49+
visible: false,
50+
},
51+
{
52+
id: 'Dashboard',
53+
label: 'Dashboard',
54+
content: <DrawerContent />,
55+
title: 'Dashboard Title',
56+
glyph: 'Dashboard',
57+
visible: false,
58+
},
59+
{
60+
id: 'Plus',
61+
label: "Perform some action, doesn't open a drawer",
62+
glyph: 'Plus',
63+
visible: false,
64+
},
65+
{
66+
id: 'Sparkle',
67+
label: 'Disabled item',
68+
glyph: 'Sparkle',
69+
disabled: true,
70+
visible: false,
71+
},
72+
```

.changeset/young-doodles-invite.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
'@leafygreen-ui/drawer': major
3+
---
4+
5+
# Unified Drawer Layout System
6+
7+
## Summary
8+
9+
This release introduces a unified grid layout system for all drawer instances, eliminating layout inconsistencies between drawers with and without toolbars. All drawers now use the same consistent grid-based layout, providing better visual consistency and easier customization.
10+
11+
## Breaking Changes
12+
13+
### Layout Unification Impact
14+
15+
Previously, drawers with toolbars used a different internal layout system than drawers without toolbars, leading to inconsistent spacing, positioning, and responsive behavior when adding new internal features. This change standardizes all drawer layouts (with one exception - see below) to use the same grid system.
16+
17+
**⚠️ Migration Required:** If you were using custom CSS to override drawer layout styles, you will need to update your styles to work with the new grid-based layout system.
18+
19+
### What's Changed
20+
21+
#### 1. Drawer without toolbar using the `drawer` prop
22+
23+
Drawers passed via the `drawer` prop now use the unified grid layout. Mobile styles have been updated to match the responsive behavior of drawers with toolbars.
24+
25+
**Impact:** Custom style overrides will need to be updated to work with the new grid system.
26+
27+
```tsx
28+
// ✅ This usage is affected - now uses unified grid layout
29+
<DrawerLayout
30+
displayMode={DisplayMode.Embedded}
31+
isDrawerOpen={open}
32+
resizable
33+
drawer={
34+
<Drawer title="Resizable Drawer">
35+
<div>
36+
Drawer content that can be resized by dragging the left edge
37+
</div>
38+
</Drawer>
39+
}
40+
onClose={() => setOpen(false)}
41+
>
42+
<main>
43+
<Button onClick={() => setOpen(prevOpen => !prevOpen)}>
44+
Toggle Resizable Drawer
45+
</Button>
46+
</main>
47+
</DrawerLayout>
48+
```
49+
50+
#### 2. Drawer passed as child (No changes required)
51+
52+
**Impact:** No changes needed. This usage pattern is unaffected.
53+
54+
When the drawer is passed as a child component, it cannot be wrapped in the new grid layout system, so this usage pattern remains unchanged.
55+
56+
```tsx
57+
// ✅ This usage is NOT affected - works exactly the same
58+
<DrawerLayout displayMode={DisplayMode.Overlay} isDrawerOpen={open}>
59+
<main>
60+
<Button onClick={() => setOpen(prevOpen => !prevOpen)}>
61+
Open Drawer
62+
</Button>
63+
</main>
64+
<Drawer
65+
displayMode={DisplayMode.Overlay}
66+
onClose={() => setOpen(false)}
67+
open={open}
68+
title="Drawer Title"
69+
>
70+
Drawer content goes here
71+
</Drawer>
72+
</DrawerLayout>
73+
```
74+
75+
#### 3. Drawer with toolbar (No changes required)
76+
77+
**Impact:** No changes needed. This usage already used the unified grid layout.
78+
79+
```tsx
80+
// ✅ This usage is NOT affected - already used the unified layout
81+
<DrawerLayout
82+
toolbarData={DRAWER_TOOLBAR_DATA}
83+
displayMode={DisplayMode.Overlay}
84+
size={Size.Default}
85+
>
86+
<Main />
87+
</DrawerLayout>
88+
```
89+
90+
## Migration Guide
91+
92+
### If you have custom CSS overrides:
93+
94+
1. **Review your custom styles:** Check if you have any CSS that targets drawer layout elements
95+
2. **Test thoroughly:** The new grid system may affect positioning, spacing, or responsive behavior
96+
3. **Update selectors:** You may need to update CSS selectors to target the new grid structure
97+
98+
### Benefits of this change:
99+
100+
- **Consistent layout:** All drawers now have the same visual structure and behavior
101+
- **Better mobile experience:** Unified responsive behavior across all drawer types
102+
- **Easier customization:** Single grid system to understand and customize
103+
- **Future-proof:** Consistent foundation for future drawer enhancements

packages/drawer/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ const DRAWER_TOOLBAR_DATA: DrawerLayoutProps['toolbarData'] = [
7373
console.log('Plus clicked, does not update drawer');
7474
},
7575
},
76+
{
77+
id: 'Settings',
78+
label: 'Settings',
79+
content: <DrawerContent />,
80+
title: 'Settings Title',
81+
glyph: 'Settings',
82+
visible: false, // This item will be hidden
83+
},
7684
];
7785

7886
const App = () => {
@@ -141,6 +149,10 @@ To use with a `Toolbar`, pass the `toolbarData` prop to render the `Toolbar` ite
141149

142150
The `Drawer` and `Toolbar` component will be rendered automatically based on the `toolbarData` provided.
143151

152+
#### Toolbar Visibility
153+
154+
Individual toolbar items can be controlled using the `visible` prop. When all toolbar items have `visible` set to `false`, the entire toolbar will be automatically hidden from view. This allows for dynamic toolbar management based on application state or user permissions.
155+
144156
### `useDrawerToolbarContext()`
145157

146158
To control the `Drawer` state, use the `useDrawerToolbarContext` hook from within `<DrawerLayout>`. This hook provides the `openDrawer()` and `closeDrawer()` functions to open and close the drawer programmatically. The hook takes no arguments and returns the following functions:
@@ -432,6 +444,7 @@ You can also use the resizable feature with a toolbar-based drawer:
432444
| `disabled` _(optional)_ | `boolean` | Whether the toolbar item is disabled. | `false` |
433445
| `hasPadding` _(optional)_ | `boolean` | Determines whether the drawer content should have padding. When false, the content area will not have padding, allowing full-width/height content. | `true` |
434446
| `scrollable` _(optional)_ | `boolean` | Determines whether the drawer content should have its own scroll container. When false, the content area will not have scroll behavior. | `true` |
447+
| `visible` _(optional)_ | `boolean` | Determines if the current toolbar item is visible. If all toolbar items have `visible` set to `false`, the toolbar will not be rendered. | `true` |
435448

436449
\+ Extends the following from LG [Toolbar props](https://github.com/mongodb/leafygreen-ui/tree/main/packages/toolbar/README.md#toolbariconbutton): `glyph`, `label`, and `onClick`.
437450

packages/drawer/src/Drawer/Drawer.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { Position, useResizable } from '@leafygreen-ui/resizable';
1616
import { BaseFontSize } from '@leafygreen-ui/tokens';
1717
import { Body } from '@leafygreen-ui/typography';
1818

19-
import { TRANSITION_DURATION } from '../constants';
19+
import { DRAWER_TOOLBAR_WIDTH, TRANSITION_DURATION } from '../constants';
2020
import { useDrawerLayoutContext } from '../DrawerLayout';
2121
import { useDrawerStackContext } from '../DrawerStackContext';
2222
import { getLgIds } from '../utils';
@@ -66,10 +66,12 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
6666
hasToolbar,
6767
setIsDrawerResizing,
6868
setDrawerWidth,
69+
drawerWidth,
6970
size: sizeContextProp,
7071
} = useDrawerLayoutContext();
7172
const [shouldAnimate, setShouldAnimate] = useState(false);
7273
const ref = useRef<HTMLDialogElement | HTMLDivElement>(null);
74+
const [previousWidth, setPreviousWidth] = useState(0);
7375

7476
// Returns the resolved displayMode, open state, and onClose function based on the component and context props.
7577
const { displayMode, open, onClose, size } = useResolvedDrawerProps({
@@ -160,12 +162,32 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
160162
isResizing,
161163
} = useResizable<HTMLDialogElement | HTMLDivElement>({
162164
enabled: isResizableEnabled,
163-
initialSize,
165+
initialSize: previousWidth !== 0 ? previousWidth : initialSize,
164166
minSize: resizableMinWidth,
165167
maxSize: resizableMaxWidth,
166168
position: Position.Right,
167169
});
168170

171+
/**
172+
* On initial render, if the drawer is embedded and there was a previous width, that means that the previous drawer was open and may have been resized. This takes that previous width and uses it as the initial size.
173+
*/
174+
useEffect(() => {
175+
if (open && isEmbedded && resizable && drawerWidth !== 0) {
176+
const prevWidth = hasToolbar
177+
? drawerWidth + DRAWER_TOOLBAR_WIDTH
178+
: drawerWidth - DRAWER_TOOLBAR_WIDTH;
179+
setPreviousWidth(prevWidth);
180+
}
181+
// eslint-disable-next-line react-hooks/exhaustive-deps
182+
}, []);
183+
184+
/**
185+
* Resets the previous width to 0 when the drawer is closed.
186+
*/
187+
useEffect(() => {
188+
if (!open) setPreviousWidth(0);
189+
}, [open]);
190+
169191
// In an embedded drawer, the parent grid container controls the drawer width with grid-template-columns, so we pass the width to the context where it is read by the parent grid container.
170192
useEffect(() => {
171193
if (!isEmbedded) return;

packages/drawer/src/DrawerLayout/DrawerLayout.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import React, { forwardRef } from 'react';
22

3-
import { consoleOnce } from '@leafygreen-ui/lib';
4-
53
import { DisplayMode, Size } from '../Drawer/Drawer.types';
64
import { DrawerToolbarLayout } from '../DrawerToolbarLayout';
75
import { LayoutComponent } from '../LayoutComponent';
@@ -23,18 +21,13 @@ export const DrawerLayout = forwardRef<HTMLDivElement, DrawerLayoutProps>(
2321
resizable = false,
2422
onClose,
2523
size = Size.Default,
24+
drawer,
2625
...rest
2726
}: DrawerLayoutProps,
2827
forwardedRef,
2928
) => {
3029
const hasToolbar = toolbarData && toolbarData.length > 0;
3130

32-
if (!hasToolbar) {
33-
consoleOnce.warn(
34-
'Using a Drawer without a toolbar is not recommended. To include a toolbar, pass a toolbarData prop containing the desired toolbar items.',
35-
);
36-
}
37-
3831
return (
3932
<DrawerLayoutProvider
4033
isDrawerOpen={isDrawerOpen}
@@ -53,7 +46,7 @@ export const DrawerLayout = forwardRef<HTMLDivElement, DrawerLayoutProps>(
5346
{children}
5447
</DrawerToolbarLayout>
5548
) : (
56-
<LayoutComponent ref={forwardedRef} {...rest}>
49+
<LayoutComponent panelContent={drawer} ref={forwardedRef} {...rest}>
5750
{children}
5851
</LayoutComponent>
5952
)}

packages/drawer/src/DrawerLayout/DrawerLayoutContext/DrawerLayoutContext.spec.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('useDrawerLayoutContext', () => {
2424
expect(typeof result.current.setIsDrawerOpen).toBe('function');
2525
expect(typeof result.current.setDrawerWidth).toBe('function');
2626
expect(typeof result.current.setIsDrawerResizing).toBe('function');
27+
expect(typeof result.current.setHasToolbar).toBe('function');
2728
});
2829

2930
test('Returns the value passed to the provider', () => {
@@ -85,4 +86,17 @@ describe('useDrawerLayoutContext', () => {
8586
});
8687
expect(result.current.isDrawerResizing).toBe(true);
8788
});
89+
90+
test('Updates hasToolbar when setHasToolbar is called', () => {
91+
const { result } = renderHook(() => useDrawerLayoutContext(), {
92+
wrapper: ({ children }) => (
93+
<DrawerLayoutProvider>{children}</DrawerLayoutProvider>
94+
),
95+
});
96+
expect(result.current.hasToolbar).toBe(false);
97+
act(() => {
98+
result?.current?.setHasToolbar?.(true);
99+
});
100+
expect(result.current.hasToolbar).toBe(true);
101+
});
88102
});

packages/drawer/src/DrawerLayout/DrawerLayoutContext/DrawerLayoutContext.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const DrawerLayoutContext = createContext<DrawerLayoutContextType>({
2020
hasToolbar: false,
2121
isDrawerResizing: false,
2222
drawerWidth: 0,
23+
setHasToolbar: () => {},
2324
setIsDrawerOpen: () => {},
2425
setDrawerWidth: () => {},
2526
setIsDrawerResizing: () => {},
@@ -36,12 +37,13 @@ export const DrawerLayoutProvider = ({
3637
resizable,
3738
displayMode,
3839
onClose,
39-
hasToolbar,
40+
hasToolbar: hasToolbarProp = false,
4041
size,
4142
}: PropsWithChildren<DrawerLayoutProviderProps>) => {
4243
const [isDrawerOpen, setIsDrawerOpen] = React.useState(isDrawerOpenProp);
4344
const [isDrawerResizing, setIsDrawerResizing] = React.useState(false);
4445
const [drawerWidth, setDrawerWidth] = React.useState(0);
46+
const [hasToolbar, setHasToolbar] = React.useState(hasToolbarProp);
4547

4648
useEffect(() => {
4749
setIsDrawerOpen(isDrawerOpenProp);
@@ -53,6 +55,7 @@ export const DrawerLayoutProvider = ({
5355
displayMode,
5456
onClose,
5557
hasToolbar,
58+
setHasToolbar,
5659
setIsDrawerOpen,
5760
isDrawerResizing,
5861
setIsDrawerResizing,

packages/drawer/src/DrawerLayout/DrawerLayoutContext/DrawerLayoutContext.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,9 @@ export interface DrawerLayoutContextType extends DrawerLayoutProviderProps {
5757
* Function to set the drawer resizing state.
5858
*/
5959
setIsDrawerResizing: React.Dispatch<React.SetStateAction<boolean>>;
60+
61+
/**
62+
* Function to set the drawer toolbar state.
63+
*/
64+
setHasToolbar: React.Dispatch<React.SetStateAction<boolean>>;
6065
}

0 commit comments

Comments
 (0)