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
6 changed files with 117 additions and 162 deletions
-7
View File
@@ -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};
}
`;
}, },
}); });