diff --git a/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx index d439938d26e..207f793cbcd 100644 --- a/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx @@ -3,9 +3,11 @@ import * as React from 'react'; import { ContextMenu } from '../ContextMenu/ContextMenu'; +export type OpenMenuFunction = (e: React.KeyboardEvent | React.MouseEvent) => void; + export interface WithContextMenuProps { /** Menu item trigger that accepts openMenu prop */ - children: (props: { openMenu: React.MouseEventHandler }) => JSX.Element; + children: (props: { openMenu: OpenMenuFunction }) => JSX.Element; /** A function that returns an array of menu items */ renderMenuItems: () => React.ReactNode; /** On menu open focus the first element */ @@ -19,11 +21,19 @@ export const WithContextMenu = ({ children, renderMenuItems, focusOnOpen = true <> {children({ openMenu: (e) => { + let x = 0; + let y = 0; + if ('pageX' in e) { + x = e.pageX; + y = e.pageY - window.scrollY; + } else if ('currentTarget' in e) { + const target = e.currentTarget; + const rect = target.getBoundingClientRect(); + x = rect.left + rect.width / 2 + window.scrollX; + y = rect.top + rect.height / 2 + window.scrollY; + } setIsMenuOpen(true); - setMenuPosition({ - x: e.pageX, - y: e.pageY - window.scrollY, - }); + setMenuPosition({ x, y }); }, })} diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx index 64d7049d8d4..c7f675423a9 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx @@ -1,10 +1,12 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { selectors } from '@grafana/e2e-selectors'; import { DataLinksContextMenu } from './DataLinksContextMenu'; const fakeAriaLabel = 'fake aria label'; + describe('DataLinksContextMenu', () => { it('renders context menu when there are more than one data links', () => { render( @@ -24,9 +26,7 @@ describe('DataLinksContextMenu', () => { }, ]} > - {() => { - return
; - }} + {() =>
} ); @@ -46,13 +46,76 @@ describe('DataLinksContextMenu', () => { }, ]} > - {() => { - return
; - }} + {() =>
} ); expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument(); expect(screen.getByTestId(selectors.components.DataLinksContextMenu.singleLink)).toBeInTheDocument(); }); + + it('does not render anything when there are no data links', () => { + render( []}>{() =>
}); + + expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument(); + expect(screen.queryByTestId(selectors.components.DataLinksContextMenu.singleLink)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(selectors.components.Menu.MenuComponent('Context'))).not.toBeInTheDocument(); + }); + + describe('openMenu function', () => { + it('accepts mouse event', async () => { + render( + [ + { + href: '/link1', + title: 'Link1', + target: '_blank', + origin: {}, + }, + { + href: '/link2', + title: 'Link2', + target: '_blank', + origin: {}, + }, + ]} + > + {({ openMenu }) =>
} + + ); + + await userEvent.click(screen.getByLabelText(fakeAriaLabel)); + + expect(screen.getByLabelText(selectors.components.Menu.MenuComponent('Context'))).toBeInTheDocument(); + }); + + it('accepts keyboard event', async () => { + render( + [ + { + href: '/link1', + title: 'Link1', + target: '_blank', + origin: {}, + }, + { + href: '/link2', + title: 'Link2', + target: '_blank', + origin: {}, + }, + ]} + > + {({ openMenu }) =>
} + + ); + + await userEvent.click(screen.getByLabelText(fakeAriaLabel)); + await userEvent.keyboard('Enter'); + + expect(screen.getByLabelText(selectors.components.Menu.MenuComponent('Context'))).toBeInTheDocument(); + }); + }); }); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index 5e3b71cbe73..26618bfdc55 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -1,13 +1,12 @@ import { css } from '@emotion/css'; import { CSSProperties, type JSX } from 'react'; -import * as React from 'react'; import { ActionModel, GrafanaTheme2, LinkModel } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../themes/ThemeContext'; import { linkModelToContextMenuItems } from '../../utils/dataLinks'; -import { WithContextMenu } from '../ContextMenu/WithContextMenu'; +import { WithContextMenu, OpenMenuFunction } from '../ContextMenu/WithContextMenu'; import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup'; import { MenuItem } from '../Menu/MenuItem'; @@ -22,7 +21,7 @@ export interface DataLinksContextMenuProps { } export interface DataLinksContextMenuApi { - openMenu?: React.MouseEventHandler; + openMenu?: OpenMenuFunction; targetClassName?: string; } @@ -66,7 +65,7 @@ export const DataLinksContextMenu = ({ children, links, style }: DataLinksContex }} ); - } else { + } else if (linksCounter === 1) { const linkModel = links()[0]; return ( ); + } else { + return <>{children({})}; } };