Skip to content

Commit ceccfc1

Browse files
authored
[LG-5523] feat(drawer): add ref and tooltipEnabled props to DrawerLayout toolbarData for ToolbarIconButton integration (#3142)
* feat(toolbar): add tooltipEnabled prop to ToolbarIconButton for customizable tooltip behavior * chore(toolbar): changeset * chore(drawer): add @leafygreen-ui/guide-cue as dev dep * feat(drawer): enhance DrawerToolbarLayout with ref and tooltipEnabled props for ToolbarIconButton integration * chore(drawer): changeset * refactor: rename tooltipEnabled prop to isTooltipEnabled * refactor(drawer): move WithGuideCue story with play test
1 parent 882373b commit ceccfc1

14 files changed

+330
-75
lines changed

.changeset/bright-doodles-post.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/toolbar': minor
3+
---
4+
5+
Add `isTooltipEnabled` prop to `ToolbarIconButton` component for customizable tooltip behavior

.changeset/rich-dancers-change.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/drawer': minor
3+
---
4+
5+
Enhance `DrawerLayout` component's `toolbarData` prop with `ref` and `isTooltipEnabled` props for `ToolbarIconButton` integration

packages/drawer/README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -436,15 +436,17 @@ You can also use the resizable feature with a toolbar-based drawer:
436436

437437
### LayoutData
438438

439-
| Prop | Type | Description | Default |
440-
| ------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
441-
| `id` | `string` | The required id of the layout. This is used to open the `Drawer` with `openDrawer(id)`. | |
442-
| `title` _(optional)_ | `React.ReactNode` | The title of the `Drawer`. If it is a string, it will be rendered as a `<h2>` element. If it is a React node, it will be rendered as is. This is not required if the `Toolbar` item should not open a `Drawer`. | |
443-
| `content` _(optional)_ | `React.ReactNode` | The content of the `Drawer`. This is not required if the `Toolbar` item should not open a `Drawer`. | |
444-
| `disabled` _(optional)_ | `boolean` | Whether the toolbar item is disabled. | `false` |
445-
| `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` |
446-
| `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` |
439+
| Prop | Type | Description | Default |
440+
| ------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
441+
| `id` | `string` | The required id of the layout. This is used to open the `Drawer` with `openDrawer(id)`. | |
442+
| `title` _(optional)_ | `React.ReactNode` | The title of the `Drawer`. If it is a string, it will be rendered as a `<h2>` element. If it is a React node, it will be rendered as is. This is not required if the `Toolbar` item should not open a `Drawer`. | |
443+
| `content` _(optional)_ | `React.ReactNode` | The content of the `Drawer`. This is not required if the `Toolbar` item should not open a `Drawer`. | |
444+
| `disabled` _(optional)_ | `boolean` | Whether the toolbar item is disabled. | `false` |
445+
| `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` |
446+
| `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` |
448+
| `ref` _(optional)_ | `React.RefObject<HTMLButtonElement>` | Optional ref to be passed to the ToolbarIconButton instance. Useful for integrating with components like `GuideCue` that need to position relative to the button. | `null` |
449+
| `isTooltipEnabled` _(optional)_ | `boolean` | Enables the tooltip to trigger based on hover events. When false, the tooltip will not show on hover. Useful when other overlays (like `GuideCue`) are positioned on the button. | `true` |
448450

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

packages/drawer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"devDependencies": {
4949
"@faker-js/faker": "^8.0.2",
5050
"@storybook/test": "8.5.3",
51+
"@leafygreen-ui/guide-cue": "workspace:^",
5152
"@lg-tools/build": "workspace:^"
5253
},
5354
"peerDependencies": {

packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout/DrawerToolbarLayout.interactions.stories.tsx

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1-
import React from 'react';
1+
import React, { useEffect, useMemo, useRef, useState } from 'react';
22
import { storybookExcludedControlParams } from '@lg-tools/storybook-utils';
33
import { StoryFn, StoryObj } from '@storybook/react';
44
import { expect, userEvent, waitFor, within } from '@storybook/test';
55

66
import Button from '@leafygreen-ui/button';
77
import { css } from '@leafygreen-ui/emotion';
8+
import { GuideCue } from '@leafygreen-ui/guide-cue';
9+
import { usePrevious } from '@leafygreen-ui/hooks';
810
import { palette } from '@leafygreen-ui/palette';
911
import { spacing } from '@leafygreen-ui/tokens';
1012

1113
import { DisplayMode, Drawer } from '../../Drawer';
12-
import { DrawerLayoutProvider } from '../../DrawerLayout';
14+
import {
15+
DrawerLayout,
16+
DrawerLayoutProps,
17+
DrawerLayoutProvider,
18+
} from '../../DrawerLayout';
1319
import { getTestUtils } from '../../testing';
1420
import { useDrawerToolbarContext } from '../DrawerToolbarContext/DrawerToolbarContext';
1521

@@ -535,3 +541,139 @@ export const EmbeddedClosesDrawerWhenActiveItemIsRemovedFromToolbarData: StoryOb
535541
},
536542
play: playClosesDrawerWhenActiveItemIsRemovedFromToolbarData,
537543
};
544+
545+
interface MainContentProps {
546+
dashboardButtonRef: React.RefObject<HTMLButtonElement>;
547+
guideCueOpen: boolean;
548+
setGuideCueOpen: React.Dispatch<React.SetStateAction<boolean>>;
549+
}
550+
551+
const MainContent: React.FC<MainContentProps> = ({
552+
dashboardButtonRef,
553+
guideCueOpen,
554+
setGuideCueOpen,
555+
}) => {
556+
const { isDrawerOpen } = useDrawerToolbarContext();
557+
const prevIsDrawerOpen = usePrevious(isDrawerOpen);
558+
559+
// Close GuideCue immediately when drawer begins transitioning (state change)
560+
useEffect(() => {
561+
if (prevIsDrawerOpen !== undefined && prevIsDrawerOpen !== isDrawerOpen) {
562+
// Close guide cue immediately when drawer transition begins
563+
if (guideCueOpen) {
564+
setGuideCueOpen(false);
565+
}
566+
}
567+
}, [isDrawerOpen, prevIsDrawerOpen, guideCueOpen, setGuideCueOpen]);
568+
569+
return (
570+
<main
571+
className={css`
572+
padding: ${spacing[400]}px;
573+
`}
574+
>
575+
<div
576+
className={css`
577+
display: flex;
578+
flex-direction: column;
579+
align-items: flex-start;
580+
gap: ${spacing[200]}px;
581+
`}
582+
>
583+
<Button onClick={() => setGuideCueOpen(true)}>Show GuideCue</Button>
584+
<p>
585+
This example demonstrates how to use refs in toolbarData to attach a
586+
GuideCue to a toolbar icon button. The button tooltip is automatically
587+
disabled while the GuideCue is visible to prevent conflicts between
588+
the two overlays.
589+
</p>
590+
</div>
591+
<LongContent />
592+
<GuideCue
593+
open={guideCueOpen}
594+
setOpen={setGuideCueOpen}
595+
title="Dashboard Feature"
596+
refEl={dashboardButtonRef}
597+
numberOfSteps={1}
598+
onPrimaryButtonClick={() => setGuideCueOpen(false)}
599+
tooltipAlign="left"
600+
tooltipJustify="start"
601+
>
602+
Click here to access your dashboard with analytics and insights!
603+
</GuideCue>
604+
</main>
605+
);
606+
};
607+
608+
const WithGuideCueComponent: StoryFn<DrawerLayoutProps> = ({
609+
displayMode,
610+
}: DrawerLayoutProps) => {
611+
const dashboardButtonRef = useRef<HTMLButtonElement>(null);
612+
const [guideCueOpen, setGuideCueOpen] = useState(false);
613+
614+
// Use useMemo to make toolbar data reactive to guideCueOpen state changes
615+
const DRAWER_TOOLBAR_DATA: DrawerLayoutProps['toolbarData'] = useMemo(
616+
() => [
617+
{
618+
id: 'Code',
619+
label: 'Code',
620+
content: <LongContent />,
621+
title: 'Code',
622+
glyph: 'Code',
623+
},
624+
{
625+
id: 'Dashboard',
626+
label: 'Dashboard',
627+
content: <LongContent />,
628+
title: 'Dashboard',
629+
glyph: 'Dashboard',
630+
ref: dashboardButtonRef, // This ref is passed to the ToolbarIconButton
631+
isTooltipEnabled: !guideCueOpen, // Disable tooltip when guide cue is open
632+
},
633+
{
634+
id: 'Apps',
635+
label: 'Apps',
636+
content: <LongContent />,
637+
title: 'Apps',
638+
glyph: 'Apps',
639+
},
640+
],
641+
[guideCueOpen],
642+
);
643+
644+
return (
645+
<div
646+
className={css`
647+
height: 90vh;
648+
width: 100%;
649+
`}
650+
>
651+
<DrawerLayout displayMode={displayMode} toolbarData={DRAWER_TOOLBAR_DATA}>
652+
<MainContent
653+
dashboardButtonRef={dashboardButtonRef}
654+
guideCueOpen={guideCueOpen}
655+
setGuideCueOpen={setGuideCueOpen}
656+
/>
657+
</DrawerLayout>
658+
</div>
659+
);
660+
};
661+
662+
export const WithGuideCue: StoryObj<DrawerLayoutProps> = {
663+
render: WithGuideCueComponent,
664+
args: {
665+
displayMode: DisplayMode.Overlay,
666+
},
667+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
668+
const canvas = within(canvasElement);
669+
const guideCueButton = await canvas.getByRole('button', {
670+
name: 'Show GuideCue',
671+
});
672+
await userEvent.click(guideCueButton);
673+
},
674+
parameters: {
675+
chromatic: {
676+
delay: 300,
677+
},
678+
},
679+
};

packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout/DrawerToolbarLayout.spec.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,38 @@ describe('packages/DrawerToolbarLayout', () => {
259259

260260
expect(isOpen()).toBe(false);
261261
});
262+
263+
test('passes ref correctly to ToolbarIconButton instances', () => {
264+
const codeButtonRef = React.createRef<HTMLButtonElement>();
265+
const code2ButtonRef = React.createRef<HTMLButtonElement>();
266+
267+
const dataWithRefs: DrawerLayoutProps['toolbarData'] = [
268+
{
269+
id: 'Code',
270+
label: 'Code',
271+
content: 'Drawer Content',
272+
title: `Drawer Title`,
273+
glyph: 'Code',
274+
ref: codeButtonRef,
275+
},
276+
{
277+
id: 'Code2',
278+
label: 'Code2',
279+
content: 'Drawer Content2',
280+
title: `Drawer Title2`,
281+
glyph: 'Code',
282+
ref: code2ButtonRef,
283+
},
284+
];
285+
286+
render(<Component data={dataWithRefs} />);
287+
288+
// Verify that refs are properly assigned to DOM elements
289+
expect(codeButtonRef.current).toBeInstanceOf(HTMLButtonElement);
290+
expect(code2ButtonRef.current).toBeInstanceOf(HTMLButtonElement);
291+
292+
// Verify the elements have the correct labels
293+
expect(codeButtonRef.current).toHaveAttribute('aria-label', 'Code');
294+
expect(code2ButtonRef.current).toHaveAttribute('aria-label', 'Code2');
295+
});
262296
});

packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout/DrawerToolbarLayout.types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ToolbarIconButtonProps } from '@leafygreen-ui/toolbar';
55

66
type PickedRequiredToolbarIconButtonProps = Pick<
77
ToolbarIconButtonProps,
8-
'glyph' | 'label' | 'onClick' | 'disabled'
8+
'glyph' | 'label' | 'onClick' | 'disabled' | 'isTooltipEnabled'
99
>;
1010

1111
interface LayoutBase extends PickedRequiredToolbarIconButtonProps {
@@ -19,6 +19,11 @@ interface LayoutBase extends PickedRequiredToolbarIconButtonProps {
1919
* @defaultValue true
2020
*/
2121
visible?: boolean;
22+
23+
/**
24+
* Optional ref to be passed to the ToolbarIconButton instance.
25+
*/
26+
ref?: React.RefObject<HTMLButtonElement>;
2227
}
2328

2429
interface LayoutWithContent extends LayoutBase {

packages/drawer/src/DrawerToolbarLayout/DrawerToolbarLayout/DrawerToolbarLayoutContent.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export const DrawerToolbarLayoutContent = forwardRef<
120120
}}
121121
active={toolbarItem.id === id}
122122
disabled={toolbarItem.disabled}
123+
ref={toolbarItem.ref}
124+
isTooltipEnabled={toolbarItem.isTooltipEnabled}
123125
/>
124126
))}
125127
</Toolbar>

packages/drawer/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
{
2424
"path": "../emotion"
2525
},
26+
{
27+
"path": "../guide-cue"
28+
},
2629
{
2730
"path": "../hooks"
2831
},

packages/toolbar/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,11 @@ import {Toolbar, ToolbarIconButton} from `@leafygreen-ui/toolbar`;
5151

5252
#### Props
5353

54-
| Prop | Type | Description | Default |
55-
| ------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
56-
| `glyph` | `Glyph` | Name of the icon glyph to display in the button. List of available glyphs can be found in the [Icon README](https://github.com/mongodb/leafygreen-ui/blob/main/packages/icon/README.md#properties). | |
57-
| `label` | `React.ReactNode` | Text that appears in the tooltip on hover/focus | |
54+
| Prop | Type | Description | Default |
55+
| ------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
56+
| `glyph` | `Glyph` | Name of the icon glyph to display in the button. List of available glyphs can be found in the [Icon README](https://github.com/mongodb/leafygreen-ui/blob/main/packages/icon/README.md#properties). | |
57+
| `label` | `React.ReactNode` | Text that appears in the tooltip on hover/focus | |
58+
| `isTooltipEnabled` _(optional)_ | `boolean` | Enables the tooltip to trigger based on hover events. When false, the tooltip will not show on hover. Useful when other overlays (like `GuideCue`) are positioned on the button. | `true` |
5859

5960
\+ Extends LG [IconButton props](https://github.com/mongodb/leafygreen-ui/tree/main/packages/icon-button#properties) with the exception of `as`, `children`, `darkMode`, `href`, `size`, and `tabIndex`
6061

0 commit comments

Comments
 (0)