-
Notifications
You must be signed in to change notification settings - Fork 229
feat(compass-components): add context menu COMPASS-9386 #6956
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
gagik
wants to merge
58
commits into
main
Choose a base branch
from
gagik/context-menu-compass-ui
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+553
−189
Open
Changes from all commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
297a943
feat(compass-context-menu): add a headless context menu package
gagik 6838513
wip
gagik 8e30a83
fix: add tests
gagik 7d17984
fix: add tests and fix types
gagik 6004593
refactor: minor stylistic changes
gagik 43023f4
fix: export types and rename MenuItem
gagik 971b0fd
fix: basic UI implementation
gagik 0ad7a63
fix: use React.RefCallback
gagik aa9f0cf
fix: switch to item-based organization
gagik 58df56a
fix: cleanup and switch to menu prop
gagik a54c738
refactor: use item groups instead of React elements, use wrapper, kee…
gagik 731ac13
Merge branch 'gagik/headless-context-menu' of github.com:mongodb-js/c…
gagik 56d11b6
fix: revert test environment
gagik 743f068
fix: justify start to make it prefer right way popups and remove comment
gagik ab14b78
fix: remove redundant context import
gagik 1d279ca
feat(compass-context-menu): add a headless context menu package
gagik 1efdb2e
wip
gagik 0f5303b
fix: add tests
gagik 33b2a41
fix: add tests and fix types
gagik 4a2f032
refactor: minor stylistic changes
gagik 8e1feb3
fix: export types and rename MenuItem
gagik 1f55000
fix: use React.RefCallback
gagik 9618e8e
refactor: use item groups instead of React elements, use wrapper, kee…
gagik ca1fb86
fix: delete redundant context menu
gagik a285d63
Merge branch 'gagik/headless-context-menu' of github.com:mongodb-js/c…
gagik aacdf1d
feat(compass-context-menu): add a headless context menu package
gagik cb99706
wip
gagik da22b51
fix: add tests
gagik 78ff94c
fix: add tests and fix types
gagik a24e89c
refactor: minor stylistic changes
gagik f3869ea
fix: export types and rename MenuItem
gagik c2d9ac1
fix: use React.RefCallback
gagik 1c6ef03
refactor: use item groups instead of React elements, use wrapper, kee…
gagik 3d14d6d
fix: delete redundant context menu
gagik ee658e1
fix: remove unused dep
gagik a173aae
Merge branch 'gagik/headless-context-menu' of github.com:mongodb-js/c…
gagik 4730c18
fix: add tests and fix bug with menu auto-closing
gagik eb65769
fix: enforce no nesting, adjsut enzyme test and move setup to testing…
gagik 220a300
fix: move to compass components provider
gagik 74f3d6e
fix: remove unintended deletion
gagik 4f05381
fix: adjust tests
gagik 4ad7231
Merge branch 'main' of github.com:mongodb-js/compass into gagik/conte…
gagik cbb0656
Merge branch 'main' of github.com:mongodb-js/compass into gagik/headl…
gagik c6d7b03
Merge branch 'gagik/headless-context-menu' of github.com:mongodb-js/c…
gagik 6b6e398
fix: throw early on
gagik 17c05f0
Merge branch 'main' of github.com:mongodb-js/compass into gagik/conte…
gagik 6dadf25
fix: correct wrapper use
gagik a62690b
Merge branch 'main' of github.com:mongodb-js/compass into gagik/conte…
gagik 4dafa7b
fix: use render directly for compass-context-menu
gagik 915c597
fix: use testingLibrary's render
gagik 8f306e4
feat: memoize items for context menu
gagik aa52fb7
fix: adjust dependencies
gagik fb9c599
fix: support nesting
gagik b684e85
Merge branch 'main' into gagik/context-menu-compass-ui
kraenhansen 9a8f24f
Fix memoization of event listeners
kraenhansen 364a081
Use top-level render mode and refs
kraenhansen 26db661
Fix update of menu position
kraenhansen fc243d0
Use nesting instead of refEl
kraenhansen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
packages/compass-components/src/components/context-menu.spec.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import React from 'react'; | ||
import { render, screen, userEvent } from '@mongodb-js/testing-library-compass'; | ||
import { expect } from 'chai'; | ||
import sinon from 'sinon'; | ||
import { ContextMenuProvider } from '@mongodb-js/compass-context-menu'; | ||
import { useContextMenuItems, ContextMenu } from './context-menu'; | ||
import type { ContextMenuItem } from '@mongodb-js/compass-context-menu'; | ||
|
||
describe('useContextMenuItems', function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might be missing a test to ensure the menu closes once clicked? |
||
const menuTestTriggerId = 'test-trigger'; | ||
|
||
const TestComponent = ({ | ||
items, | ||
children, | ||
'data-testid': dataTestId = menuTestTriggerId, | ||
}: { | ||
items: ContextMenuItem[]; | ||
children?: React.ReactNode; | ||
'data-testid'?: string; | ||
}) => { | ||
const ref = useContextMenuItems(() => items, [items]); | ||
|
||
return ( | ||
<div data-testid={dataTestId} ref={ref}> | ||
Test Component | ||
{children} | ||
</div> | ||
); | ||
}; | ||
|
||
it('works with nested providers, using the parent provider', function () { | ||
const items = [ | ||
{ | ||
label: 'Test Item', | ||
onAction: () => {}, | ||
}, | ||
]; | ||
|
||
const { container } = render( | ||
<ContextMenuProvider menuWrapper={ContextMenu}> | ||
<ContextMenuProvider menuWrapper={ContextMenu}> | ||
<TestComponent items={items} /> | ||
</ContextMenuProvider> | ||
</ContextMenuProvider> | ||
); | ||
|
||
// Should only find one context menu (from the parent provider) | ||
expect( | ||
container.querySelectorAll('[data-testid="context-menu"]') | ||
).to.have.length(1); | ||
// Should still render the trigger | ||
expect(screen.getByTestId(menuTestTriggerId)).to.exist; | ||
}); | ||
|
||
it('renders without error', function () { | ||
const items = [ | ||
{ | ||
label: 'Test Item', | ||
onAction: () => {}, | ||
}, | ||
]; | ||
|
||
render(<TestComponent items={items} />); | ||
|
||
expect(screen.getByTestId(menuTestTriggerId)).to.exist; | ||
}); | ||
|
||
it('shows context menu with items on right click', function () { | ||
const items = [ | ||
{ | ||
label: 'Test Item 1', | ||
onAction: () => {}, | ||
}, | ||
{ | ||
label: 'Test Item 2', | ||
onAction: () => {}, | ||
}, | ||
]; | ||
|
||
render(<TestComponent items={items} />); | ||
|
||
const trigger = screen.getByTestId(menuTestTriggerId); | ||
userEvent.click(trigger, { button: 2 }); | ||
|
||
// The menu items should be rendered | ||
expect(screen.getByTestId('menu-group-0-item-0')).to.exist; | ||
expect(screen.getByTestId('menu-group-0-item-1')).to.exist; | ||
}); | ||
|
||
it('triggers the correct action when menu item is clicked', function () { | ||
const onAction = sinon.spy(); | ||
const items = [ | ||
{ | ||
label: 'Test Item 1', | ||
onAction: () => onAction(1), | ||
}, | ||
{ | ||
label: 'Test Item 2', | ||
onAction: () => onAction(2), | ||
}, | ||
]; | ||
|
||
render(<TestComponent items={items} />); | ||
|
||
const trigger = screen.getByTestId(menuTestTriggerId); | ||
userEvent.click(trigger, { button: 2 }); | ||
|
||
const menuItem = screen.getByTestId('menu-group-0-item-1'); | ||
userEvent.click(menuItem); | ||
|
||
expect(onAction).to.have.been.calledOnceWithExactly(2); | ||
}); | ||
|
||
describe('with nested components', function () { | ||
const childTriggerId = 'child-trigger'; | ||
|
||
beforeEach(function () { | ||
const items = [ | ||
{ | ||
label: 'Test Item 1', | ||
onAction: () => {}, | ||
}, | ||
{ | ||
label: 'Test Item 2', | ||
onAction: () => {}, | ||
}, | ||
]; | ||
|
||
const childItems = [ | ||
{ | ||
label: 'Child Item 1', | ||
onAction: () => {}, | ||
}, | ||
]; | ||
|
||
render( | ||
<TestComponent items={items}> | ||
<TestComponent items={childItems} data-testid={childTriggerId} /> | ||
</TestComponent> | ||
); | ||
}); | ||
|
||
it('renders menu items with separators', function () { | ||
const trigger = screen.getByTestId(childTriggerId); | ||
userEvent.click(trigger, { button: 2 }); | ||
|
||
// Should find the menu item and the separator | ||
expect(screen.getByTestId('menu-group-0').children.length).to.equal(2); | ||
expect( | ||
screen.getByTestId('menu-group-0').children.item(0)?.textContent | ||
).to.equal('Child Item 1'); | ||
|
||
expect(screen.getByTestId('menu-group-0-separator')).to.exist; | ||
|
||
expect(screen.getByTestId('menu-group-1').children.length).to.equal(2); | ||
expect( | ||
screen.getByTestId('menu-group-1').children.item(0)?.textContent | ||
).to.equal('Test Item 1'); | ||
expect( | ||
screen.getByTestId('menu-group-1').children.item(1)?.textContent | ||
).to.equal('Test Item 2'); | ||
|
||
expect(screen.queryByTestId('menu-group-1-separator')).not.to.exist; | ||
}); | ||
}); | ||
}); |
102 changes: 102 additions & 0 deletions
102
packages/compass-components/src/components/context-menu.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import React, { useEffect, useMemo, useRef } from 'react'; | ||
import { Menu, MenuItem, MenuSeparator } from './leafygreen'; | ||
import type { ContextMenuItem } from '@mongodb-js/compass-context-menu'; | ||
import { useContextMenu } from '@mongodb-js/compass-context-menu'; | ||
import { ContextMenuProvider as ContextMenuProviderBase } from '@mongodb-js/compass-context-menu'; | ||
import type { | ||
ContextMenuItemGroup, | ||
ContextMenuWrapperProps, | ||
} from '@mongodb-js/compass-context-menu'; | ||
|
||
export function ContextMenuProvider({ | ||
children, | ||
}: { | ||
children: React.ReactNode; | ||
}) { | ||
return ( | ||
<ContextMenuProviderBase menuWrapper={ContextMenu}> | ||
{children} | ||
</ContextMenuProviderBase> | ||
); | ||
} | ||
|
||
export function ContextMenu({ menu }: ContextMenuWrapperProps) { | ||
const menuRef = useRef(null); | ||
|
||
const position = menu.position; | ||
const itemGroups = menu.itemGroups; | ||
|
||
useEffect(() => { | ||
if (!menu.isOpen) { | ||
menu.close(); | ||
} | ||
}, [menu.isOpen]); | ||
|
||
return ( | ||
<div | ||
data-testid="context-menu" | ||
style={{ | ||
position: 'absolute', | ||
left: position.x, | ||
top: position.y, | ||
// This is to ensure the menu gets positioned correctly as the left and top updates | ||
width: 1, | ||
height: 1, | ||
}} | ||
> | ||
<Menu | ||
ref={menuRef} | ||
open={menu.isOpen} | ||
setOpen={menu.close} | ||
justify="start" | ||
> | ||
{itemGroups.map( | ||
(itemGroup: ContextMenuItemGroup, groupIndex: number) => { | ||
return ( | ||
<div | ||
key={`menu-group-${groupIndex}`} | ||
data-testid={`menu-group-${groupIndex}`} | ||
> | ||
{itemGroup.items.map( | ||
(item: ContextMenuItem, itemIndex: number) => { | ||
return ( | ||
<MenuItem | ||
key={`menu-group-${groupIndex}-item-${itemIndex}`} | ||
data-text={item.label} | ||
data-testid={`menu-group-${groupIndex}-item-${itemIndex}`} | ||
onClick={(evt: React.MouseEvent) => { | ||
item.onAction?.(evt); | ||
menu.close(); | ||
}} | ||
> | ||
{item.label} | ||
</MenuItem> | ||
); | ||
} | ||
)} | ||
{groupIndex < itemGroups.length - 1 && ( | ||
<div | ||
key={`menu-group-${groupIndex}-separator`} | ||
data-testid={`menu-group-${groupIndex}-separator`} | ||
> | ||
<MenuSeparator /> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
} | ||
)} | ||
</Menu> | ||
</div> | ||
); | ||
} | ||
|
||
export function useContextMenuItems( | ||
getItems: () => ContextMenuItem[], | ||
dependencies: React.DependencyList | undefined | ||
): React.RefCallback<HTMLElement> { | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
const memoizedItems = useMemo(getItems, dependencies); | ||
const contextMenu = useContextMenu(); | ||
Anemy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return contextMenu.registerItems(memoizedItems); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one might want to keep this empty... the reason the context menu always exists even when closed is for the sake of animations