mirror of
https://github.com/grafana/grafana.git
synced 2026-07-30 00:08:10 -05:00
Chore: replace react-popper with floating-ui in Popover (#82922)
* replace react-popper with floating-ui in Popover * update HoverCard * fix unit tests * mock useTransitionStyles to ensure consistent unit test results
This commit is contained in:
@@ -1662,13 +1662,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx:5381": [
|
"public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/HoverCard.tsx:5381": [
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "4"]
|
|
||||||
],
|
|
||||||
"public/app/features/alerting/unified/components/Label.tsx:5381": [
|
"public/app/features/alerting/unified/components/Label.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||||
|
|||||||
@@ -92,23 +92,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
color: theme.colors.text.primary,
|
color: theme.colors.text.primary,
|
||||||
maxWidth: '400px',
|
maxWidth: '400px',
|
||||||
fontSize: theme.typography.size.sm,
|
fontSize: theme.typography.size.sm,
|
||||||
// !important because these styles are also provided to popper via .popper classes from Tooltip component
|
|
||||||
// hope to get rid of those soon
|
|
||||||
padding: '15px !important',
|
|
||||||
'& [data-placement^="top"]': {
|
|
||||||
paddingLeft: '0 !important',
|
|
||||||
paddingRight: '0 !important',
|
|
||||||
},
|
|
||||||
'& [data-placement^="bottom"]': {
|
|
||||||
paddingLeft: '0 !important',
|
|
||||||
paddingRight: '0 !important',
|
|
||||||
},
|
|
||||||
'& [data-placement^="left"]': {
|
|
||||||
paddingTop: '0 !important',
|
|
||||||
},
|
|
||||||
'& [data-placement^="right"]': {
|
|
||||||
paddingTop: '0 !important',
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
backgroundColor: theme.colors.background.primary,
|
backgroundColor: theme.colors.background.primary,
|
||||||
border: `1px solid ${theme.colors.border.weak}`,
|
border: `1px solid ${theme.colors.border.weak}`,
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
margin: theme.spacing(1, 0),
|
|
||||||
boxShadow: theme.shadows.z3,
|
boxShadow: theme.shadows.z3,
|
||||||
borderRadius: theme.shape.radius.default,
|
borderRadius: theme.shape.radius.default,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame } f
|
|||||||
import { Table } from './Table';
|
import { Table } from './Table';
|
||||||
import { Props } from './types';
|
import { Props } from './types';
|
||||||
|
|
||||||
|
// mock transition styles to ensure consistent behaviour in unit tests
|
||||||
|
jest.mock('@floating-ui/react', () => ({
|
||||||
|
...jest.requireActual('@floating-ui/react'),
|
||||||
|
useTransitionStyles: () => ({
|
||||||
|
styles: {},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
function getDefaultDataFrame(): DataFrame {
|
function getDefaultDataFrame(): DataFrame {
|
||||||
const dataFrame = toDataFrame({
|
const dataFrame = toDataFrame({
|
||||||
name: 'A',
|
name: 'A',
|
||||||
|
|||||||
@@ -1,96 +1,102 @@
|
|||||||
import { Placement, VirtualElement } from '@popperjs/core';
|
import {
|
||||||
import React, { PureComponent } from 'react';
|
FloatingArrow,
|
||||||
import { Manager, Popper as ReactPopper, PopperArrowProps } from 'react-popper';
|
arrow,
|
||||||
import Transition from 'react-transition-group/Transition';
|
autoUpdate,
|
||||||
|
flip,
|
||||||
|
offset,
|
||||||
|
shift,
|
||||||
|
useFloating,
|
||||||
|
useTransitionStyles,
|
||||||
|
} from '@floating-ui/react';
|
||||||
|
import React, { useLayoutEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { useTheme2 } from '../../themes';
|
||||||
|
import { getPlacement } from '../../utils/tooltipUtils';
|
||||||
import { Portal } from '../Portal/Portal';
|
import { Portal } from '../Portal/Portal';
|
||||||
|
|
||||||
import { PopoverContent } from './types';
|
import { PopoverContent, TooltipPlacement } from './types';
|
||||||
|
|
||||||
const defaultTransitionStyles = {
|
|
||||||
transitionProperty: 'opacity',
|
|
||||||
transitionDuration: '200ms',
|
|
||||||
transitionTimingFunction: 'linear',
|
|
||||||
opacity: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const transitionStyles: { [key: string]: object } = {
|
|
||||||
exited: { opacity: 0 },
|
|
||||||
entering: { opacity: 0 },
|
|
||||||
entered: { opacity: 1, transitionDelay: '0s' },
|
|
||||||
exiting: { opacity: 0, transitionDelay: '500ms' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RenderPopperArrowFn = (props: { arrowProps: PopperArrowProps; placement: string }) => JSX.Element;
|
|
||||||
|
|
||||||
interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'content'> {
|
interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'content'> {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
placement?: Placement;
|
placement?: TooltipPlacement;
|
||||||
content: PopoverContent;
|
content: PopoverContent;
|
||||||
referenceElement: HTMLElement | VirtualElement;
|
referenceElement: HTMLElement;
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
renderArrow?: RenderPopperArrowFn;
|
renderArrow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Popover extends PureComponent<Props> {
|
export function Popover({
|
||||||
render() {
|
content,
|
||||||
const { content, show, placement, className, wrapperClassName, renderArrow, referenceElement, ...rest } =
|
show,
|
||||||
this.props;
|
placement,
|
||||||
|
className,
|
||||||
|
wrapperClassName,
|
||||||
|
referenceElement,
|
||||||
|
renderArrow,
|
||||||
|
...rest
|
||||||
|
}: Props) {
|
||||||
|
const theme = useTheme2();
|
||||||
|
const arrowRef = useRef(null);
|
||||||
|
|
||||||
return (
|
// the order of middleware is important!
|
||||||
<Manager>
|
// `arrow` should almost always be at the end
|
||||||
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
// see https://floating-ui.com/docs/arrow#order
|
||||||
{(transitionState) => {
|
const middleware = [
|
||||||
return (
|
offset(8),
|
||||||
<Portal>
|
flip({
|
||||||
<ReactPopper
|
fallbackAxisSideDirection: 'end',
|
||||||
placement={placement}
|
// see https://floating-ui.com/docs/flip#combining-with-shift
|
||||||
referenceElement={referenceElement}
|
crossAxis: false,
|
||||||
modifiers={[
|
boundary: document.body,
|
||||||
{ name: 'preventOverflow', enabled: true, options: { rootBoundary: 'viewport' } },
|
}),
|
||||||
{
|
shift(),
|
||||||
name: 'eventListeners',
|
];
|
||||||
options: { scroll: true, resize: true },
|
|
||||||
},
|
if (renderArrow) {
|
||||||
]}
|
middleware.push(
|
||||||
>
|
arrow({
|
||||||
{({ ref, style, placement, arrowProps, update }) => {
|
element: arrowRef,
|
||||||
return (
|
})
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
...defaultTransitionStyles,
|
|
||||||
...transitionStyles[transitionState],
|
|
||||||
}}
|
|
||||||
data-placement={placement}
|
|
||||||
className={`${wrapperClassName}`}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<div className={className}>
|
|
||||||
{typeof content === 'string' && content}
|
|
||||||
{React.isValidElement(content) && React.cloneElement(content)}
|
|
||||||
{typeof content === 'function' &&
|
|
||||||
content({
|
|
||||||
updatePopperPosition: update,
|
|
||||||
})}
|
|
||||||
{renderArrow &&
|
|
||||||
renderArrow({
|
|
||||||
arrowProps,
|
|
||||||
placement,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</ReactPopper>
|
|
||||||
</Portal>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Transition>
|
|
||||||
</Manager>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export { Popover };
|
const { context, refs, floatingStyles } = useFloating({
|
||||||
|
open: show,
|
||||||
|
placement: getPlacement(placement),
|
||||||
|
middleware,
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
strategy: 'fixed',
|
||||||
|
});
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
refs.setReference(referenceElement);
|
||||||
|
}, [referenceElement, refs]);
|
||||||
|
|
||||||
|
const { styles: placementStyles } = useTransitionStyles(context, {
|
||||||
|
initial: () => ({
|
||||||
|
opacity: 0,
|
||||||
|
}),
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
});
|
||||||
|
|
||||||
|
return show ? (
|
||||||
|
<Portal>
|
||||||
|
<div
|
||||||
|
ref={refs.setFloating}
|
||||||
|
style={{
|
||||||
|
...floatingStyles,
|
||||||
|
...placementStyles,
|
||||||
|
}}
|
||||||
|
className={wrapperClassName}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<div className={className}>
|
||||||
|
{renderArrow && <FloatingArrow fill={theme.colors.border.weak} ref={arrowRef} context={context} />}
|
||||||
|
{typeof content === 'string' && content}
|
||||||
|
{React.isValidElement(content) && React.cloneElement(content)}
|
||||||
|
{typeof content === 'function' && content({})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
) : undefined;
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,17 +53,13 @@ export const HoverCard = ({
|
|||||||
<GrafanaPopover
|
<GrafanaPopover
|
||||||
{...popperProps}
|
{...popperProps}
|
||||||
{...rest}
|
{...rest}
|
||||||
wrapperClassName={classnames(styles.popover(arrow ? 1.25 : 0), wrapperClassName)}
|
wrapperClassName={classnames(styles.popover, wrapperClassName)}
|
||||||
onMouseLeave={hidePopper}
|
onMouseLeave={hidePopper}
|
||||||
onMouseEnter={showPopper}
|
onMouseEnter={showPopper}
|
||||||
onFocus={showPopper}
|
onFocus={showPopper}
|
||||||
onBlur={hidePopper}
|
onBlur={hidePopper}
|
||||||
referenceElement={popoverRef.current}
|
referenceElement={popoverRef.current}
|
||||||
renderArrow={
|
renderArrow={arrow}
|
||||||
arrow
|
|
||||||
? ({ arrowProps, placement }) => <div className={styles.arrow(placement)} {...arrowProps} />
|
|
||||||
: () => <></>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -82,55 +78,25 @@ export const HoverCard = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
popover: (offset: number) => css`
|
popover: css({
|
||||||
border-radius: ${theme.shape.radius.default};
|
borderRadius: theme.shape.radius.default,
|
||||||
box-shadow: ${theme.shadows.z3};
|
boxShadow: theme.shadows.z3,
|
||||||
background: ${theme.colors.background.primary};
|
background: theme.colors.background.primary,
|
||||||
border: 1px solid ${theme.colors.border.medium};
|
border: `1px solid ${theme.colors.border.medium}`,
|
||||||
|
}),
|
||||||
margin-bottom: ${theme.spacing(offset)};
|
|
||||||
`,
|
|
||||||
card: {
|
card: {
|
||||||
body: css`
|
body: css({
|
||||||
padding: ${theme.spacing(1)};
|
padding: theme.spacing(1),
|
||||||
`,
|
}),
|
||||||
header: css`
|
header: css({
|
||||||
padding: ${theme.spacing(1)};
|
padding: theme.spacing(1),
|
||||||
background: ${theme.colors.background.secondary};
|
background: theme.colors.background.secondary,
|
||||||
border-bottom: solid 1px ${theme.colors.border.medium};
|
borderBottom: `solid 1px ${theme.colors.border.medium}`,
|
||||||
`,
|
}),
|
||||||
footer: css`
|
footer: css({
|
||||||
padding: ${theme.spacing(0.5)} ${theme.spacing(1)};
|
padding: theme.spacing(0.5, 1),
|
||||||
background: ${theme.colors.background.secondary};
|
background: theme.colors.background.secondary,
|
||||||
border-top: solid 1px ${theme.colors.border.medium};
|
borderTop: `solid 1px ${theme.colors.border.medium}`,
|
||||||
`,
|
}),
|
||||||
},
|
|
||||||
// TODO currently only works with bottom placement
|
|
||||||
arrow: (placement: string) => {
|
|
||||||
const ARROW_SIZE = '9px';
|
|
||||||
|
|
||||||
return css`
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
|
|
||||||
border-left: ${ARROW_SIZE} solid transparent;
|
|
||||||
border-right: ${ARROW_SIZE} solid transparent;
|
|
||||||
/* using hex colors here because the border colors use alpha transparency */
|
|
||||||
border-top: ${ARROW_SIZE} solid ${theme.isLight ? '#d2d3d4' : '#2d3037'};
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
border: ${ARROW_SIZE} solid ${theme.colors.background.primary};
|
|
||||||
border-bottom: 0;
|
|
||||||
border-left-color: transparent;
|
|
||||||
border-right-color: transparent;
|
|
||||||
|
|
||||||
margin-top: 1px;
|
|
||||||
bottom: 1px;
|
|
||||||
left: -${ARROW_SIZE};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user