Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-doodles-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/toolbar': minor
---

Add `isTooltipEnabled` prop to `ToolbarIconButton` component for customizable tooltip behavior
5 changes: 5 additions & 0 deletions .changeset/rich-dancers-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/drawer': minor
---

Enhance `DrawerLayout` component's `toolbarData` prop with `ref` and `isTooltipEnabled` props for `ToolbarIconButton` integration
20 changes: 11 additions & 9 deletions packages/drawer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,15 +436,17 @@ You can also use the resizable feature with a toolbar-based drawer:

### LayoutData

| Prop | Type | Description | Default |
| ------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `id` | `string` | The required id of the layout. This is used to open the `Drawer` with `openDrawer(id)`. | |
| `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`. | |
| `content` _(optional)_ | `React.ReactNode` | The content of the `Drawer`. This is not required if the `Toolbar` item should not open a `Drawer`. | |
| `disabled` _(optional)_ | `boolean` | Whether the toolbar item is disabled. | `false` |
| `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` |
| `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` |
| `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` |
| Prop | Type | Description | Default |
| ------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `id` | `string` | The required id of the layout. This is used to open the `Drawer` with `openDrawer(id)`. | |
| `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`. | |
| `content` _(optional)_ | `React.ReactNode` | The content of the `Drawer`. This is not required if the `Toolbar` item should not open a `Drawer`. | |
| `disabled` _(optional)_ | `boolean` | Whether the toolbar item is disabled. | `false` |
| `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` |
| `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` |
| `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` |
| `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` |
| `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` |

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

Expand Down
1 change: 1 addition & 0 deletions packages/drawer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"devDependencies": {
"@faker-js/faker": "^8.0.2",
"@storybook/test": "8.5.3",
"@leafygreen-ui/guide-cue": "workspace:^",
"@lg-tools/build": "workspace:^"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import React from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { storybookExcludedControlParams } from '@lg-tools/storybook-utils';
import { StoryFn, StoryObj } from '@storybook/react';
import { expect, userEvent, waitFor, within } from '@storybook/test';

import Button from '@leafygreen-ui/button';
import { css } from '@leafygreen-ui/emotion';
import { GuideCue } from '@leafygreen-ui/guide-cue';
import { usePrevious } from '@leafygreen-ui/hooks';
import { palette } from '@leafygreen-ui/palette';
import { spacing } from '@leafygreen-ui/tokens';

import { DisplayMode, Drawer } from '../../Drawer';
import { DrawerLayoutProvider } from '../../DrawerLayout';
import {
DrawerLayout,
DrawerLayoutProps,
DrawerLayoutProvider,
} from '../../DrawerLayout';
import { getTestUtils } from '../../testing';
import { useDrawerToolbarContext } from '../DrawerToolbarContext/DrawerToolbarContext';

Expand Down Expand Up @@ -535,3 +541,139 @@ export const EmbeddedClosesDrawerWhenActiveItemIsRemovedFromToolbarData: StoryOb
},
play: playClosesDrawerWhenActiveItemIsRemovedFromToolbarData,
};

interface MainContentProps {
dashboardButtonRef: React.RefObject<HTMLButtonElement>;
guideCueOpen: boolean;
setGuideCueOpen: React.Dispatch<React.SetStateAction<boolean>>;
}

const MainContent: React.FC<MainContentProps> = ({
dashboardButtonRef,
guideCueOpen,
setGuideCueOpen,
}) => {
const { isDrawerOpen } = useDrawerToolbarContext();
const prevIsDrawerOpen = usePrevious(isDrawerOpen);

// Close GuideCue immediately when drawer begins transitioning (state change)
useEffect(() => {
if (prevIsDrawerOpen !== undefined && prevIsDrawerOpen !== isDrawerOpen) {
// Close guide cue immediately when drawer transition begins
if (guideCueOpen) {
setGuideCueOpen(false);
}
}
}, [isDrawerOpen, prevIsDrawerOpen, guideCueOpen, setGuideCueOpen]);

return (
<main
className={css`
padding: ${spacing[400]}px;
`}
>
<div
className={css`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: ${spacing[200]}px;
`}
>
<Button onClick={() => setGuideCueOpen(true)}>Show GuideCue</Button>
<p>
This example demonstrates how to use refs in toolbarData to attach a
GuideCue to a toolbar icon button. The button tooltip is automatically
disabled while the GuideCue is visible to prevent conflicts between
the two overlays.
</p>
</div>
<LongContent />
<GuideCue
open={guideCueOpen}
setOpen={setGuideCueOpen}
title="Dashboard Feature"
refEl={dashboardButtonRef}
numberOfSteps={1}
onPrimaryButtonClick={() => setGuideCueOpen(false)}
tooltipAlign="left"
tooltipJustify="start"
>
Click here to access your dashboard with analytics and insights!
</GuideCue>
</main>
);
};

