Chore: replace react-popper with floating-ui in InlineToast (#82381)

replace react-popper with floating-ui in InlineToast
This commit is contained in:
Ashley Harrison 2024-02-15 09:44:44 +00:00 committed by GitHub
parent 5105be4eeb
commit c0b5b32650
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 63 additions and 100 deletions

View File

@ -104,17 +104,12 @@ export function AutoSaveField<T = string>(props: Props<T>) {
)}
</Field>
{fieldState.isLoading && (
<InlineToast referenceElement={fieldRef.current} placement="right" alternativePlacement="bottom">
<InlineToast referenceElement={fieldRef.current} placement="right">
Saving <EllipsisAnimated />
</InlineToast>
)}
{fieldState.showSuccess && (
<InlineToast
suffixIcon={'check'}
referenceElement={fieldRef.current}
placement="right"
alternativePlacement="bottom"
>
<InlineToast suffixIcon={'check'} referenceElement={fieldRef.current} placement="right">
Saved!
</InlineToast>
)}

View File

@ -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<HTMLElement | null>(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 (
<Portal>
<div
style={{ display: 'inline-block', ...popper.styles.popper }}
{...popper.attributes.popper}
ref={setIndicatorElement}
aria-live="polite"
>
<span className={cx(styles.root, placementStyles[toastPlacement])}>
<div style={{ display: 'inline-block', ...floatingStyles }} ref={refs.setFloating} aria-live="polite">
<span className={cx(styles.root)} style={placementStyles}>
{children && <span>{children}</span>}
{suffixIcon && <Icon name={suffixIcon} />}
</span>
@ -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<InlineToastProps['placement'], string> => {
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)})`;
}
};