diff --git a/configs/testing-library-compass/src/index.tsx b/configs/testing-library-compass/src/index.tsx index ea4910c149e..f114a6f318e 100644 --- a/configs/testing-library-compass/src/index.tsx +++ b/configs/testing-library-compass/src/index.tsx @@ -410,6 +410,13 @@ function unwrapContextMenuContainer(result: RenderResult) { firstChild instanceof HTMLElement && firstChild.getAttribute('data-testid') === 'context-menu-children-container' ) { + if ( + firstChild.firstChild instanceof HTMLElement && + firstChild.firstChild.getAttribute('data-testid') === + 'copy-paste-context-menu-container' + ) { + return { container: firstChild.firstChild, ...rest }; + } return { container: firstChild, ...rest }; } else { return { container, ...rest }; diff --git a/packages/compass-components/src/components/compass-components-provider.tsx b/packages/compass-components/src/components/compass-components-provider.tsx index 8da0abfaf86..789f79f51c0 100644 --- a/packages/compass-components/src/components/compass-components-provider.tsx +++ b/packages/compass-components/src/components/compass-components-provider.tsx @@ -12,6 +12,7 @@ import { ContextMenuProvider, } from './context-menu'; import { DrawerContentProvider } from './drawer-portal'; +import { CopyPasteContextMenu } from '../hooks/use-copy-paste-context-menu'; type GuideCueProviderProps = React.ComponentProps; @@ -174,15 +175,17 @@ export const CompassComponentsProvider = ({ onContextMenuOpen={onContextMenuOpen} onContextMenuItemClick={onContextMenuItemClick} > - - {typeof children === 'function' - ? children({ - darkMode, - portalContainerRef: setPortalContainer, - scrollContainerRef: setScrollContainer, - }) - : children} - + + + {typeof children === 'function' + ? children({ + darkMode, + portalContainerRef: setPortalContainer, + scrollContainerRef: setScrollContainer, + }) + : children} + + diff --git a/packages/compass-components/src/components/content-with-fallback.spec.tsx b/packages/compass-components/src/components/content-with-fallback.spec.tsx index 7c2e0b3a74b..d8329bc8751 100644 --- a/packages/compass-components/src/components/content-with-fallback.spec.tsx +++ b/packages/compass-components/src/components/content-with-fallback.spec.tsx @@ -62,10 +62,15 @@ describe('ContentWithFallback', function () { const [contentContainer, contextMenuContainer] = Array.from( container.children ); - expect(contentContainer.children.length).to.equal(0); + expect(contentContainer.children.length).to.equal(1); expect(contextMenuContainer.getAttribute('data-testid')).to.equal( 'context-menu-container' ); + const copyPasteContextMenu = contentContainer.children[0]; + expect(copyPasteContextMenu.children.length).to.equal(0); + expect(copyPasteContextMenu.getAttribute('data-testid')).to.equal( + 'copy-paste-context-menu-container' + ); }); it('should render fallback when the timeout passes', async function () { diff --git a/packages/compass-components/src/components/context-menu.tsx b/packages/compass-components/src/components/context-menu.tsx index 778bb31d16b..bbf2597a12e 100644 --- a/packages/compass-components/src/components/context-menu.tsx +++ b/packages/compass-components/src/components/context-menu.tsx @@ -119,6 +119,11 @@ export function ContextMenu({ data-text={item.label} data-testid={`menu-group-${groupIndex}-item-${itemIndex}`} className={itemStyles} + onMouseDown={(evt: React.MouseEvent) => { + // Keep focus on the element that was right-clicked to open the menu. + evt.preventDefault(); + evt.stopPropagation(); + }} onClick={(evt: React.MouseEvent) => { item.onAction?.(evt); onContextMenuItemClick?.(itemGroup, item); diff --git a/packages/compass-components/src/hooks/use-copy-paste-context-menu.spec.tsx b/packages/compass-components/src/hooks/use-copy-paste-context-menu.spec.tsx new file mode 100644 index 00000000000..cd428915b24 --- /dev/null +++ b/packages/compass-components/src/hooks/use-copy-paste-context-menu.spec.tsx @@ -0,0 +1,266 @@ +import React from 'react'; +import { + render, + screen, + userEvent, + waitFor, +} from '@mongodb-js/testing-library-compass'; +import { expect } from 'chai'; +import sinon from 'sinon'; + +describe('useCopyPasteContextMenu', function () { + let mockClipboard: { + writeText: sinon.SinonStub; + readText: sinon.SinonStub; + }; + let setExecCommand: boolean = false; + beforeEach(function () { + mockClipboard = { + writeText: sinon.stub().resolves(), + readText: sinon.stub().resolves('pasted text'), + }; + + // Create execCommand if it doesn't exist in test environment + if (!document.execCommand) { + setExecCommand = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (document as any).execCommand = () => true; + } + sinon.stub(document, 'execCommand').returns(true); + sinon + .stub(global.navigator.clipboard, 'writeText') + .callsFake(mockClipboard.writeText); + sinon + .stub(global.navigator.clipboard, 'readText') + .callsFake(mockClipboard.readText); + }); + + afterEach(function () { + sinon.restore(); + if (setExecCommand) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (document as any).execCommand; + setExecCommand = false; + } + }); + + const TestComponent = () => { + // The copy-paste functionality is already provided through the + // test rendering hook. So we only render a few elements + // that can be interacted with. + return ( +
+ +