mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana-UI: ContextMenu docs (#28508)
* Add story * Add docs * Get rid of selectThemeVariant * Minor tweaks
This commit is contained in:
parent
60d40fa99b
commit
75ebf3f15c
@ -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} />
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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) {
|
||||||
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
Loading…
Reference in New Issue
Block a user