diff --git a/packages/grafana-ui/src/components/Menu/Menu.mdx b/packages/grafana-ui/src/components/Menu/Menu.mdx
new file mode 100644
index 00000000000..297e6b7ce7c
--- /dev/null
+++ b/packages/grafana-ui/src/components/Menu/Menu.mdx
@@ -0,0 +1,34 @@
+import { Meta, Props } from '@storybook/addon-docs/blocks';
+import { Overlay } from 'ol';
+
+import { Menu } from './Menu';
+
+
+
+# Menu
+
+A simple menu component.
+
+### When to use
+
+When you need to display a list of actions or navigation options in a dropdown.
+
+### Usage
+
+```tsx
+import { Dropdown, Menu, Button } from '@grafana/ui';
+const menu = (
+
+
+
+
+
+);
+return (
+
+
+
+);
+```
+
+
diff --git a/packages/grafana-ui/src/components/Menu/Menu.story.internal.tsx b/packages/grafana-ui/src/components/Menu/Menu.story.internal.tsx
deleted file mode 100644
index c6190dc4b7d..00000000000
--- a/packages/grafana-ui/src/components/Menu/Menu.story.internal.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { Story, ComponentMeta } from '@storybook/react';
-import React from 'react';
-
-import { GraphContextMenuHeader } from '..';
-import { StoryExample } from '../../utils/storybook/StoryExample';
-import { VerticalGroup } from '../Layout/Layout';
-
-import { Menu } from './Menu';
-import { MenuGroup } from './MenuGroup';
-import { MenuItem } from './MenuItem';
-
-const meta: ComponentMeta = {
- title: 'General/Menu',
- component: Menu,
- argTypes: {},
- parameters: {
- knobs: {
- disabled: true,
- },
- controls: {
- disabled: true,
- },
- actions: {
- disabled: true,
- },
- },
-};
-
-export const Simple: Story = (args) => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- ,
- ,
- ,
- ,
- ]}
- />,
- ]}
- />
-
-
-
-
- );
-};
-
-Simple.args = {
- header: (
-
- ),
-};
-
-export default meta;
diff --git a/packages/grafana-ui/src/components/Menu/Menu.story.tsx b/packages/grafana-ui/src/components/Menu/Menu.story.tsx
new file mode 100644
index 00000000000..80499f0e3b5
--- /dev/null
+++ b/packages/grafana-ui/src/components/Menu/Menu.story.tsx
@@ -0,0 +1,104 @@
+import { ComponentMeta } from '@storybook/react';
+import React from 'react';
+
+import { GraphContextMenuHeader } from '..';
+import { StoryExample } from '../../utils/storybook/StoryExample';
+import { VerticalGroup } from '../Layout/Layout';
+
+import { Menu } from './Menu';
+import mdx from './Menu.mdx';
+
+const meta: ComponentMeta = {
+ title: 'General/Menu',
+ component: Menu,
+ argTypes: {},
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ knobs: {
+ disabled: true,
+ },
+ controls: {
+ disabled: true,
+ },
+ actions: {
+ disabled: true,
+ },
+ },
+};
+
+export function Examples() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ ariaLabel="Menu header"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ ,
+ ,
+ ,
+ ,
+ ]}
+ />,
+ ]}
+ />
+
+
+
+
+ );
+}
+
+export default meta;
diff --git a/packages/grafana-ui/src/components/Menu/Menu.tsx b/packages/grafana-ui/src/components/Menu/Menu.tsx
index 1104dac5e9d..1b3da92425f 100644
--- a/packages/grafana-ui/src/components/Menu/Menu.tsx
+++ b/packages/grafana-ui/src/components/Menu/Menu.tsx
@@ -5,9 +5,11 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
+import { MenuDivider } from './MenuDivider';
+import { MenuGroup } from './MenuGroup';
+import { MenuItem } from './MenuItem';
import { useMenuFocus } from './hooks';
-/** @internal */
export interface MenuProps extends React.HTMLAttributes {
/** React element rendered at the top of the menu */
header?: React.ReactNode;
@@ -18,8 +20,7 @@ export interface MenuProps extends React.HTMLAttributes {
onKeyDown?: React.KeyboardEventHandler;
}
-/** @internal */
-export const Menu = React.forwardRef(
+const MenuComp = React.forwardRef(
({ header, children, ariaLabel, onOpen, onClose, onKeyDown, ...otherProps }, forwardedRef) => {
const styles = useStyles2(getStyles);
@@ -44,9 +45,15 @@ export const Menu = React.forwardRef(
);
}
);
-Menu.displayName = 'Menu';
-/** @internal */
+MenuComp.displayName = 'Menu';
+
+export const Menu = Object.assign(MenuComp, {
+ Item: MenuItem,
+ Divider: MenuDivider,
+ Group: MenuGroup,
+});
+
const getStyles = (theme: GrafanaTheme2) => {
return {
header: css`
@@ -58,6 +65,7 @@ const getStyles = (theme: GrafanaTheme2) => {
box-shadow: ${theme.shadows.z3};
display: inline-block;
border-radius: ${theme.shape.borderRadius()};
+ padding: ${theme.spacing(0.5, 0)};
`,
};
};
diff --git a/packages/grafana-ui/src/components/Menu/MenuDivider.tsx b/packages/grafana-ui/src/components/Menu/MenuDivider.tsx
new file mode 100644
index 00000000000..cf818fcad36
--- /dev/null
+++ b/packages/grafana-ui/src/components/Menu/MenuDivider.tsx
@@ -0,0 +1,21 @@
+import { css } from '@emotion/css';
+import React from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+
+import { useStyles2 } from '../../themes';
+
+export function MenuDivider() {
+ const styles = useStyles2(getStyles);
+ return
;
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ divider: css({
+ height: 1,
+ backgroundColor: theme.colors.border.weak,
+ margin: theme.spacing(0.5, 0),
+ }),
+ };
+};
diff --git a/packages/grafana-ui/src/components/Menu/MenuItem.tsx b/packages/grafana-ui/src/components/Menu/MenuItem.tsx
index 7f47ba10b92..f6f271478d6 100644
--- a/packages/grafana-ui/src/components/Menu/MenuItem.tsx
+++ b/packages/grafana-ui/src/components/Menu/MenuItem.tsx
@@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
-import React, { ReactElement, useCallback, useMemo, useState, useRef, useImperativeHandle } from 'react';
+import React, { ReactElement, useCallback, useState, useRef, useImperativeHandle } from 'react';
import { GrafanaTheme2, LinkTarget } from '@grafana/data';
@@ -35,9 +35,9 @@ export interface MenuItemProps {
className?: string;
/** Active */
active?: boolean;
-
+ /** Show in destructive style (error color) */
+ destructive?: boolean;
tabIndex?: number;
-
/** List of menu items for the subMenu */
childItems?: Array>;
}
@@ -55,6 +55,7 @@ export const MenuItem = React.memo(
onClick,
className,
active,
+ destructive,
childItems,
role = 'menuitem',
tabIndex = -1,
@@ -71,12 +72,14 @@ export const MenuItem = React.memo(
setIsSubMenuOpen(false);
setIsActive(false);
}, []);
- const hasSubMenu = useMemo(() => childItems && childItems.length > 0, [childItems]);
- const Wrapper = hasSubMenu ? 'div' : url === undefined ? 'button' : 'a';
+
+ const hasSubMenu = childItems && childItems.length > 0;
+ const ItemElement = hasSubMenu ? 'div' : url === undefined ? 'button' : 'a';
const itemStyle = cx(
{
[styles.item]: true,
- [styles.activeItem]: isActive,
+ [styles.active]: isActive,
+ [styles.destructive]: destructive,
},
className
);
@@ -107,7 +110,7 @@ export const MenuItem = React.memo(
};
return (
-
)}
-
+
);
})
);
+
MenuItem.displayName = 'MenuItem';
-/** @internal */
const getStyles = (theme: GrafanaTheme2) => {
return {
item: css`
@@ -159,7 +162,9 @@ const getStyles = (theme: GrafanaTheme2) => {
white-space: nowrap;
color: ${theme.colors.text.primary};
display: flex;
- padding: 5px 12px 5px 10px;
+ align-items: center;
+ padding: ${theme.spacing(0.5, 2)};
+ min-height: ${theme.spacing(4)};
margin: 0;
border: none;
width: 100%;
@@ -177,12 +182,31 @@ const getStyles = (theme: GrafanaTheme2) => {
${getFocusStyles(theme)}
}
`,
- activeItem: css`
- background: ${theme.colors.action.selected};
+ active: css`
+ background: ${theme.colors.action.hover};
+ `,
+ destructive: css`
+ color: ${theme.colors.error.text};
+
+ svg {
+ color: ${theme.colors.error.text};
+ }
+
+ &:hover,
+ &:focus,
+ &:focus-visible {
+ background: ${theme.colors.error.main};
+ color: ${theme.colors.error.contrastText};
+
+ svg {
+ color: ${theme.colors.error.contrastText};
+ }
+ }
`,
icon: css`
opacity: 0.7;
margin-right: 10px;
+ margin-left: -4px;
color: ${theme.colors.text.secondary};
`,
};
diff --git a/packages/grafana-ui/src/components/Menu/SubMenu.tsx b/packages/grafana-ui/src/components/Menu/SubMenu.tsx
index dfab72235e3..2f8c170dbee 100644
--- a/packages/grafana-ui/src/components/Menu/SubMenu.tsx
+++ b/packages/grafana-ui/src/components/Menu/SubMenu.tsx
@@ -58,6 +58,7 @@ export const SubMenu: React.FC = React.memo(
);
}
);
+
SubMenu.displayName = 'SubMenu';
/** @internal */
@@ -70,7 +71,7 @@ const getStyles = (theme: GrafanaTheme2) => {
`,
icon: css`
opacity: 0.7;
- margin-left: 10px;
+ margin-left: ${theme.spacing(2)};
color: ${theme.colors.text.secondary};
`,
itemsWrapper: css`