mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: replace react-popper
with floating-ui
in InlineToast
(#82381)
replace react-popper with floating-ui in InlineToast
This commit is contained in:
parent
5105be4eeb
commit
c0b5b32650
@ -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>
|
||||
)}
|
||||
|
@ -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)})`;
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user