Grafana-UI: ContextMenu docs (#28508)

* Add story

* Add docs

* Get rid of selectThemeVariant

* Minor tweaks
This commit is contained in:
Alex Khomenko 2020-10-27 17:18:45 +02:00 committed by GitHub
parent 60d40fa99b
commit 75ebf3f15c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 110 additions and 83 deletions

View File

@ -0,0 +1,31 @@
import { Props } from '@storybook/addon-docs/blocks';
import { ContextMenu } from './ContextMenu';
import { WithContextMenu } from "./WithContextMenu";
# ContextMenu
A menu displaying additional options when it's not possible to show them at all times due to a space constraint.
### Usage
There are controlled and uncontrolled versions of the component available. With the controlled component (`ContextMenu`) the open/close logic needs to be handled separately. Uncontrolled component (`WithContextMenu`) handles this logic internally.
#### Controlled component
```jsx
<ContextMenu x={10} y={11} onClose={() => {}} items={[{ label: 'Test', items: [{ label: 'First' }, { label: 'Second' }] }]} />
```
#### Uncontrolled component
```jsx
<WithContextMenu getContextMenuItems={() => [{ label: 'Test', items: [{ label: 'First' }, { label: 'Second' }] }]}>
{({ openMenu }) => <IconButton name="info-circle" onClick={openMenu} />}
</WithContextMenu>
```
### Props of ContextMenu
<Props of={ContextMenu} />
### Props of WithContextMenu
<Props of={WithContextMenu} />

View File

@ -0,0 +1,31 @@
import React from 'react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { IconButton } from '../IconButton/IconButton';
import { ContextMenu } from './ContextMenu';
import { WithContextMenu } from './WithContextMenu';
import mdx from './ContextMenu.mdx';
export default {
title: 'General/ContextMenu',
component: ContextMenu,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
const menuItems = [{ label: 'Test', items: [{ label: 'First' }, { label: 'Second' }] }];
export const Basic = () => {
return <ContextMenu x={10} y={11} onClose={() => {}} items={menuItems} />;
};
export const WithState = () => {
return (
<WithContextMenu getContextMenuItems={() => menuItems}>
{({ openMenu }) => <IconButton name="info-circle" onClick={openMenu} />}
</WithContextMenu>
);
};

View File

@ -1,7 +1,7 @@
import React, { useContext, useRef, useState, useLayoutEffect } from 'react'; import React, { useRef, useState, useLayoutEffect } from 'react';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import useClickAway from 'react-use/lib/useClickAway'; import useClickAway from 'react-use/lib/useClickAway';
import { selectThemeVariant, ThemeContext } from '../../index'; import { useTheme } from '../../index';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { stylesFactory } from '../../themes/stylesFactory'; import { stylesFactory } from '../../themes/stylesFactory';
import { Portal, List } from '../index'; import { Portal, List } from '../index';
@ -22,120 +22,86 @@ export interface ContextMenuGroup {
label?: string; label?: string;
items: ContextMenuItem[]; items: ContextMenuItem[];
} }
export interface ContextMenuProps { export interface ContextMenuProps {
/** Starting horizontal position for the menu */
x: number; x: number;
/** Starting vertical position for the menu */
y: number; y: number;
/** Callback for closing the menu */
onClose: () => void; onClose: () => void;
/** List of the menu items to display */
items?: ContextMenuGroup[]; items?: ContextMenuGroup[];
/** A function that returns header element */
renderHeader?: () => React.ReactNode; renderHeader?: () => React.ReactNode;
} }
const getContextMenuStyles = stylesFactory((theme: GrafanaTheme) => { const getContextMenuStyles = stylesFactory((theme: GrafanaTheme) => {
const linkColor = selectThemeVariant( const { white, black, dark1, dark2, dark7, gray1, gray3, gray5, gray7 } = theme.palette;
{ const lightThemeStyles = {
light: theme.palette.dark2, linkColor: dark2,
dark: theme.colors.text, linkColorHover: theme.colors.link,
}, wrapperBg: gray7,
theme.type wrapperShadow: gray3,
); itemColor: black,
const linkColorHover = selectThemeVariant( groupLabelColor: gray1,
{ itemBgHover: gray5,
light: theme.colors.link, headerBg: white,
dark: theme.palette.white, headerSeparator: white,
}, };
theme.type const darkThemeStyles = {
); linkColor: theme.colors.text,
const wrapperBg = selectThemeVariant( linkColorHover: white,
{ wrapperBg: dark2,
light: theme.palette.gray7, wrapperShadow: black,
dark: theme.palette.dark2, itemColor: white,
}, groupLabelColor: theme.colors.textWeak,
theme.type itemBgHover: dark7,
); headerBg: dark1,
const wrapperShadow = selectThemeVariant( headerSeparator: dark7,
{ };
light: theme.palette.gray3,
dark: theme.palette.black,
},
theme.type
);
const itemColor = selectThemeVariant(
{
light: theme.palette.black,
dark: theme.palette.white,
},
theme.type
);
const groupLabelColor = selectThemeVariant( const styles = theme.isDark ? darkThemeStyles : lightThemeStyles;
{
light: theme.palette.gray1,
dark: theme.colors.textWeak,
},
theme.type
);
const itemBgHover = selectThemeVariant(
{
light: theme.palette.gray5,
dark: theme.palette.dark7,
},
theme.type
);
const headerBg = selectThemeVariant(
{
light: theme.palette.white,
dark: theme.palette.dark1,
},
theme.type
);
const headerSeparator = selectThemeVariant(
{
light: theme.palette.white,
dark: theme.palette.dark7,
},
theme.type
);
return { return {
header: css` header: css`
padding: 4px; padding: 4px;
border-bottom: 1px solid ${headerSeparator}; border-bottom: 1px solid ${styles.headerSeparator};
background: ${headerBg}; background: ${styles.headerBg};
margin-bottom: ${theme.spacing.xs}; margin-bottom: ${theme.spacing.xs};
border-radius: ${theme.border.radius.sm} ${theme.border.radius.sm} 0 0; border-radius: ${theme.border.radius.sm} ${theme.border.radius.sm} 0 0;
`, `,
wrapper: css` wrapper: css`
background: ${wrapperBg}; background: ${styles.wrapperBg};
z-index: 1; z-index: 1;
box-shadow: 0 2px 5px 0 ${wrapperShadow}; box-shadow: 0 2px 5px 0 ${styles.wrapperShadow};
min-width: 200px; min-width: 200px;
display: inline-block; display: inline-block;
border-radius: ${theme.border.radius.sm}; border-radius: ${theme.border.radius.sm};
`, `,
link: css` link: css`
color: ${linkColor}; color: ${styles.linkColor};
display: flex; display: flex;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: ${linkColorHover}; color: ${styles.linkColorHover};
text-decoration: none; text-decoration: none;
} }
`, `,
item: css` item: css`
background: none; background: none;
padding: 4px 8px; padding: 4px 8px;
color: ${itemColor}; color: ${styles.itemColor};
border-left: 2px solid transparent; border-left: 2px solid transparent;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: ${itemBgHover}; background: ${styles.itemBgHover};
border-image: linear-gradient(#f05a28 30%, #fbca0a 99%); border-image: linear-gradient(#f05a28 30%, #fbca0a 99%);
border-image-slice: 1; border-image-slice: 1;
} }
`, `,
groupLabel: css` groupLabel: css`
color: ${groupLabelColor}; color: ${styles.groupLabelColor};
font-size: ${theme.typography.size.sm}; font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.lineHeight.md}; line-height: ${theme.typography.lineHeight.md};
padding: ${theme.spacing.xs} ${theme.spacing.sm}; padding: ${theme.spacing.xs} ${theme.spacing.sm};
@ -149,7 +115,7 @@ const getContextMenuStyles = stylesFactory((theme: GrafanaTheme) => {
}); });
export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClose, items, renderHeader }) => { export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClose, items, renderHeader }) => {
const theme = useContext(ThemeContext); const theme = useTheme();
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const [positionStyles, setPositionStyles] = useState({}); const [positionStyles, setPositionStyles] = useState({});
@ -186,11 +152,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClo
<List <List
items={items || []} items={items || []}
renderItem={(item, index) => { renderItem={(item, index) => {
return ( return <ContextMenuGroupComponent group={item} onClick={onClose} />;
<>
<ContextMenuGroupComponent group={item} onClick={onClose} />
</>
);
}} }}
/> />
</div> </div>
@ -209,7 +171,7 @@ interface ContextMenuItemProps {
const ContextMenuItemComponent: React.FC<ContextMenuItemProps> = React.memo( const ContextMenuItemComponent: React.FC<ContextMenuItemProps> = React.memo(
({ url, icon, label, target, onClick, className }) => { ({ url, icon, label, target, onClick, className }) => {
const theme = useContext(ThemeContext); const theme = useTheme();
const styles = getContextMenuStyles(theme); const styles = getContextMenuStyles(theme);
return ( return (
<div className={styles.item}> <div className={styles.item}>
@ -236,7 +198,7 @@ interface ContextMenuGroupProps {
} }
const ContextMenuGroupComponent: React.FC<ContextMenuGroupProps> = ({ group, onClick }) => { const ContextMenuGroupComponent: React.FC<ContextMenuGroupProps> = ({ group, onClick }) => {
const theme = useContext(ThemeContext); const theme = useTheme();
const styles = getContextMenuStyles(theme); const styles = getContextMenuStyles(theme);
if (group.items.length === 0) { if (group.items.length === 0) {

View File

@ -2,7 +2,9 @@ import React, { useState } from 'react';
import { ContextMenu, ContextMenuGroup } from '../ContextMenu/ContextMenu'; import { ContextMenu, ContextMenuGroup } from '../ContextMenu/ContextMenu';
interface WithContextMenuProps { interface WithContextMenuProps {
/** Menu item trigger that accepts openMenu prop */
children: (props: { openMenu: React.MouseEventHandler<HTMLElement> }) => JSX.Element; children: (props: { openMenu: React.MouseEventHandler<HTMLElement> }) => JSX.Element;
/** A function that returns an array of menu items */
getContextMenuItems: () => ContextMenuGroup[]; getContextMenuItems: () => ContextMenuGroup[];
} }

View File

@ -104,6 +104,7 @@ export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
export * from './SingleStatShared/index'; export * from './SingleStatShared/index';
export { CallToActionCard } from './CallToActionCard/CallToActionCard'; export { CallToActionCard } from './CallToActionCard/CallToActionCard';
export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu'; export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu';
export { WithContextMenu } from './ContextMenu/WithContextMenu';
export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor'; export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor';
export { DataLinkInput } from './DataLinks/DataLinkInput'; export { DataLinkInput } from './DataLinks/DataLinkInput';
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu'; export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';