const WithGuideCueComponent: StoryFn<DrawerLayoutProps> = ({
displayMode,
}: DrawerLayoutProps) => {
const dashboardButtonRef = useRef<HTMLButtonElement>(null);
const [guideCueOpen, setGuideCueOpen] = useState(false);

// Use useMemo to make toolbar data reactive to guideCueOpen state changes
const DRAWER_TOOLBAR_DATA: DrawerLayoutProps['toolbarData'] = useMemo(
() => [
{
id: 'Code',
label: 'Code',
content: <LongContent />,
title: 'Code',
glyph: 'Code',
},
{
id: 'Dashboard',
label: 'Dashboard',
content: <LongContent />,
title: 'Dashboard',
glyph: 'Dashboard',
ref: dashboardButtonRef, // This ref is passed to the ToolbarIconButton
isTooltipEnabled: !guideCueOpen, // Disable tooltip when guide cue is open
},
{
id: 'Apps',
label: 'Apps',
content: <LongContent />,
title: 'Apps',
glyph: 'Apps',
},
],
[guideCueOpen],
);

return (
<div
className={css`
height: 90vh;
width: 100%;
`}
>
<DrawerLayout displayMode={displayMode} toolbarData={DRAWER_TOOLBAR_DATA}>
<MainContent
dashboardButtonRef={dashboardButtonRef}
guideCueOpen={guideCueOpen}
setGuideCueOpen={setGuideCueOpen}
/>
</DrawerLayout>
</div>
);
};

export const WithGuideCue: StoryObj<DrawerLayoutProps> = {
render: WithGuideCueComponent,
args: {
displayMode: DisplayMode.Overlay,
},
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
const canvas = within(canvasElement);
const guideCueButton = await canvas.getByRole('button', {
name: 'Show GuideCue',
});
await userEvent.click(guideCueButton);
},
parameters: {
chromatic: {
delay: 300,
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,38 @@ describe('packages/DrawerToolbarLayout', () => {

expect(isOpen()).toBe(false);
});

test('passes ref correctly to ToolbarIconButton instances', () => {
const codeButtonRef = React.createRef<HTMLButtonElement>();
const code2ButtonRef = React.createRef<HTMLButtonElement>();

const dataWithRefs: DrawerLayoutProps['toolbarData'] = [
{
id: 'Code',
label: 'Code',
content: 'Drawer Content',
title: `Drawer Title`,
glyph: 'Code',
ref: codeButtonRef,
},
{
id: 'Code2',
label: 'Code2',
content: 'Drawer Content2',
title: `Drawer Title2`,
glyph: 'Code',
ref: code2ButtonRef,
},
];

render(<Component data={dataWithRefs} />);

// Verify that refs are properly assigned to DOM elements
expect(codeButtonRef.current).toBeInstanceOf(HTMLButtonElement);
expect(code2ButtonRef.current).toBeInstanceOf(HTMLButtonElement);

// Verify the elements have the correct labels
expect(codeButtonRef.current).toHaveAttribute('aria-label', 'Code');
expect(code2ButtonRef.current).toHaveAttribute('aria-label', 'Code2');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ToolbarIconButtonProps } from '@leafygreen-ui/toolbar';

type PickedRequiredToolbarIconButtonProps = Pick<
ToolbarIconButtonProps,
'glyph' | 'label' | 'onClick' | 'disabled'
'glyph' | 'label' | 'onClick' | 'disabled' | 'isTooltipEnabled'
>;

interface LayoutBase extends PickedRequiredToolbarIconButtonProps {
Expand All @@ -19,6 +19,11 @@ interface LayoutBase extends PickedRequiredToolbarIconButtonProps {
* @defaultValue true
*/
visible?: boolean;

/**
* Optional ref to be passed to the ToolbarIconButton instance.
*/
ref?: React.RefObject<HTMLButtonElement>;
}

interface LayoutWithContent extends LayoutBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export const DrawerToolbarLayoutContent = forwardRef<
}}
active={toolbarItem.id === id}
disabled={toolbarItem.disabled}
ref={toolbarItem.ref}
isTooltipEnabled={toolbarItem.isTooltipEnabled}
/>
))}
</Toolbar>
Expand Down
3 changes: 3 additions & 0 deletions packages/drawer/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
{
"path": "../emotion"
},
{
"path": "../guide-cue"
},
{
"path": "../hooks"
},
Expand Down
9 changes: 5 additions & 4 deletions packages/toolbar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ import {Toolbar, ToolbarIconButton} from `@leafygreen-ui/toolbar`;

