mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelHeaderMenu: Use UI/Menu component (#63040)
This commit is contained in:
@@ -12,6 +12,7 @@ import { LoadingState, PreferredVisualisationType } from './data';
|
||||
import { DataFrame, FieldType } from './dataFrame';
|
||||
import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource';
|
||||
import { FieldConfigSource } from './fieldOverrides';
|
||||
import { IconName } from './icon';
|
||||
import { OptionEditorConfig } from './options';
|
||||
import { PluginMeta } from './plugin';
|
||||
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
|
||||
@@ -156,7 +157,7 @@ export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = an
|
||||
export interface PanelMenuItem {
|
||||
type?: 'submenu' | 'divider';
|
||||
text: string;
|
||||
iconClassName?: string;
|
||||
iconClassName?: IconName;
|
||||
onClick?: (event: React.MouseEvent<any>) => void;
|
||||
shortcut?: string;
|
||||
href?: string;
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface MenuItemProps<T = any> {
|
||||
/** Url of the menu item */
|
||||
url?: string;
|
||||
/** Handler for the click behaviour */
|
||||
onClick?: (event?: React.MouseEvent<HTMLElement>, payload?: T) => void;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>, payload?: T) => void;
|
||||
/** Custom MenuItem styles*/
|
||||
className?: string;
|
||||
/** Active */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { CSSProperties, ReactElement, useRef } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { CSSProperties, ReactElement, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
@@ -9,7 +9,7 @@ import { Icon } from '../Icon/Icon';
|
||||
|
||||
import { MenuItemProps } from './MenuItem';
|
||||
import { useMenuFocus } from './hooks';
|
||||
import { getPosition } from './utils';
|
||||
import { isElementOverflowing } from './utils';
|
||||
|
||||
/** @internal */
|
||||
export interface SubMenuProps {
|
||||
@@ -40,6 +40,13 @@ export const SubMenu: React.FC<SubMenuProps> = React.memo(
|
||||
close,
|
||||
});
|
||||
|
||||
const [pushLeft, setPushLeft] = useState(false);
|
||||
useEffect(() => {
|
||||
if (isOpen && localRef.current) {
|
||||
setPushLeft(isElementOverflowing(localRef.current));
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.iconWrapper} aria-label={selectors.components.Menu.SubMenu.icon}>
|
||||
@@ -48,7 +55,7 @@ export const SubMenu: React.FC<SubMenuProps> = React.memo(
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={localRef}
|
||||
className={styles.subMenu(localRef.current)}
|
||||
className={cx(styles.subMenu, { [styles.pushLeft]: pushLeft })}
|
||||
aria-label={selectors.components.Menu.SubMenu.container}
|
||||
style={customStyle}
|
||||
>
|
||||
@@ -83,11 +90,15 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
display: inline-block;
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
`,
|
||||
subMenu: (element: HTMLElement | null) => css`
|
||||
pushLeft: css`
|
||||
right: 100%;
|
||||
left: unset;
|
||||
`,
|
||||
subMenu: css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
${getPosition(element)}: 100%;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getPosition } from './utils';
|
||||
import { isElementOverflowing } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
it('getPosition', () => {
|
||||
it('isElementOverflowing', () => {
|
||||
const getElement = (right: number, width: number) =>
|
||||
({
|
||||
parentElement: {
|
||||
@@ -12,9 +12,9 @@ describe('utils', () => {
|
||||
|
||||
Object.defineProperty(window, 'innerWidth', { value: 1000 });
|
||||
|
||||
expect(getPosition(null)).toBe('left');
|
||||
expect(getPosition(getElement(900, 100))).toBe('right');
|
||||
expect(getPosition(getElement(800, 100))).toBe('left');
|
||||
expect(getPosition(getElement(1200, 0))).toBe('left');
|
||||
expect(isElementOverflowing(null)).toBe(false);
|
||||
expect(isElementOverflowing(getElement(900, 100))).toBe(true);
|
||||
expect(isElementOverflowing(getElement(800, 100))).toBe(false);
|
||||
expect(isElementOverflowing(getElement(1200, 0))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
/**
|
||||
* Returns where the subMenu should be positioned (left or right)
|
||||
* Returns whether the provided element overflows the viewport bounds
|
||||
*
|
||||
* @param element HTMLElement for the subMenu wrapper
|
||||
* @param element The element we want to know about
|
||||
*/
|
||||
export const getPosition = (element: HTMLElement | null) => {
|
||||
export const isElementOverflowing = (element: HTMLElement | null) => {
|
||||
if (!element) {
|
||||
return 'left';
|
||||
return false;
|
||||
}
|
||||
|
||||
const wrapperPos = element.parentElement!.getBoundingClientRect();
|
||||
const pos = element.getBoundingClientRect();
|
||||
|
||||
if (pos.width === 0) {
|
||||
return 'left';
|
||||
}
|
||||
|
||||
if (wrapperPos.right + pos.width + 10 > window.innerWidth) {
|
||||
return 'right';
|
||||
} else {
|
||||
return 'left';
|
||||
}
|
||||
return pos.width !== 0 && wrapperPos.right + pos.width + 10 > window.innerWidth;
|
||||
};
|
||||
|
||||
@@ -165,6 +165,7 @@ export function PanelChrome({
|
||||
<PanelMenu
|
||||
menu={menu}
|
||||
title={title}
|
||||
placement="bottom-end"
|
||||
menuButtonClass={cx(styles.menuItem, dragClassCancel, 'show-on-hover')}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user