mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Menu: Improvements to menu component (#52686)
This commit is contained in:
parent
da5bc5bde7
commit
feaa037a19
34
packages/grafana-ui/src/components/Menu/Menu.mdx
Normal file
34
packages/grafana-ui/src/components/Menu/Menu.mdx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Meta, Props } from '@storybook/addon-docs/blocks';
|
||||||
|
import { Overlay } from 'ol';
|
||||||
|
|
||||||
|
import { Menu } from './Menu';
|
||||||
|
|
||||||
|
<Meta title="MDX|Menu" component={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 = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item label="Google" />
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item label="Delete" icon="trash-alt" destructive />
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu}>
|
||||||
|
<Button icon="bars" />
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
<Props of={Menu} />
|
@ -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<typeof Menu> = {
|
|
||||||
title: 'General/Menu',
|
|
||||||
component: Menu,
|
|
||||||
argTypes: {},
|
|
||||||
parameters: {
|
|
||||||
knobs: {
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
controls: {
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Simple: Story = (args) => {
|
|
||||||
return (
|
|
||||||
<VerticalGroup>
|
|
||||||
<StoryExample name="Simple">
|
|
||||||
<Menu>
|
|
||||||
<MenuItem label="Google" icon="search-plus" />
|
|
||||||
<MenuItem label="Filter" icon="filter" />
|
|
||||||
<MenuItem label="History" icon="history" />
|
|
||||||
<MenuItem label="Active" icon="history" active />
|
|
||||||
<MenuItem label="Apps" icon="apps" />
|
|
||||||
</Menu>
|
|
||||||
</StoryExample>
|
|
||||||
<StoryExample name="With header & groups">
|
|
||||||
<Menu header={args.header} ariaLabel="Menu header">
|
|
||||||
<MenuGroup label="Group 1">
|
|
||||||
<MenuItem label="item1" icon="history" />
|
|
||||||
<MenuItem label="item2" icon="filter" />
|
|
||||||
</MenuGroup>
|
|
||||||
<MenuGroup label="Group 2">
|
|
||||||
<MenuItem label="item1" icon="history" />
|
|
||||||
</MenuGroup>
|
|
||||||
</Menu>
|
|
||||||
</StoryExample>
|
|
||||||
<StoryExample name="With submenu">
|
|
||||||
<Menu>
|
|
||||||
<MenuItem label="item1" icon="history" />
|
|
||||||
<MenuItem
|
|
||||||
label="item2"
|
|
||||||
icon="apps"
|
|
||||||
childItems={[
|
|
||||||
<MenuItem key="subitem1" label="subitem1" icon="history" />,
|
|
||||||
<MenuItem key="subitem2" label="subitem2" icon="apps" />,
|
|
||||||
<MenuItem
|
|
||||||
key="subitem3"
|
|
||||||
label="subitem3"
|
|
||||||
icon="search-plus"
|
|
||||||
childItems={[
|
|
||||||
<MenuItem key="subitem1" label="subitem1" icon="history" />,
|
|
||||||
<MenuItem key="subitem2" label="subitem2" icon="apps" />,
|
|
||||||
<MenuItem key="subitem3" label="subitem3" icon="search-plus" />,
|
|
||||||
]}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<MenuItem label="item3" icon="filter" />
|
|
||||||
</Menu>
|
|
||||||
</StoryExample>
|
|
||||||
</VerticalGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Simple.args = {
|
|
||||||
header: (
|
|
||||||
<GraphContextMenuHeader
|
|
||||||
timestamp="2020-11-25 19:04:25"
|
|
||||||
seriesColor="#00ff00"
|
|
||||||
displayName="A-series"
|
|
||||||
displayValue={{
|
|
||||||
text: '128',
|
|
||||||
suffix: 'km/h',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
104
packages/grafana-ui/src/components/Menu/Menu.story.tsx
Normal file
104
packages/grafana-ui/src/components/Menu/Menu.story.tsx
Normal file
@ -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<typeof Menu> = {
|
||||||
|
title: 'General/Menu',
|
||||||
|
component: Menu,
|
||||||
|
argTypes: {},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
page: mdx,
|
||||||
|
},
|
||||||
|
knobs: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Examples() {
|
||||||
|
return (
|
||||||
|
<VerticalGroup>
|
||||||
|
<StoryExample name="Plain">
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item label="Google" />
|
||||||
|
<Menu.Item label="Filter" />
|
||||||
|
<Menu.Item label="Active" active />
|
||||||
|
<Menu.Item label="I am a link" url="http://google.com" target="_blank" />
|
||||||
|
<Menu.Item label="With destructive prop set" destructive />
|
||||||
|
</Menu>
|
||||||
|
</StoryExample>
|
||||||
|
<StoryExample name="With icons and a divider">
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item label="Google" icon="search-plus" />
|
||||||
|
<Menu.Item label="Filter" icon="filter" />
|
||||||
|
<Menu.Item label="History" icon="history" />
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item label="With destructive prop set" icon="trash-alt" destructive />
|
||||||
|
</Menu>
|
||||||
|
</StoryExample>
|
||||||
|
<StoryExample name="With header & groups">
|
||||||
|
<Menu
|
||||||
|
header={
|
||||||
|
<GraphContextMenuHeader
|
||||||
|
timestamp="2020-11-25 19:04:25"
|
||||||
|
seriesColor="#00ff00"
|
||||||
|
displayName="A-series"
|
||||||
|
displayValue={{
|
||||||
|
text: '128',
|
||||||
|
suffix: 'km/h',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
ariaLabel="Menu header"
|
||||||
|
>
|
||||||
|
<Menu.Group label="Group 1">
|
||||||
|
<Menu.Item label="item1" icon="history" />
|
||||||
|
<Menu.Item label="item2" icon="filter" />
|
||||||
|
</Menu.Group>
|
||||||
|
<Menu.Group label="Group 2">
|
||||||
|
<Menu.Item label="item1" icon="history" />
|
||||||
|
</Menu.Group>
|
||||||
|
</Menu>
|
||||||
|
</StoryExample>
|
||||||
|
<StoryExample name="With submenu">
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item label="item1" icon="history" />
|
||||||
|
<Menu.Item
|
||||||
|
label="item2"
|
||||||
|
icon="apps"
|
||||||
|
childItems={[
|
||||||
|
<Menu.Item key="subitem1" label="subitem1" icon="history" />,
|
||||||
|
<Menu.Item key="subitem2" label="subitem2" icon="apps" />,
|
||||||
|
<Menu.Item
|
||||||
|
key="subitem3"
|
||||||
|
label="subitem3"
|
||||||
|
icon="search-plus"
|
||||||
|
childItems={[
|
||||||
|
<Menu.Item key="subitem1" label="subitem1" icon="history" />,
|
||||||
|
<Menu.Item key="subitem2" label="subitem2" icon="apps" />,
|
||||||
|
<Menu.Item key="subitem3" label="subitem3" icon="search-plus" />,
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Menu.Item label="item3" icon="filter" />
|
||||||
|
</Menu>
|
||||||
|
</StoryExample>
|
||||||
|
</VerticalGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta;
|
@ -5,9 +5,11 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
|
|
||||||
import { useStyles2 } from '../../themes';
|
import { useStyles2 } from '../../themes';
|
||||||
|
|
||||||
|
import { MenuDivider } from './MenuDivider';
|
||||||
|
import { MenuGroup } from './MenuGroup';
|
||||||
|
import { MenuItem } from './MenuItem';
|
||||||
import { useMenuFocus } from './hooks';
|
import { useMenuFocus } from './hooks';
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export interface MenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
export interface MenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
/** React element rendered at the top of the menu */
|
/** React element rendered at the top of the menu */
|
||||||
header?: React.ReactNode;
|
header?: React.ReactNode;
|
||||||
@ -18,8 +20,7 @@ export interface MenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
onKeyDown?: React.KeyboardEventHandler;
|
onKeyDown?: React.KeyboardEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
const MenuComp = React.forwardRef<HTMLDivElement, MenuProps>(
|
||||||
export const Menu = React.forwardRef<HTMLDivElement, MenuProps>(
|
|
||||||
({ header, children, ariaLabel, onOpen, onClose, onKeyDown, ...otherProps }, forwardedRef) => {
|
({ header, children, ariaLabel, onOpen, onClose, onKeyDown, ...otherProps }, forwardedRef) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
@ -44,9 +45,15 @@ export const Menu = React.forwardRef<HTMLDivElement, MenuProps>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
Menu.displayName = 'Menu';
|
|
||||||
|
|
||||||
/** @internal */
|
MenuComp.displayName = 'Menu';
|
||||||
|
|
||||||
|
export const Menu = Object.assign(MenuComp, {
|
||||||
|
Item: MenuItem,
|
||||||
|
Divider: MenuDivider,
|
||||||
|
Group: MenuGroup,
|
||||||
|
});
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
header: css`
|
header: css`
|
||||||
@ -58,6 +65,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
box-shadow: ${theme.shadows.z3};
|
box-shadow: ${theme.shadows.z3};
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: ${theme.shape.borderRadius()};
|
border-radius: ${theme.shape.borderRadius()};
|
||||||
|
padding: ${theme.spacing(0.5, 0)};
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
21
packages/grafana-ui/src/components/Menu/MenuDivider.tsx
Normal file
21
packages/grafana-ui/src/components/Menu/MenuDivider.tsx
Normal file
@ -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 <div className={styles.divider} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
divider: css({
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: theme.colors.border.weak,
|
||||||
|
margin: theme.spacing(0.5, 0),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
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';
|
import { GrafanaTheme2, LinkTarget } from '@grafana/data';
|
||||||
|
|
||||||
@ -35,9 +35,9 @@ export interface MenuItemProps<T = any> {
|
|||||||
className?: string;
|
className?: string;
|
||||||
/** Active */
|
/** Active */
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
/** Show in destructive style (error color) */
|
||||||
|
destructive?: boolean;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
|
|
||||||
/** List of menu items for the subMenu */
|
/** List of menu items for the subMenu */
|
||||||
childItems?: Array<ReactElement<MenuItemProps>>;
|
childItems?: Array<ReactElement<MenuItemProps>>;
|
||||||
}
|
}
|
||||||
@ -55,6 +55,7 @@ export const MenuItem = React.memo(
|
|||||||
onClick,
|
onClick,
|
||||||
className,
|
className,
|
||||||
active,
|
active,
|
||||||
|
destructive,
|
||||||
childItems,
|
childItems,
|
||||||
role = 'menuitem',
|
role = 'menuitem',
|
||||||
tabIndex = -1,
|
tabIndex = -1,
|
||||||
@ -71,12 +72,14 @@ export const MenuItem = React.memo(
|
|||||||
setIsSubMenuOpen(false);
|
setIsSubMenuOpen(false);
|
||||||
setIsActive(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(
|
const itemStyle = cx(
|
||||||
{
|
{
|
||||||
[styles.item]: true,
|
[styles.item]: true,
|
||||||
[styles.activeItem]: isActive,
|
[styles.active]: isActive,
|
||||||
|
[styles.destructive]: destructive,
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
);
|
);
|
||||||
@ -107,7 +110,7 @@ export const MenuItem = React.memo(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<ItemElement
|
||||||
target={target}
|
target={target}
|
||||||
className={itemStyle}
|
className={itemStyle}
|
||||||
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
|
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
|
||||||
@ -144,13 +147,13 @@ export const MenuItem = React.memo(
|
|||||||
close={closeSubMenu}
|
close={closeSubMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</ItemElement>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
MenuItem.displayName = 'MenuItem';
|
MenuItem.displayName = 'MenuItem';
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
item: css`
|
item: css`
|
||||||
@ -159,7 +162,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: ${theme.colors.text.primary};
|
color: ${theme.colors.text.primary};
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 5px 12px 5px 10px;
|
align-items: center;
|
||||||
|
padding: ${theme.spacing(0.5, 2)};
|
||||||
|
min-height: ${theme.spacing(4)};
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -177,12 +182,31 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
${getFocusStyles(theme)}
|
${getFocusStyles(theme)}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
activeItem: css`
|
active: css`
|
||||||
background: ${theme.colors.action.selected};
|
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`
|
icon: css`
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
margin-left: -4px;
|
||||||
color: ${theme.colors.text.secondary};
|
color: ${theme.colors.text.secondary};
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
@ -58,6 +58,7 @@ export const SubMenu: React.FC<SubMenuProps> = React.memo(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
SubMenu.displayName = 'SubMenu';
|
SubMenu.displayName = 'SubMenu';
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@ -70,7 +71,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
`,
|
`,
|
||||||
icon: css`
|
icon: css`
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin-left: 10px;
|
margin-left: ${theme.spacing(2)};
|
||||||
color: ${theme.colors.text.secondary};
|
color: ${theme.colors.text.secondary};
|
||||||
`,
|
`,
|
||||||
itemsWrapper: css`
|
itemsWrapper: css`
|
||||||
|
Loading…
Reference in New Issue
Block a user