ButtonSelect: Fixes menu shadow (fixes issue with RefreshPicker) (#111431)

* ButtonSelect: Fixes menu shadow

* Update e2e tests to look in portal
This commit is contained in:
Torkel Ödegaard 2025-09-22 19:55:27 +02:00 committed by GitHub
parent bd550d2f06
commit 28c19036f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 30 additions and 81 deletions

View File

@ -3,13 +3,14 @@ import { test, expect } from '@grafana/plugin-e2e';
import pluginJson from '../plugin.json';
import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json';
import { testIds } from '../testIds';
import { selectors } from '@grafana/e2e-selectors';
test.describe('grafana-extensionstest-app', { tag: ['@plugins'] }, () => {
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
await page.goto(`/a/${pluginJson.id}/added-links`);
const section = await page.getByTestId(testIds.addedLinksPage.section1);
await section.getByTestId(testIds.actions.button).click();
await page.getByTestId(testIds.container).getByText('Go to A').click();
await page.getByTestId(selectors.components.Portal.container).getByText('Go to A').click();
await page.getByTestId(testIds.modal.open).click();
await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
});
@ -18,7 +19,7 @@ test.describe('grafana-extensionstest-app', { tag: ['@plugins'] }, () => {
await page.goto(`/a/${pluginJson.id}/added-links`);
const section = await page.getByTestId(testIds.addedLinksPage.section1);
await section.getByTestId(testIds.actions.button).click();
await page.getByTestId(testIds.container).getByText('Open from B').click();
await page.getByTestId(selectors.components.Portal.container).getByText('Open from B').click();
await expect(page.getByTestId(testIds.appB.modal)).toBeVisible();
});
@ -26,7 +27,7 @@ test.describe('grafana-extensionstest-app', { tag: ['@plugins'] }, () => {
await page.goto(`/a/${pluginJson.id}/added-links`);
const section = await page.getByTestId(testIds.addedLinksPage.section1);
await section.getByTestId(testIds.actions.button).click();
await page.getByTestId(testIds.container).getByText('Basic link').click();
await page.getByTestId(selectors.components.Portal.container).getByText('Basic link').click();
await page.getByTestId(testIds.modal.open).click();
await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
});

View File

@ -1,17 +1,15 @@
import { css } from '@emotion/css';
import { autoUpdate, offset, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react';
import { FocusScope } from '@react-aria/focus';
import { memo, HTMLAttributes, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { SelectableValue } from '@grafana/data';
import { useStyles2 } from '../../themes/ThemeContext';
import { getPositioningMiddleware } from '../../utils/floating';
import { Menu } from '../Menu/Menu';
import { MenuItem } from '../Menu/MenuItem';
import { ScrollContainer } from '../ScrollContainer/ScrollContainer';
import { ToolbarButton, ToolbarButtonVariant } from '../ToolbarButton/ToolbarButton';
import { PopoverContent } from '../Tooltip/types';
import { Dropdown } from './Dropdown';
export interface Props<T> extends HTMLAttributes<HTMLButtonElement> {
className?: string;
options: Array<SelectableValue<T>>;
@ -29,70 +27,34 @@ export interface Props<T> extends HTMLAttributes<HTMLButtonElement> {
*/
const ButtonSelectComponent = <T,>(props: Props<T>) => {
const { className, options, value, onChange, narrow, variant, ...restProps } = props;
const styles = useStyles2(getStyles);
const [isOpen, setIsOpen] = useState(false);
const placement = 'bottom-end';
// the order of middleware is important!
const middleware = [offset(0), ...getPositioningMiddleware(placement)];
const { context, refs, floatingStyles } = useFloating({
open: isOpen,
placement,
onOpenChange: setIsOpen,
middleware,
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
const onChangeInternal = (item: SelectableValue<T>) => {
onChange(item);
setIsOpen(false);
};
const renderMenu = () => (
<Menu tabIndex={-1} onClose={() => setIsOpen(false)}>
<ScrollContainer maxHeight="100vh">
{options.map((item) => (
<MenuItem
key={`${item.value}`}
label={item.label ?? String(item.value)}
onClick={() => onChange(item)}
active={item.value === value?.value}
ariaChecked={item.value === value?.value}
ariaLabel={item.ariaLabel || item.label}
disabled={item.isDisabled}
component={item.component}
role="menuitemradio"
/>
))}
</ScrollContainer>
</Menu>
);
return (
<div className={styles.wrapper}>
<ToolbarButton
className={className}
isOpen={isOpen}
narrow={narrow}
variant={variant}
ref={refs.setReference}
{...getReferenceProps()}
{...restProps}
>
<Dropdown overlay={renderMenu} placement="bottom-end">
<ToolbarButton className={className} isOpen={isOpen} narrow={narrow} variant={variant} {...restProps}>
{value?.label || (value?.value != null ? String(value?.value) : null)}
</ToolbarButton>
{isOpen && (
<div className={styles.menuWrapper} ref={refs.setFloating} {...getFloatingProps()} style={floatingStyles}>
<FocusScope contain autoFocus restoreFocus>
{/*
tabIndex=-1 is needed here to support highlighting text within the menu when using FocusScope
see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668
*/}
<Menu tabIndex={-1} onClose={() => setIsOpen(false)}>
{options.map((item) => (
<MenuItem
key={`${item.value}`}
label={item.label ?? String(item.value)}
onClick={() => onChangeInternal(item)}
active={item.value === value?.value}
ariaChecked={item.value === value?.value}
ariaLabel={item.ariaLabel || item.label}
disabled={item.isDisabled}
component={item.component}
role="menuitemradio"
/>
))}
</Menu>
</FocusScope>
</div>
)}
</div>
</Dropdown>
);
};
@ -102,17 +64,3 @@ ButtonSelectComponent.displayName = 'ButtonSelect';
// see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/37087#issuecomment-656596623
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
export const ButtonSelect = memo(ButtonSelectComponent) as typeof ButtonSelectComponent;
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css({
position: 'relative',
display: 'inline-flex',
}),
menuWrapper: css({
zIndex: theme.zIndex.dropdown,
maxHeight: '100vh',
overflow: 'auto',
}),
};
};