mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GrafanaUI: Implement new component Toggletip (#64459)
This commit is contained in:
parent
0b1ad0a879
commit
e8c131eb6f
81
packages/grafana-ui/src/components/Toggletip/Toggletip.mdx
Normal file
81
packages/grafana-ui/src/components/Toggletip/Toggletip.mdx
Normal file
@ -0,0 +1,81 @@
|
||||
import { Props } from '@storybook/addon-docs/blocks';
|
||||
import { Toggletip } from './Toggletip';
|
||||
|
||||
# Toggletip
|
||||
|
||||
Toggletips, similar to Tooltips, provide contextual support for users when needed. They are hidden by default, a UI trigger or text link are clicked to set them to their visible state.
|
||||
Toggletips, unlike tooltips, are persistent until a user takes action to dismiss them by clicking on the required “X” (close) trigger.
|
||||
Toggletips are capable of containing varying types of complex content including interactive components, buttons, and dropdowns.
|
||||
|
||||
## When to use
|
||||
|
||||
- Users need further context to understand or learn a topic
|
||||
- Links to supporting documentation or content are needed to provide
|
||||
- When an interactive element must be placed within the popover
|
||||
- When content needs to persist for consumption until the user dismisses it
|
||||
|
||||
## When not to use
|
||||
|
||||
- When only a primary label or an auxiliary clarification is needed to be displayed (see: Tooltip)
|
||||
- Do not house information critical to user’s task completion
|
||||
- Do not request required information from a user to complete a task or workflow
|
||||
|
||||
## Content
|
||||
|
||||
Toggletips are able to house various types of content. Below is a potential list:
|
||||
|
||||
- Buttons
|
||||
- Text links
|
||||
- Dropdowns
|
||||
- Selects
|
||||
- Images/gifs/videos
|
||||
- Various combinations of elements — ex: strings of text with buttons and an image
|
||||
|
||||
## Theme
|
||||
|
||||
There are currently 2 themes available for the Toggletip.
|
||||
|
||||
- Info
|
||||
- Error
|
||||
|
||||
### Info
|
||||
|
||||
This is the default theme, usually used in forms to show more information.
|
||||
|
||||
### Error
|
||||
|
||||
Tooltip with a red background.
|
||||
|
||||
## Triggers
|
||||
|
||||
- Toggletips display on
|
||||
- user click of UI trigger
|
||||
- pressing ENTER or SPACE on a keyboard while the trigger element has focus
|
||||
- Toggletips dismiss by:
|
||||
- user click of close icon (x) — optional
|
||||
- clicking outside of the popover container
|
||||
- pressing the ESC key
|
||||
|
||||
### Usage
|
||||
|
||||
```tsx
|
||||
function onClose() {
|
||||
// code to execute when the toggletip is closed
|
||||
}
|
||||
|
||||
return (
|
||||
<Toogletip
|
||||
content="Toggletip body"
|
||||
title="This is the title of the Toggletip"
|
||||
footer="Toggletip footer text"
|
||||
closeButton={true}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Button type="button">
|
||||
<Icon name="question-circle" />
|
||||
</Button>
|
||||
</Toogletip>
|
||||
);
|
||||
```
|
||||
|
||||
<Props of={Toggletip} />
|
178
packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx
Normal file
178
packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { Button } from '../Button';
|
||||
import { ButtonSelect } from '../Dropdown/ButtonSelect';
|
||||
import { InlineField } from '../Forms/InlineField';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Select } from '../Select/Select';
|
||||
import mdx from '../Toggletip/Toggletip.mdx';
|
||||
|
||||
import { Toggletip } from './Toggletip';
|
||||
|
||||
const meta: ComponentMeta<typeof Toggletip> = {
|
||||
title: 'Overlays/Toggletip',
|
||||
component: Toggletip,
|
||||
decorators: [withCenteredStory],
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
controls: {
|
||||
exclude: ['onClose', 'children'],
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
title: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
content: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
},
|
||||
closeButton: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Basic: ComponentStory<typeof Toggletip> = ({
|
||||
title,
|
||||
content,
|
||||
footer,
|
||||
theme,
|
||||
closeButton,
|
||||
placement,
|
||||
...args
|
||||
}) => {
|
||||
return (
|
||||
<Toggletip
|
||||
title={title}
|
||||
content={content}
|
||||
footer={footer}
|
||||
theme={theme}
|
||||
closeButton={closeButton}
|
||||
placement={placement}
|
||||
{...args}
|
||||
>
|
||||
<Button>Click to show Toggletip with header and footer!</Button>
|
||||
</Toggletip>
|
||||
);
|
||||
};
|
||||
Basic.args = {
|
||||
title: 'Title of the Toggletip',
|
||||
content: 'This is the content of the Toggletip',
|
||||
footer: 'Footer of the Toggletip',
|
||||
placement: 'auto',
|
||||
closeButton: true,
|
||||
theme: 'info',
|
||||
};
|
||||
|
||||
export const HostingMultiElements: ComponentStory<typeof Toggletip> = ({ theme, closeButton, placement }) => {
|
||||
const selectOptions: Array<SelectableValue<number>> = [
|
||||
{ label: 'Sharilyn Markowitz', value: 1 },
|
||||
{ label: 'Naomi Striplin', value: 2 },
|
||||
{ label: 'Beau Bevel', value: 3 },
|
||||
{ label: 'Garrett Starkes', value: 4 },
|
||||
];
|
||||
const dropdownOptions: Array<SelectableValue<string>> = [
|
||||
{ label: 'Option A', value: 'a' },
|
||||
{ label: 'Option B', value: 'b' },
|
||||
{ label: 'Option C', value: 'c' },
|
||||
];
|
||||
const header = (
|
||||
<div>
|
||||
<Icon name="apps" />
|
||||
<strong>Header title with icon</strong>
|
||||
</div>
|
||||
);
|
||||
const body = (
|
||||
<div>
|
||||
<InlineField label="Users" labelWidth={15}>
|
||||
<Select width={20} options={selectOptions} value={2} onChange={() => {}} />
|
||||
</InlineField>
|
||||
<InlineField label="Job Title" labelWidth={15}>
|
||||
<Input value={'Professor'} width={20} />
|
||||
</InlineField>
|
||||
<InlineField label="My Button Select" labelWidth={15}>
|
||||
<ButtonSelect
|
||||
options={dropdownOptions}
|
||||
value={dropdownOptions[2]}
|
||||
variant={'primary'}
|
||||
onChange={() => {}}
|
||||
style={{ width: '160px' }}
|
||||
></ButtonSelect>
|
||||
</InlineField>
|
||||
<div>
|
||||
<br />
|
||||
<span>Wants to know more?</span>
|
||||
<a href="https://grafana.com/" target="_blank" rel="noreferrer">
|
||||
<Icon name="link" />
|
||||
Click here!
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const footer = (
|
||||
<div>
|
||||
<Button type="button" variant="success" onClick={() => alert('Click on Save!')}>
|
||||
Save on footer
|
||||
</Button>
|
||||
|
||||
<Button type="button" variant="destructive" onClick={() => alert('Click on Delete!')}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Toggletip
|
||||
title={header}
|
||||
content={body}
|
||||
footer={footer}
|
||||
theme={theme}
|
||||
closeButton={closeButton}
|
||||
placement={placement}
|
||||
>
|
||||
<Button type="button">Click to show Toggletip hosting multiple components!</Button>
|
||||
</Toggletip>
|
||||
);
|
||||
};
|
||||
|
||||
HostingMultiElements.parameters = {
|
||||
controls: {
|
||||
hideNoControlsWarning: true,
|
||||
exclude: ['title', 'content', 'footer', 'onClose', 'children'],
|
||||
},
|
||||
};
|
||||
HostingMultiElements.args = {
|
||||
placement: 'top',
|
||||
closeButton: true,
|
||||
theme: 'info',
|
||||
};
|
||||
|
||||
export default meta;
|
@ -0,0 +1,90 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '../Button';
|
||||
|
||||
import { Toggletip } from './Toggletip';
|
||||
|
||||
describe('Toggletip', () => {
|
||||
it('should display toogletip after click on "Click me!" button', async () => {
|
||||
render(
|
||||
<Toggletip placement="auto" content="Tooltip text">
|
||||
<Button type="button" data-testid="myButton">
|
||||
Click me!
|
||||
</Button>
|
||||
</Toggletip>
|
||||
);
|
||||
expect(screen.getByText('Click me!')).toBeInTheDocument();
|
||||
const button = screen.getByTestId('myButton');
|
||||
button.click();
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('toggletip-content')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should close toogletip after click on close button', async () => {
|
||||
const closeSpy = jest.fn();
|
||||
render(
|
||||
<Toggletip placement="auto" content="Tooltip text" onClose={closeSpy}>
|
||||
<Button type="button" data-testid="myButton">
|
||||
Click me!
|
||||
</Button>
|
||||
</Toggletip>
|
||||
);
|
||||
const button = screen.getByTestId('myButton');
|
||||
button.click();
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('toggletip-content')).toBeInTheDocument());
|
||||
|
||||
const closeButton = screen.getByTestId('toggletip-header-close');
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
closeButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should close toogletip after press ESC', async () => {
|
||||
const closeSpy = jest.fn();
|
||||
render(
|
||||
<Toggletip placement="auto" content="Tooltip text" onClose={closeSpy}>
|
||||
<Button type="button" data-testid="myButton">
|
||||
Click me!
|
||||
</Button>
|
||||
</Toggletip>
|
||||
);
|
||||
const button = screen.getByTestId('myButton');
|
||||
button.click();
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('toggletip-content')).toBeInTheDocument());
|
||||
|
||||
fireEvent.keyDown(global.document, {
|
||||
code: 'Escape',
|
||||
key: 'Escape',
|
||||
keyCode: 27,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(closeSpy).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should display the toogletip after press ENTER', async () => {
|
||||
const closeSpy = jest.fn();
|
||||
render(
|
||||
<Toggletip placement="auto" content="Tooltip text" onClose={closeSpy}>
|
||||
<Button type="button" data-testid="myButton">
|
||||
Click me!
|
||||
</Button>
|
||||
</Toggletip>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('toggletip-content')).not.toBeInTheDocument();
|
||||
|
||||
// open toggletip with enter
|
||||
const button = screen.getByTestId('myButton');
|
||||
button.focus();
|
||||
userEvent.keyboard('{enter}');
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('toggletip-content')).toBeInTheDocument());
|
||||
});
|
||||
});
|
144
packages/grafana-ui/src/components/Toggletip/Toggletip.tsx
Normal file
144
packages/grafana-ui/src/components/Toggletip/Toggletip.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import { Placement } from '@popperjs/core';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { buildTooltipTheme } from '../../utils/tooltipUtils';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
|
||||
import { ToggletipContent } from './types';
|
||||
|
||||
export interface ToggletipProps {
|
||||
/** The theme used to display the toggletip */
|
||||
theme?: 'info' | 'error';
|
||||
/** The title to be displayed on the header */
|
||||
title?: JSX.Element | string;
|
||||
/** determine whether to show or not the close button **/
|
||||
closeButton?: boolean;
|
||||
/** Callback function to be called when the toggletip is closed */
|
||||
onClose?: Function;
|
||||
/** The preferred placement of the toggletip */
|
||||
placement?: Placement;
|
||||
/** The text or component that houses the content of the toggleltip */
|
||||
content: ToggletipContent;
|
||||
/** The text or component to be displayed on the toggletip's bottom */
|
||||
footer?: JSX.Element | string;
|
||||
/** The UI control users interact with to display toggletips */
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const Toggletip = React.memo(
|
||||
({
|
||||
children,
|
||||
theme = 'info',
|
||||
placement = 'auto',
|
||||
content,
|
||||
title,
|
||||
closeButton = true,
|
||||
onClose,
|
||||
footer,
|
||||
}: ToggletipProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const style = styles[theme];
|
||||
const contentRef = useRef(null);
|
||||
const [controlledVisible, setControlledVisible] = React.useState(false);
|
||||
|
||||
const closeToggletip = useCallback(() => {
|
||||
setControlledVisible(false);
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (controlledVisible) {
|
||||
const handleKeyDown = (enterKey: KeyboardEvent) => {
|
||||
if (enterKey.key === 'Escape') {
|
||||
closeToggletip();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [controlledVisible, closeToggletip]);
|
||||
|
||||
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible, update } = usePopperTooltip({
|
||||
visible: controlledVisible,
|
||||
placement: placement,
|
||||
interactive: true,
|
||||
offset: [0, 8],
|
||||
trigger: 'click',
|
||||
onVisibleChange: (value: boolean) => {
|
||||
setControlledVisible(value);
|
||||
if (!value) {
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, {
|
||||
ref: setTriggerRef,
|
||||
tabIndex: 0,
|
||||
})}
|
||||
{visible && (
|
||||
<Portal>
|
||||
<div
|
||||
data-testid="toggletip-content"
|
||||
ref={setTooltipRef}
|
||||
{...getTooltipProps({ className: style.container })}
|
||||
>
|
||||
{Boolean(title) && <div className={style.header}>{title}</div>}
|
||||
{closeButton && (
|
||||
<div className={style.headerClose}>
|
||||
<IconButton
|
||||
aria-label="Close Toggletip"
|
||||
name="times"
|
||||
size="md"
|
||||
data-testid="toggletip-header-close"
|
||||
onClick={closeToggletip}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div ref={contentRef} {...getArrowProps({ className: style.arrow })} />
|
||||
<div className={style.body}>
|
||||
{(typeof content === 'string' || React.isValidElement(content)) && content}
|
||||
{typeof content === 'function' && update && content({ update })}
|
||||
</div>
|
||||
{Boolean(footer) && <div className={style.footer}>{footer}</div>}
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Toggletip.displayName = 'Toggletip';
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
const info = buildTooltipTheme(
|
||||
theme,
|
||||
theme.components.tooltip.background,
|
||||
theme.components.tooltip.background,
|
||||
theme.components.tooltip.text,
|
||||
{ topBottom: 3, rightLeft: 3 }
|
||||
);
|
||||
const error = buildTooltipTheme(
|
||||
theme,
|
||||
theme.colors.error.main,
|
||||
theme.colors.error.main,
|
||||
theme.colors.error.contrastText,
|
||||
{ topBottom: 3, rightLeft: 3 }
|
||||
);
|
||||
|
||||
return {
|
||||
info,
|
||||
error,
|
||||
};
|
||||
};
|
2
packages/grafana-ui/src/components/Toggletip/index.ts
Normal file
2
packages/grafana-ui/src/components/Toggletip/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Toggletip, type ToggletipProps } from './Toggletip';
|
||||
export type { ToggletipContent, ToggletipContentProps } from './types';
|
9
packages/grafana-ui/src/components/Toggletip/types.ts
Normal file
9
packages/grafana-ui/src/components/Toggletip/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* This API allows popovers to update Popper's position when e.g. popover content changes
|
||||
* update is delivered to content by react-popper.
|
||||
*/
|
||||
export interface ToggletipContentProps {
|
||||
update?: () => void;
|
||||
}
|
||||
|
||||
export type ToggletipContent = string | React.ReactElement | ((props: ToggletipContentProps) => JSX.Element);
|
@ -1,10 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
|
||||
import { colorManipulator, GrafanaTheme2 } from '@grafana/data';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { buildTooltipTheme } from '../../utils/tooltipUtils';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
|
||||
import { PopoverContent, TooltipPlacement } from './types';
|
||||
@ -52,7 +52,7 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme
|
||||
});
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const containerStyle = styles[theme ?? 'info'];
|
||||
const style = styles[theme ?? 'info'];
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -62,8 +62,8 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme
|
||||
})}
|
||||
{visible && (
|
||||
<Portal>
|
||||
<div ref={setTooltipRef} {...getTooltipProps({ className: containerStyle })}>
|
||||
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
|
||||
<div ref={setTooltipRef} {...getTooltipProps({ className: style.container })}>
|
||||
<div {...getArrowProps({ className: style.arrow })} />
|
||||
{typeof content === 'string' && content}
|
||||
{React.isValidElement(content) && React.cloneElement(content)}
|
||||
{typeof content === 'function' &&
|
||||
@ -80,154 +80,25 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme
|
||||
|
||||
Tooltip.displayName = 'Tooltip';
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
function buildTooltipTheme(tooltipBg: string, tooltipBorder: string, tooltipText: string) {
|
||||
return css`
|
||||
background-color: ${tooltipBg};
|
||||
border-radius: 3px;
|
||||
border: 1px solid ${tooltipBorder};
|
||||
box-shadow: ${theme.shadows.z2};
|
||||
color: ${tooltipText};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
padding: ${theme.spacing(0.5, 1)};
|
||||
transition: opacity 0.3s;
|
||||
z-index: ${theme.zIndex.tooltip};
|
||||
max-width: 400px;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
&[data-popper-interactive='false'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
height: 1rem;
|
||||
position: absolute;
|
||||
width: 1rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-arrow::before {
|
||||
border-style: solid;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: auto;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.tooltip-arrow::after {
|
||||
border-style: solid;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='bottom'] .tooltip-arrow {
|
||||
left: 0;
|
||||
margin-top: -10px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='bottom'] .tooltip-arrow::before {
|
||||
border-color: transparent transparent ${tooltipBorder} transparent;
|
||||
border-width: 0 8px 7px 8px;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='bottom'] .tooltip-arrow::after {
|
||||
border-color: transparent transparent ${tooltipBg} transparent;
|
||||
border-width: 0 8px 7px 8px;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='top'] .tooltip-arrow {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin-bottom: -11px;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='top'] .tooltip-arrow::before {
|
||||
border-color: ${tooltipBorder} transparent transparent transparent;
|
||||
border-width: 7px 8px 0 7px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='top'] .tooltip-arrow::after {
|
||||
border-color: ${tooltipBg} transparent transparent transparent;
|
||||
border-width: 7px 8px 0 7px;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='right'] .tooltip-arrow {
|
||||
left: 0;
|
||||
margin-left: -11px;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='right'] .tooltip-arrow::before {
|
||||
border-color: transparent ${tooltipBorder} transparent transparent;
|
||||
border-width: 7px 6px 7px 0;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='right'] .tooltip-arrow::after {
|
||||
border-color: transparent ${tooltipBg} transparent transparent;
|
||||
border-width: 6px 7px 7px 0;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='left'] .tooltip-arrow {
|
||||
margin-right: -10px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='left'] .tooltip-arrow::before {
|
||||
border-color: transparent transparent transparent ${tooltipBorder};
|
||||
border-width: 7px 0px 6px 7px;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='left'] .tooltip-arrow::after {
|
||||
border-color: transparent transparent transparent ${tooltipBg};
|
||||
border-width: 6px 0 5px 5px;
|
||||
left: 1px;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
code {
|
||||
border: none;
|
||||
display: inline;
|
||||
background: ${colorManipulator.darken(tooltipBg, 0.1)};
|
||||
color: ${tooltipText};
|
||||
}
|
||||
|
||||
pre {
|
||||
background: ${colorManipulator.darken(tooltipBg, 0.1)};
|
||||
color: ${tooltipText};
|
||||
}
|
||||
|
||||
a {
|
||||
color: ${tooltipText};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
const info = buildTooltipTheme(
|
||||
theme,
|
||||
theme.components.tooltip.background,
|
||||
theme.components.tooltip.background,
|
||||
theme.components.tooltip.text
|
||||
theme.components.tooltip.text,
|
||||
{ topBottom: 0.5, rightLeft: 1 }
|
||||
);
|
||||
const error = buildTooltipTheme(
|
||||
theme,
|
||||
theme.colors.error.main,
|
||||
theme.colors.error.main,
|
||||
theme.colors.error.contrastText,
|
||||
{ topBottom: 0.5, rightLeft: 1 }
|
||||
);
|
||||
const error = buildTooltipTheme(theme.colors.error.main, theme.colors.error.main, theme.colors.error.contrastText);
|
||||
|
||||
return {
|
||||
info: info,
|
||||
info,
|
||||
['info-alt']: info,
|
||||
error,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
167
packages/grafana-ui/src/utils/tooltipUtils.ts
Normal file
167
packages/grafana-ui/src/utils/tooltipUtils.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { colorManipulator, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
export function buildTooltipTheme(
|
||||
theme: GrafanaTheme2,
|
||||
tooltipBg: string,
|
||||
toggletipBorder: string,
|
||||
tooltipText: string,
|
||||
tooltipPadding: { topBottom: number; rightLeft: number }
|
||||
) {
|
||||
return {
|
||||
arrow: css`
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
border-style: solid;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: auto;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-style: solid;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
}
|
||||
`,
|
||||
container: css`
|
||||
background-color: ${tooltipBg};
|
||||
border-radius: 3px;
|
||||
border: 1px solid ${toggletipBorder};
|
||||
box-shadow: ${theme.shadows.z2};
|
||||
color: ${tooltipText};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
padding: ${theme.spacing(tooltipPadding.topBottom, tooltipPadding.rightLeft)};
|
||||
transition: opacity 0.3s;
|
||||
z-index: ${theme.zIndex.tooltip};
|
||||
max-width: 400px;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
&[data-popper-interactive='false'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&[data-popper-placement*='bottom'] > div[data-popper-arrow='true'] {
|
||||
left: 0;
|
||||
margin-top: -7px;
|
||||
top: 0;
|
||||
|
||||
&::before {
|
||||
border-color: transparent transparent ${toggletipBorder} transparent;
|
||||
border-width: 0 8px 7px 8px;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-color: transparent transparent ${tooltipBg} transparent;
|
||||
border-width: 0 8px 7px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-popper-placement*='top'] > div[data-popper-arrow='true'] {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin-bottom: -14px;
|
||||
|
||||
&::before {
|
||||
border-color: ${toggletipBorder} transparent transparent transparent;
|
||||
border-width: 7px 8px 0 7px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-color: ${tooltipBg} transparent transparent transparent;
|
||||
border-width: 7px 8px 0 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-popper-placement*='right'] > div[data-popper-arrow='true'] {
|
||||
left: 0;
|
||||
margin-left: -10px;
|
||||
|
||||
&::before {
|
||||
border-color: transparent ${toggletipBorder} transparent transparent;
|
||||
border-width: 7px 6px 7px 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-color: transparent ${tooltipBg} transparent transparent;
|
||||
border-width: 6px 7px 7px 0;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-popper-placement*='left'] > div[data-popper-arrow='true'] {
|
||||
margin-right: -11px;
|
||||
right: 0;
|
||||
|
||||
&::before {
|
||||
border-color: transparent transparent transparent ${toggletipBorder};
|
||||
border-width: 7px 0 6px 7px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-color: transparent transparent transparent ${tooltipBg};
|
||||
border-width: 6px 0 5px 5px;
|
||||
left: 1px;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
border: none;
|
||||
display: inline;
|
||||
background: ${colorManipulator.darken(tooltipBg, 0.1)};
|
||||
color: ${tooltipText};
|
||||
}
|
||||
|
||||
pre {
|
||||
background: ${colorManipulator.darken(tooltipBg, 0.1)};
|
||||
color: ${tooltipText};
|
||||
}
|
||||
|
||||
a {
|
||||
color: ${tooltipText};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
headerClose: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
position: absolute;
|
||||
right: ${theme.spacing(1)};
|
||||
top: ${theme.spacing(1.5)};
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
`,
|
||||
header: css`
|
||||
padding-top: ${theme.spacing(1)};
|
||||
padding-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
body: css`
|
||||
padding-top: ${theme.spacing(1)};
|
||||
padding-bottom: ${theme.spacing(1)};
|
||||
`,
|
||||
footer: css`
|
||||
padding-top: ${theme.spacing(2)};
|
||||
padding-bottom: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user