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:
Ashley Harrison 2024-02-23 15:00:24 +00:00 committed by GitHub
parent 83c01f9711
commit 49e18a3e7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 117 additions and 162 deletions

View File

@ -1662,13 +1662,6 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx:5381": [
[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": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],

View File

@ -92,23 +92,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
color: theme.colors.text.primary,
maxWidth: '400px',
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',
},
}),
};
});

View File

@ -108,7 +108,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
backgroundColor: theme.colors.background.primary,
border: `1px solid ${theme.colors.border.weak}`,
padding: theme.spacing(2),
margin: theme.spacing(1, 0),
boxShadow: theme.shadows.z3,
borderRadius: theme.shape.radius.default,
}),

View File

@ -7,6 +7,14 @@ import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame } f
import { Table } from './Table';
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 {
const dataFrame = toDataFrame({
name: 'A',

View File

@ -1,96 +1,102 @@
import { Placement, VirtualElement } from '@popperjs/core';
import React, { PureComponent } from 'react';
import { Manager, Popper as ReactPopper, PopperArrowProps } from 'react-popper';
import Transition from 'react-transition-group/Transition';
import {
FloatingArrow,
arrow,
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 { PopoverContent } 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;
import { PopoverContent, TooltipPlacement } from './types';
interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'content'> {
show: boolean;
placement?: Placement;
placement?: TooltipPlacement;
content: PopoverContent;
referenceElement: HTMLElement | VirtualElement;
referenceElement: HTMLElement;
wrapperClassName?: string;
renderArrow?: RenderPopperArrowFn;
renderArrow?: boolean;
}
class Popover extends PureComponent<Props> {
render() {
const { content, show, placement, className, wrapperClassName, renderArrow, referenceElement, ...rest } =
this.props;
export function Popover({
content,
show,
placement,
className,
wrapperClassName,
referenceElement,
renderArrow,
...rest
}: Props) {
const theme = useTheme2();
const arrowRef = useRef(null);
return (
<Manager>
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
{(transitionState) => {
return (
<Portal>
<ReactPopper
placement={placement}
referenceElement={referenceElement}
modifiers={[
{ name: 'preventOverflow', enabled: true, options: { rootBoundary: 'viewport' } },
{
name: 'eventListeners',
options: { scroll: true, resize: true },
},
]}
>
{({ ref, style, placement, arrowProps, update }) => {
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>
// the order of middleware is important!
// `arrow` should almost always be at the end
// see https://floating-ui.com/docs/arrow#order
const middleware = [
offset(8),
flip({
fallbackAxisSideDirection: 'end',
// see https://floating-ui.com/docs/flip#combining-with-shift
crossAxis: false,
boundary: document.body,
}),
shift(),
];
if (renderArrow) {
middleware.push(
arrow({
element: arrowRef,
})
);
}
}
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;
}

View File

@ -53,17 +53,13 @@ export const HoverCard = ({
<GrafanaPopover
{...popperProps}
{...rest}
wrapperClassName={classnames(styles.popover(arrow ? 1.25 : 0), wrapperClassName)}
wrapperClassName={classnames(styles.popover, wrapperClassName)}
onMouseLeave={hidePopper}
onMouseEnter={showPopper}
onFocus={showPopper}
onBlur={hidePopper}
referenceElement={popoverRef.current}
renderArrow={
arrow
? ({ arrowProps, placement }) => <div className={styles.arrow(placement)} {...arrowProps} />
: () => <></>
}
renderArrow={arrow}
/>
)}
@ -82,55 +78,25 @@ export const HoverCard = ({
};
const getStyles = (theme: GrafanaTheme2) => ({
popover: (offset: number) => css`
border-radius: ${theme.shape.radius.default};
box-shadow: ${theme.shadows.z3};
background: ${theme.colors.background.primary};
border: 1px solid ${theme.colors.border.medium};
margin-bottom: ${theme.spacing(offset)};
`,
popover: css({
borderRadius: theme.shape.radius.default,
boxShadow: theme.shadows.z3,
background: theme.colors.background.primary,
border: `1px solid ${theme.colors.border.medium}`,
}),
card: {
body: css`
padding: ${theme.spacing(1)};
`,
header: css`
padding: ${theme.spacing(1)};
background: ${theme.colors.background.secondary};
border-bottom: solid 1px ${theme.colors.border.medium};
`,
footer: css`
padding: ${theme.spacing(0.5)} ${theme.spacing(1)};
background: ${theme.colors.background.secondary};
border-top: 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};
}
`;
body: css({
padding: theme.spacing(1),
}),
header: css({
padding: theme.spacing(1),
background: theme.colors.background.secondary,
borderBottom: `solid 1px ${theme.colors.border.medium}`,
}),
footer: css({
padding: theme.spacing(0.5, 1),
background: theme.colors.background.secondary,
borderTop: `solid 1px ${theme.colors.border.medium}`,
}),
},
});