#### Props

| Prop | Type | Description | Default |
| ------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `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). | |
| `label` | `React.ReactNode` | Text that appears in the tooltip on hover/focus | |
| Prop | Type | Description | Default |
| ------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `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). | |
| `label` | `React.ReactNode` | Text that appears in the tooltip on hover/focus | |
| `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` |

\+ 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`

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { createRef } from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';

import { Toolbar } from '../Toolbar';
Expand Down Expand Up @@ -35,6 +36,48 @@ describe('packages/toolbar-icon-button', () => {
expect(ref.current).toBeDefined();
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});

test('shows tooltip on hover when isTooltipEnabled is true', async () => {
const { getByRole, findByRole } = render(
<Toolbar>
<ToolbarIconButton
glyph="Code"
label="Code Tooltip"
isTooltipEnabled={true}
/>
</Toolbar>,
);

const button = getByRole('button');
userEvent.hover(button);
Copy link
Preview

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The userEvent.hover call should be awaited since it returns a Promise in modern versions of @testing-library/user-event.

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only in RTL 14


// Tooltip should appear
const tooltip = await findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(tooltip).toHaveTextContent('Code Tooltip');
});

test('does not show tooltip on hover when isTooltipEnabled is false', async () => {
const { getByRole, queryByRole } = render(
<Toolbar>
<ToolbarIconButton
glyph="Code"
label="Code Tooltip"
isTooltipEnabled={false}
/>
</Toolbar>,
);

const button = getByRole('button');
userEvent.hover(button);
Copy link
Preview

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The userEvent.hover call should be awaited since it returns a Promise in modern versions of @testing-library/user-event.

Copilot uses AI. Check for mistakes.


// Wait a bit to ensure tooltip would have appeared if enabled
await new Promise(resolve => setTimeout(resolve, 100));

// Tooltip should not appear
const tooltip = queryByRole('tooltip');
expect(tooltip).not.toBeInTheDocument();
});
});

/* eslint-disable jest/no-disabled-tests */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const ToolbarIconButton = React.forwardRef<
glyph,
disabled = false,
active = false,
isTooltipEnabled = true,
'aria-label': ariaLabel,
...rest
}: ToolbarIconButtonProps,
Expand Down Expand Up @@ -60,6 +61,7 @@ export const ToolbarIconButton = React.forwardRef<
data-testid={`${lgIds.iconButtonTooltip}-${index}`}
data-lgid={`${lgIds.iconButtonTooltip}-${index}`}
align={Align.Left}
enabled={isTooltipEnabled}
trigger={
<div className={triggerStyles}>
<IconButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,11 @@ export interface ToolbarIconButtonProps extends ButtonProps {
* Callback fired when the ToolbarIconButton is clicked
*/
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;

/**
* Enables the tooltip to trigger based on hover events.
* When false, the tooltip will not show on hover.
* @default true
*/
isTooltipEnabled?: boolean;
}
Loading
Loading