Skip to content

Commit 392b350

Browse files
authored
[LG-2187] feat: Menu Prop Updates (#3240)
* [LG-2187] feat: Menu Prop Updates * fixed contextMenu * cleanup * updated changeset * rm old changeset * updated ContextMenu implementation
1 parent c6b4d3f commit 392b350

File tree

4 files changed

+117
-10
lines changed

4 files changed

+117
-10
lines changed

.changeset/cyan-taxes-cry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@leafygreen-ui/menu': major
3+
'@leafygreen-ui/code-editor': minor
4+
---
5+
6+
Updated menu to require either refEl or trigger prop

packages/code-editor/src/ContextMenu/ContextMenu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const ContextMenu = ({
4747
const [position, setPosition] = useState({ x: 0, y: 0 });
4848
const [selectedText, setSelectedText] = useState('');
4949
const containerRef = useRef<HTMLDivElement>(null);
50+
const menuRef = useRef<HTMLDivElement>(null);
5051

5152
/**
5253
* Handle showing and positioning custom menu onContextMenu
@@ -151,6 +152,8 @@ export const ContextMenu = ({
151152

152153
{isOpen && (
153154
<div className={getMenuContainerStyles(position)}>
155+
{/* This is a workaround to prevent the Menu from being rendered in the DOM when it's not open */}
156+
<div ref={menuRef} />
154157
<Menu
155158
/** Force re-mount when position changes so Menu recalculates its positioning */
156159
key={`${position.x}-${position.y}`}
@@ -160,6 +163,7 @@ export const ContextMenu = ({
160163
variant={MenuVariant.Compact}
161164
data-lgid={lgIds.contextMenu}
162165
data-testid={lgIds.contextMenu}
166+
refEl={menuRef}
163167
>
164168
{menuItems.map((item, index) => {
165169
if (item.isSeparator) {

packages/menu/src/Menu.spec.tsx

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -639,15 +639,102 @@ describe('packages/menu', () => {
639639
});
640640
});
641641

642-
// eslint-disable-next-line jest/no-disabled-tests
643-
describe.skip('Types work as expected', () => {
644-
test('Types work as expected', () => {
645-
render(
646-
<Menu data-testid="menu">
647-
<MenuItem>Item</MenuItem>
648-
<MenuItem>Item</MenuItem>
642+
describe('refEl and trigger props', () => {
643+
test('Menu works with only trigger prop', async () => {
644+
const trigger = <button data-testid="trigger-btn">Open Menu</button>;
645+
const { getByTestId, findByTestId } = render(
646+
<Menu trigger={trigger}>
647+
<MenuItem data-testid="menu-item">Item</MenuItem>
649648
</Menu>,
650649
);
650+
651+
const triggerBtn = getByTestId('trigger-btn');
652+
expect(triggerBtn).toBeInTheDocument();
653+
654+
userEvent.click(triggerBtn);
655+
656+
const menu = await findByTestId(lgIds.root);
657+
expect(menu).toBeInTheDocument();
658+
659+
const menuItem = getByTestId('menu-item');
660+
expect(menuItem).toBeInTheDocument();
661+
});
662+
663+
test('Menu works with only refEl prop', async () => {
664+
const refEl = React.createRef<HTMLButtonElement>();
665+
const { getByTestId, findByTestId } = render(
666+
<>
667+
<button ref={refEl} data-testid="ref-element">
668+
Reference Element
669+
</button>
670+
<Menu refEl={refEl} open>
671+
<MenuItem data-testid="menu-item">Item</MenuItem>
672+
</Menu>
673+
</>,
674+
);
675+
676+
const refElement = getByTestId('ref-element');
677+
expect(refElement).toBeInTheDocument();
678+
679+
const menu = await findByTestId(lgIds.root);
680+
expect(menu).toBeInTheDocument();
681+
682+
const menuItem = getByTestId('menu-item');
683+
expect(menuItem).toBeInTheDocument();
684+
});
685+
686+
test('Menu works with both trigger and refEl props', async () => {
687+
const refEl = React.createRef<HTMLDivElement>();
688+
const trigger = <button data-testid="trigger-btn">Open Menu</button>;
689+
const { getByTestId, findByTestId } = render(
690+
<>
691+
<div ref={refEl} data-testid="ref-element">
692+
Reference Element
693+
</div>
694+
<Menu trigger={trigger} refEl={refEl}>
695+
<MenuItem data-testid="menu-item">Item</MenuItem>
696+
</Menu>
697+
</>,
698+
);
699+
700+
const triggerBtn = getByTestId('trigger-btn');
701+
expect(triggerBtn).toBeInTheDocument();
702+
703+
userEvent.click(triggerBtn);
704+
705+
const menu = await findByTestId(lgIds.root);
706+
expect(menu).toBeInTheDocument();
707+
708+
const menuItem = getByTestId('menu-item');
709+
expect(menuItem).toBeInTheDocument();
710+
});
711+
});
712+
713+
// eslint-disable-next-line jest/no-disabled-tests
714+
describe.skip('Types work as expected', () => {
715+
test('Menu requires at least one of refEl or trigger', () => {
716+
const refEl = React.createRef<HTMLDivElement>();
717+
const trigger = <button>Open Menu</button>;
718+
719+
// @ts-expect-error - Missing both refEl and trigger
720+
<Menu>
721+
<MenuItem>Item</MenuItem>
722+
</Menu>;
723+
724+
// Valid: Menu with only trigger
725+
<Menu trigger={trigger}>
726+
<MenuItem>Item</MenuItem>
727+
</Menu>;
728+
729+
// Valid: Menu with only refEl
730+
<Menu refEl={refEl} open>
731+
<MenuItem>Item</MenuItem>
732+
</Menu>;
733+
734+
// Valid: Menu with both trigger and refEl
735+
<Menu trigger={trigger} refEl={refEl}>
736+
<MenuItem>Item</MenuItem>
737+
</Menu>;
651738
});
652739
});
653740
});

packages/menu/src/Menu/Menu.types.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ReactElement, ReactNode } from 'react';
22

3-
import { LgIdProps } from '@leafygreen-ui/lib';
3+
import type { Either, LgIdProps } from '@leafygreen-ui/lib';
44
import {
55
InferredPolymorphicPropsWithRef,
66
PolymorphicAs,
@@ -19,9 +19,9 @@ export const MenuVariant = {
1919
} as const;
2020
export type MenuVariant = (typeof MenuVariant)[keyof typeof MenuVariant];
2121

22-
export interface MenuProps
22+
interface BaseMenuProps
2323
extends Omit<React.ComponentPropsWithoutRef<'ul'>, 'onClick'>,
24-
Omit<PopoverProps, 'active' | 'dismissMode' | 'onToggle'>,
24+
Omit<PopoverProps, 'active' | 'dismissMode' | 'onToggle' | 'refEl'>,
2525
LgIdProps {
2626
/**
2727
* The menu items, or submenus
@@ -34,6 +34,11 @@ export interface MenuProps
3434
*/
3535
'data-testid'?: string;
3636

37+
/**
38+
* A reference to the element against which the popover component will be positioned.
39+
*/
40+
refEl?: React.RefObject<HTMLElement>;
41+
3742
/**
3843
* A slot for the element used to trigger the Menu. Passing a trigger allows
3944
* Menu to control opening and closing itself internally.
@@ -107,3 +112,8 @@ export interface MenuProps
107112
*/
108113
variant?: MenuVariant;
109114
}
115+
116+
/**
117+
* Menu props require at least one of `refEl` or `trigger` to be provided
118+
*/
119+
export type MenuProps = Either<BaseMenuProps, 'refEl' | 'trigger'>;

0 commit comments

Comments
 (0)