mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 01:23:32 -06: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:
parent
83c01f9711
commit
49e18a3e7a
@ -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"],
|
||||
|
@ -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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user