Menu: Improvements to menu component (#52686)

This commit is contained in:
Torkel Ödegaard 2022-07-25 16:54:58 +02:00 committed by GitHub
parent da5bc5bde7
commit feaa037a19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 210 additions and 112 deletions

View 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} />

View File

@ -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;

View 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;

View File

@ -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)};
`,
};
};

View 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),
}),
};
};

View File

@ -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};
`,
};

View File

@ -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`