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 { MenuDivider } from './MenuDivider';
|
||||
import { MenuGroup } from './MenuGroup';
|
||||
import { MenuItem } from './MenuItem';
|
||||
import { useMenuFocus } from './hooks';
|
||||
|
||||
/** @internal */
|
||||
export interface MenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** React element rendered at the top of the menu */
|
||||
header?: React.ReactNode;
|
||||
@ -18,8 +20,7 @@ export interface MenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onKeyDown?: React.KeyboardEventHandler;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const Menu = React.forwardRef<HTMLDivElement, MenuProps>(
|
||||
const MenuComp = React.forwardRef<HTMLDivElement, MenuProps>(
|
||||
({ header, children, ariaLabel, onOpen, onClose, onKeyDown, ...otherProps }, forwardedRef) => {
|
||||
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) => {
|
||||
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)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
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 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<T = any> {
|
||||
className?: string;
|
||||
/** Active */
|
||||
active?: boolean;
|
||||
|
||||
/** Show in destructive style (error color) */
|
||||
destructive?: boolean;
|
||||
tabIndex?: number;
|
||||
|
||||
/** List of menu items for the subMenu */
|
||||
childItems?: Array<ReactElement<MenuItemProps>>;
|
||||
}
|
||||
@ -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 (
|
||||
<Wrapper
|
||||
<ItemElement
|
||||
target={target}
|
||||
className={itemStyle}
|
||||
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
|
||||
@ -144,13 +147,13 @@ export const MenuItem = React.memo(
|
||||
close={closeSubMenu}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
</ItemElement>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
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};
|
||||
`,
|
||||
};
|
||||
|
@ -58,6 +58,7 @@ export const SubMenu: React.FC<SubMenuProps> = 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`
|
||||
|
Loading…
Reference in New Issue
Block a user