From c0b5b3265092f5ed402b3bc53e8281ba7b8bbf9a Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 15 Feb 2024 09:44:44 +0000 Subject: [PATCH] Chore: replace `react-popper` with `floating-ui` in `InlineToast` (#82381) replace react-popper with floating-ui in InlineToast --- .../AutoSaveField/AutoSaveField.tsx | 9 +- .../components/InlineToast/InlineToast.tsx | 154 +++++++----------- 2 files changed, 63 insertions(+), 100 deletions(-) diff --git a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx index 9aa84af51e0..da268d61094 100644 --- a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx +++ b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx @@ -104,17 +104,12 @@ export function AutoSaveField(props: Props) { )} {fieldState.isLoading && ( - + Saving )} {fieldState.showSuccess && ( - + Saved! )} diff --git a/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx b/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx index efbda8b8390..7efde97f352 100644 --- a/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx +++ b/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx @@ -1,11 +1,10 @@ -import { css, cx, keyframes } from '@emotion/css'; -import { BasePlacement } from '@popperjs/core'; -import React, { useState } from 'react'; -import { usePopper } from 'react-popper'; +import { css, cx } from '@emotion/css'; +import { autoUpdate, flip, offset, shift, Side, useFloating, useTransitionStyles } from '@floating-ui/react'; +import React, { useLayoutEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '../../themes'; +import { useStyles2, useTheme2 } from '../../themes'; import { IconName } from '../../types'; import { Icon } from '../Icon/Icon'; import { Portal } from '../Portal/Portal'; @@ -14,39 +13,59 @@ export interface InlineToastProps { children: React.ReactNode; suffixIcon?: IconName; referenceElement: HTMLElement | null; - placement: BasePlacement; - /** Placement to use if there is not enough space to show the full toast with the original placement*/ - alternativePlacement?: BasePlacement; + placement: Side; + /** + * @deprecated + * Placement to use if there is not enough space to show the full toast with the original placement + * This is now done automatically. + */ + alternativePlacement?: Side; } -export function InlineToast({ - referenceElement, - children, - suffixIcon, - placement, - alternativePlacement, -}: InlineToastProps) { - const [indicatorElement, setIndicatorElement] = useState(null); - const [toastPlacement, setToastPlacement] = useState(placement); - const popper = usePopper(referenceElement, indicatorElement, { placement: toastPlacement }); +export function InlineToast({ referenceElement, children, suffixIcon, placement }: InlineToastProps) { const styles = useStyles2(getStyles); - const placementStyles = useStyles2(getPlacementStyles); + const theme = useTheme2(); - React.useEffect(() => { - if (alternativePlacement && shouldUseAlt(placement, indicatorElement, referenceElement)) { - setToastPlacement(alternativePlacement); - } - }, [alternativePlacement, placement, indicatorElement, referenceElement]); + // 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(), + ]; + + const { context, refs, floatingStyles } = useFloating({ + open: true, + placement, + middleware, + whileElementsMounted: autoUpdate, + strategy: 'fixed', + }); + + useLayoutEffect(() => { + refs.setReference(referenceElement); + }, [referenceElement, refs]); + + const { styles: placementStyles } = useTransitionStyles(context, { + initial: ({ side }) => { + return { + opacity: 0, + transform: getInitialTransform(side, theme), + }; + }, + duration: theme.transitions.duration.shortest, + }); return ( -
- +
+ {children && {children}} {suffixIcon && } @@ -71,68 +90,17 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; -//To calculate if the InlineToast is displayed off-screen and should use the alternative placement -const shouldUseAlt = ( - placement: BasePlacement, - indicatorElement: HTMLElement | null, - referenceElement: HTMLElement | null -) => { - const indicatorSizes = indicatorElement?.getBoundingClientRect(); - const referenceSizes = referenceElement?.getBoundingClientRect(); - if (!indicatorSizes || !referenceSizes) { - return false; - } - switch (placement) { - case 'right': - return indicatorSizes.width + referenceSizes.right > window.innerWidth; - case 'bottom': - return indicatorSizes.height + referenceSizes.bottom > window.innerHeight; - case 'left': - return referenceSizes.left - indicatorSizes.width < 0; - case 'top': - return referenceSizes.top - indicatorSizes.height < 0; - default: - return false; - } -}; - -const createAnimation = (fromX: string | number, fromY: string | number) => - keyframes({ - from: { - opacity: 0, - transform: `translate(${fromX}, ${fromY})`, - }, - - to: { - opacity: 1, - transform: 'translate(0, 0px)', - }, - }); - -const getPlacementStyles = (theme: GrafanaTheme2): Record => { +const getInitialTransform = (placement: InlineToastProps['placement'], theme: GrafanaTheme2) => { const gap = 1; - const placementTopAnimation = createAnimation(0, theme.spacing(gap)); - const placementBottomAnimation = createAnimation(0, theme.spacing(gap * -1)); - const placementLeftAnimation = createAnimation(theme.spacing(gap), 0); - const placementRightAnimation = createAnimation(theme.spacing(gap * -1), 0); - - return { - top: css({ - marginBottom: theme.spacing(gap), - animation: `${placementTopAnimation} ease-out 100ms`, - }), - bottom: css({ - marginTop: theme.spacing(gap), - animation: `${placementBottomAnimation} ease-out 100ms`, - }), - left: css({ - marginRight: theme.spacing(gap), - animation: `${placementLeftAnimation} ease-out 100ms`, - }), - right: css({ - marginLeft: theme.spacing(gap), - animation: `${placementRightAnimation} ease-out 100ms`, - }), - }; + switch (placement) { + case 'top': + return `translateY(${theme.spacing(gap)})`; + case 'bottom': + return `translateY(-${theme.spacing(gap)})`; + case 'left': + return `translateX(${theme.spacing(gap)})`; + case 'right': + return `translateX(-${theme.spacing(gap)})`; + } };