Chore: remove react-popper-tooltip in favour of @floating-ui/react (#79465)

* use floating-ui instead of react-popper-tooltip in Tooltip

* remove useTheme2 usage

* remove escape handling logic in favour of useDismiss

* don't need this useEffect anymore

* convert Toggletip to use floating-ui

* use explicit version

* convert OperationInfoButton to use Toggletip

* convert nestedFolderPicker to use floating-ui

* convert Dropdown to use floating-ui and remove react-popper-tooltip

* fix Modal/Tooltip tests

* revert to old toggletip behaviour

* revert OperationInfoButton to not use Toggletip

* add mock for requestAnimationFrame

* remove requestAnimationFrame mock

* remove fakeTimers where they're not used

* use floating-ui in ButtonSelect

* Fix filters unit tests

* only attach description if label is different

* use 'fixed' strategy for Toggletip

* use stroke and strokeWidth

* set move: false to only show the tooltip if a hover event occurs

* update type for onClose
This commit is contained in:
Ashley Harrison 2024-01-03 12:42:26 +00:00 committed by GitHub
parent 9c9a055c3e
commit 9de79fb5e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 429 additions and 392 deletions

View File

@ -241,6 +241,7 @@
"@emotion/css": "11.11.2", "@emotion/css": "11.11.2",
"@emotion/react": "11.11.1", "@emotion/react": "11.11.1",
"@fingerprintjs/fingerprintjs": "^3.4.2", "@fingerprintjs/fingerprintjs": "^3.4.2",
"@floating-ui/react": "0.26.4",
"@glideapps/glide-data-grid": "^5.2.1", "@glideapps/glide-data-grid": "^5.2.1",
"@grafana-plugins/grafana-testdata-datasource": "workspace:*", "@grafana-plugins/grafana-testdata-datasource": "workspace:*",
"@grafana-plugins/parca": "workspace:*", "@grafana-plugins/parca": "workspace:*",
@ -382,7 +383,6 @@
"react-loading-skeleton": "3.3.1", "react-loading-skeleton": "3.3.1",
"react-moveable": "0.46.1", "react-moveable": "0.46.1",
"react-popper": "2.3.0", "react-popper": "2.3.0",
"react-popper-tooltip": "4.4.2",
"react-redux": "8.1.3", "react-redux": "8.1.3",
"react-resizable": "3.0.5", "react-resizable": "3.0.5",
"react-responsive-carousel": "^3.2.23", "react-responsive-carousel": "^3.2.23",

View File

@ -49,6 +49,7 @@
"dependencies": { "dependencies": {
"@emotion/css": "11.11.2", "@emotion/css": "11.11.2",
"@emotion/react": "11.11.1", "@emotion/react": "11.11.1",
"@floating-ui/react": "0.26.4",
"@grafana/data": "10.3.0-pre", "@grafana/data": "10.3.0-pre",
"@grafana/e2e-selectors": "10.3.0-pre", "@grafana/e2e-selectors": "10.3.0-pre",
"@grafana/faro-web-sdk": "^1.3.5", "@grafana/faro-web-sdk": "^1.3.5",
@ -96,7 +97,6 @@
"react-inlinesvg": "3.0.2", "react-inlinesvg": "3.0.2",
"react-loading-skeleton": "3.3.1", "react-loading-skeleton": "3.3.1",
"react-popper": "2.3.0", "react-popper": "2.3.0",
"react-popper-tooltip": "4.4.2",
"react-router-dom": "5.3.3", "react-router-dom": "5.3.3",
"react-select": "5.7.4", "react-select": "5.7.4",
"react-table": "7.8.0", "react-table": "7.8.0",

View File

@ -151,7 +151,7 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
{isOpen && ( {isOpen && (
<div data-testid={selectors.components.TimePicker.overlayContent}> <div data-testid={selectors.components.TimePicker.overlayContent}>
<div role="presentation" className={cx(modalBackdrop, styles.backdrop)} {...underlayProps} /> <div role="presentation" className={cx(modalBackdrop, styles.backdrop)} {...underlayProps} />
<FocusScope contain autoFocus> <FocusScope contain autoFocus restoreFocus>
<section className={styles.content} ref={overlayRef} {...overlayProps} {...dialogProps}> <section className={styles.content} ref={overlayRef} {...overlayProps} {...dialogProps}>
<TimePickerContent <TimePickerContent
timeZone={timeZone} timeZone={timeZone}

View File

@ -1,14 +1,20 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useButton } from '@react-aria/button'; import {
autoUpdate,
flip,
offset,
shift,
useClick,
useDismiss,
useFloating,
useInteractions,
} from '@floating-ui/react';
import { FocusScope } from '@react-aria/focus'; import { FocusScope } from '@react-aria/focus';
import { useMenuTrigger } from '@react-aria/menu'; import React, { HTMLAttributes, useState } from 'react';
import { useMenuTriggerState } from '@react-stately/menu';
import React, { HTMLAttributes } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { useStyles2 } from '../../themes/ThemeContext'; import { useStyles2 } from '../../themes/ThemeContext';
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
import { Menu } from '../Menu/Menu'; import { Menu } from '../Menu/Menu';
import { MenuItem } from '../Menu/MenuItem'; import { MenuItem } from '../Menu/MenuItem';
import { ToolbarButton, ToolbarButtonVariant } from '../ToolbarButton'; import { ToolbarButton, ToolbarButtonVariant } from '../ToolbarButton';
@ -33,53 +39,72 @@ export interface Props<T> extends HTMLAttributes<HTMLButtonElement> {
const ButtonSelectComponent = <T,>(props: Props<T>) => { const ButtonSelectComponent = <T,>(props: Props<T>) => {
const { className, options, value, onChange, narrow, variant, ...restProps } = props; const { className, options, value, onChange, narrow, variant, ...restProps } = props;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const state = useMenuTriggerState({}); const [isOpen, setIsOpen] = useState(false);
const ref = React.useRef(null); // the order of middleware is important!
const { menuTriggerProps, menuProps } = useMenuTrigger({}, state, ref); const middleware = [
const { buttonProps } = useButton(menuTriggerProps, ref); offset(0),
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: isOpen,
placement: 'bottom-end',
onOpenChange: setIsOpen,
middleware,
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
const onChangeInternal = (item: SelectableValue<T>) => { const onChangeInternal = (item: SelectableValue<T>) => {
onChange(item); onChange(item);
state.close(); setIsOpen(false);
}; };
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<ToolbarButton <ToolbarButton
className={className} className={className}
isOpen={state.isOpen} isOpen={isOpen}
narrow={narrow} narrow={narrow}
variant={variant} variant={variant}
ref={ref} ref={refs.setReference}
{...buttonProps} {...getReferenceProps()}
{...restProps} {...restProps}
> >
{value?.label || (value?.value != null ? String(value?.value) : null)} {value?.label || (value?.value != null ? String(value?.value) : null)}
</ToolbarButton> </ToolbarButton>
{state.isOpen && ( {isOpen && (
<div className={styles.menuWrapper}> <div className={styles.menuWrapper} ref={refs.setFloating} {...getFloatingProps()} style={floatingStyles}>
<ClickOutsideWrapper onClick={state.close} parent={document} includeButtonPress={false}> <FocusScope contain autoFocus restoreFocus>
<FocusScope contain autoFocus restoreFocus> {/*
{/* tabIndex=-1 is needed here to support highlighting text within the menu when using FocusScope
tabIndex=-1 is needed here to support highlighting text within the menu when using FocusScope see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668
see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 */}
*/} <Menu tabIndex={-1} onClose={() => setIsOpen(false)}>
<Menu tabIndex={-1} onClose={state.close} {...menuProps} autoFocus={!!menuProps.autoFocus}> {options.map((item) => (
{options.map((item) => ( <MenuItem
<MenuItem key={`${item.value}`}
key={`${item.value}`} label={item.label ?? String(item.value)}
label={item.label ?? String(item.value)} onClick={() => onChangeInternal(item)}
onClick={() => onChangeInternal(item)} active={item.value === value?.value}
active={item.value === value?.value} ariaChecked={item.value === value?.value}
ariaChecked={item.value === value?.value} ariaLabel={item.ariaLabel || item.label}
ariaLabel={item.ariaLabel || item.label} role="menuitemradio"
role="menuitemradio" />
/> ))}
))} </Menu>
</Menu> </FocusScope>
</FocusScope>
</ClickOutsideWrapper>
</div> </div>
)} )}
</div> </div>
@ -100,10 +125,7 @@ const getStyles = (theme: GrafanaTheme2) => {
display: 'inline-flex', display: 'inline-flex',
}), }),
menuWrapper: css({ menuWrapper: css({
position: 'absolute',
zIndex: theme.zIndex.dropdown, zIndex: theme.zIndex.dropdown,
top: theme.spacing(4),
right: 0,
}), }),
}; };
}; };

View File

@ -1,10 +1,20 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import {
autoUpdate,
flip,
offset as floatingUIOffset,
shift,
useClick,
useDismiss,
useFloating,
useInteractions,
} from '@floating-ui/react';
import { FocusScope } from '@react-aria/focus'; import { FocusScope } from '@react-aria/focus';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { usePopperTooltip } from 'react-popper-tooltip';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import { ReactUtils } from '../../utils'; import { ReactUtils } from '../../utils';
import { getPlacement } from '../../utils/tooltipUtils';
import { Portal } from '../Portal/Portal'; import { Portal } from '../Portal/Portal';
import { TooltipPlacement } from '../Tooltip/types'; import { TooltipPlacement } from '../Tooltip/types';
@ -25,17 +35,33 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
onVisibleChange?.(show); onVisibleChange?.(show);
}, [onVisibleChange, show]); }, [onVisibleChange, show]);
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible } = usePopperTooltip({ // the order of middleware is important!
visible: show, const middleware = [
placement: placement, floatingUIOffset({
onVisibleChange: setShow, mainAxis: offset?.[0] ?? 8,
interactive: true, crossAxis: offset?.[1] ?? 0,
delayHide: 0, }),
delayShow: 0, flip({
offset: offset ?? [0, 8], fallbackAxisSideDirection: 'end',
trigger: ['click'], // see https://floating-ui.com/docs/flip#combining-with-shift
crossAxis: false,
boundary: document.body,
}),
shift(),
];
const { context, refs, floatingStyles } = useFloating({
open: show,
placement: getPlacement(placement),
onOpenChange: setShow,
middleware,
whileElementsMounted: autoUpdate,
}); });
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
const animationDuration = 150; const animationDuration = 150;
const animationStyles = getStyles(animationDuration); const animationStyles = getStyles(animationDuration);
@ -44,7 +70,7 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
}; };
const handleKeys = (event: React.KeyboardEvent) => { const handleKeys = (event: React.KeyboardEvent) => {
if (event.key === 'Escape' || event.key === 'Tab') { if (event.key === 'Tab') {
setShow(false); setShow(false);
} }
}; };
@ -52,9 +78,10 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
return ( return (
<> <>
{React.cloneElement(children, { {React.cloneElement(children, {
ref: setTriggerRef, ref: refs.setReference,
...getReferenceProps(),
})} })}
{visible && ( {show && (
<Portal> <Portal>
<FocusScope autoFocus restoreFocus contain> <FocusScope autoFocus restoreFocus contain>
{/* {/*
@ -62,8 +89,7 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
*/} */}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */} {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
<div ref={setTooltipRef} {...getTooltipProps()} onClick={onOverlayClicked} onKeyDown={handleKeys}> <div ref={refs.setFloating} style={floatingStyles} onClick={onOverlayClicked} onKeyDown={handleKeys}>
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
<CSSTransition <CSSTransition
nodeRef={transitionRef} nodeRef={transitionRef}
appear={true} appear={true}
@ -71,7 +97,7 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
timeout={{ appear: animationDuration, exit: 0, enter: 0 }} timeout={{ appear: animationDuration, exit: 0, enter: 0 }}
classNames={animationStyles} classNames={animationStyles}
> >
<div ref={transitionRef}>{ReactUtils.renderOrCallToRender(overlay, {})}</div> <div ref={transitionRef}>{ReactUtils.renderOrCallToRender(overlay, { ...getFloatingProps() })}</div>
</CSSTransition> </CSSTransition>
</div> </div>
</FocusScope> </FocusScope>

View File

@ -88,7 +88,7 @@ export function Modal(props: PropsWithChildren<Props>) {
name="times" name="times"
size="xl" size="xl"
onClick={onDismiss} onClick={onDismiss}
tooltip={t('grafana-ui.modal.close-tooltip', 'Close')} aria-label={t('grafana-ui.modal.close-tooltip', 'Close')}
/> />
</div> </div>
</div> </div>

View File

@ -15,7 +15,7 @@ const meta: Meta<typeof Toggletip> = {
page: mdx, page: mdx,
}, },
controls: { controls: {
exclude: ['onClose', 'children'], exclude: ['children'],
}, },
}, },
argTypes: { argTypes: {

View File

@ -1,4 +1,4 @@
import { act, render, screen } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
@ -48,16 +48,15 @@ describe('Toggletip', () => {
expect(await screen.findByTestId('toggletip-content')).toBeInTheDocument(); expect(await screen.findByTestId('toggletip-content')).toBeInTheDocument();
// Escape should not close the toggletip
const button = screen.getByTestId('myButton');
await userEvent.click(button);
expect(onClose).toHaveBeenCalledTimes(1);
// Close button should not close the toggletip // Close button should not close the toggletip
const closeButton = screen.getByTestId('toggletip-header-close'); const closeButton = screen.getByTestId('toggletip-header-close');
expect(closeButton).toBeInTheDocument(); expect(closeButton).toBeInTheDocument();
await userEvent.click(closeButton); await userEvent.click(closeButton);
expect(onClose).toHaveBeenCalledTimes(1);
// Escape should not close the toggletip
const button = screen.getByTestId('myButton');
await userEvent.click(button);
await userEvent.keyboard('{escape}');
expect(onClose).toHaveBeenCalledTimes(2); expect(onClose).toHaveBeenCalledTimes(2);
// Either way, the toggletip should still be visible // Either way, the toggletip should still be visible
@ -162,7 +161,7 @@ describe('Toggletip', () => {
const button = screen.getByTestId('myButton'); const button = screen.getByTestId('myButton');
const afterButton = screen.getByText(afterInDom); const afterButton = screen.getByText(afterInDom);
await userEvent.click(button); await userEvent.click(button);
await userEvent.tab();
const closeButton = screen.getByTestId('toggletip-header-close'); const closeButton = screen.getByTestId('toggletip-header-close');
expect(closeButton).toHaveFocus(); expect(closeButton).toHaveFocus();
@ -183,14 +182,7 @@ describe('Toggletip', () => {
let user: ReturnType<typeof userEvent.setup>; let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); user = userEvent.setup();
// Need to use delay: null here to work with fakeTimers
// see https://github.com/testing-library/user-event/issues/833
user = userEvent.setup({ delay: null });
});
afterEach(() => {
jest.useRealTimers();
}); });
it('should restore focus to the button that opened the toggletip when closed from within the toggletip', async () => { it('should restore focus to the button that opened the toggletip when closed from within the toggletip', async () => {
@ -208,11 +200,10 @@ describe('Toggletip', () => {
const closeButton = await screen.findByTestId('toggletip-header-close'); const closeButton = await screen.findByTestId('toggletip-header-close');
expect(closeButton).toBeInTheDocument(); expect(closeButton).toBeInTheDocument();
await user.click(closeButton); await user.click(closeButton);
act(() => {
jest.runAllTimers();
});
expect(button).toHaveFocus(); await waitFor(() => {
expect(button).toHaveFocus();
});
}); });
it('should NOT restore focus to the button that opened the toggletip when closed from outside the toggletip', async () => { it('should NOT restore focus to the button that opened the toggletip when closed from outside the toggletip', async () => {
@ -239,9 +230,6 @@ describe('Toggletip', () => {
afterButton.focus(); afterButton.focus();
await user.keyboard('{escape}'); await user.keyboard('{escape}');
act(() => {
jest.runAllTimers();
});
expect(afterButton).toHaveFocus(); expect(afterButton).toHaveFocus();
}); });

View File

@ -1,12 +1,24 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import {
arrow,
autoUpdate,
flip,
FloatingArrow,
FloatingFocusManager,
offset,
shift,
useClick,
useDismiss,
useFloating,
useInteractions,
} from '@floating-ui/react';
import { Placement } from '@popperjs/core'; import { Placement } from '@popperjs/core';
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useRef, useState } from 'react';
import { usePopperTooltip } from 'react-popper-tooltip';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes/ThemeContext'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
import { buildTooltipTheme } from '../../utils/tooltipUtils'; import { buildTooltipTheme, getPlacement } from '../../utils/tooltipUtils';
import { IconButton } from '../IconButton/IconButton'; import { IconButton } from '../IconButton/IconButton';
import { ToggletipContent } from './types'; import { ToggletipContent } from './types';
@ -19,7 +31,7 @@ export interface ToggletipProps {
/** determine whether to show or not the close button **/ /** determine whether to show or not the close button **/
closeButton?: boolean; closeButton?: boolean;
/** Callback function to be called when the toggletip is closed */ /** Callback function to be called when the toggletip is closed */
onClose?: Function; onClose?: () => void;
/** The preferred placement of the toggletip */ /** The preferred placement of the toggletip */
placement?: Placement; placement?: Placement;
/** The text or component that houses the content of the toggleltip */ /** The text or component that houses the content of the toggleltip */
@ -50,94 +62,100 @@ export const Toggletip = React.memo(
onOpen, onOpen,
show, show,
}: ToggletipProps) => { }: ToggletipProps) => {
const arrowRef = useRef(null);
const grafanaTheme = useTheme2();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const style = styles[theme]; const style = styles[theme];
const contentRef = useRef(null); const [controlledVisible, setControlledVisible] = useState(show);
const [controlledVisible, setControlledVisible] = React.useState(show); const isOpen = show ?? controlledVisible;
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible, update, tooltipRef, triggerRef } = // the order of middleware is important!
usePopperTooltip( // `arrow` should almost always be at the end
{ // see https://floating-ui.com/docs/arrow#order
visible: show ?? controlledVisible, const middleware = [
placement: placement, offset(8),
interactive: true, flip({
offset: [0, 8], fallbackAxisSideDirection: 'end',
// If show is undefined, the toggletip will be shown on click // see https://floating-ui.com/docs/flip#combining-with-shift
trigger: 'click', crossAxis: false,
onVisibleChange: (visible: boolean) => { boundary: document.body,
if (show === undefined) { }),
setControlledVisible(visible); shift(),
} arrow({
if (!visible) { element: arrowRef,
onClose?.(); }),
} else { ];
onOpen?.();
} const { context, refs, floatingStyles } = useFloating({
}, open: isOpen,
}, placement: getPlacement(placement),
{ onOpenChange: (open) => {
strategy: 'fixed', if (show === undefined) {
setControlledVisible(open);
} }
); if (!open) {
onClose?.();
const closeToggletip = useCallback( } else {
(event: KeyboardEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>) => { onOpen?.();
setControlledVisible(false);
onClose?.();
if (event.target instanceof Node && tooltipRef?.contains(event.target)) {
triggerRef?.focus();
} }
}, },
[onClose, tooltipRef, triggerRef] middleware,
); whileElementsMounted: autoUpdate,
strategy: 'fixed',
});
useEffect(() => { const click = useClick(context);
if (controlledVisible) { const dismiss = useDismiss(context);
const handleKeyDown = (enterKey: KeyboardEvent) => {
if (enterKey.key === 'Escape') { const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
closeToggletip(enterKey);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}
return;
}, [controlledVisible, closeToggletip]);
return ( return (
<> <>
{React.cloneElement(children, { {React.cloneElement(children, {
ref: setTriggerRef, ref: refs.setReference,
tabIndex: 0, tabIndex: 0,
'aria-expanded': visible, 'aria-expanded': isOpen,
...getReferenceProps(),
})} })}
{visible && ( {isOpen && (
<div <FloatingFocusManager context={context} modal={false} closeOnFocusOut={false}>
data-testid="toggletip-content" <div
ref={setTooltipRef} data-testid="toggletip-content"
{...getTooltipProps({ className: cx(style.container, fitContent && styles.fitContent) })} className={cx(style.container, {
> [styles.fitContent]: fitContent,
{Boolean(title) && <div className={style.header}>{title}</div>} })}
{closeButton && ( ref={refs.setFloating}
<div className={style.headerClose}> style={floatingStyles}
<IconButton {...getFloatingProps()}
tooltip="Close" >
name="times" <FloatingArrow
data-testid="toggletip-header-close" strokeWidth={0.3}
onClick={closeToggletip} stroke={grafanaTheme.colors.border.weak}
/> className={style.arrow}
ref={arrowRef}
context={context}
/>
{Boolean(title) && <div className={style.header}>{title}</div>}
{closeButton && (
<div className={style.headerClose}>
<IconButton
aria-label="Close"
name="times"
data-testid="toggletip-header-close"
onClick={() => {
setControlledVisible(false);
onClose?.();
}}
/>
</div>
)}
<div className={style.body}>
{(typeof content === 'string' || React.isValidElement(content)) && content}
{typeof content === 'function' && content({})}
</div> </div>
)} {Boolean(footer) && <div className={style.footer}>{footer}</div>}
<div ref={contentRef} {...getArrowProps({ className: style.arrow })} />
<div className={style.body}>
{(typeof content === 'string' || React.isValidElement(content)) && content}
{typeof content === 'function' && update && content({ update })}
</div> </div>
{Boolean(footer) && <div className={style.footer}>{footer}</div>} </FloatingFocusManager>
</div>
)} )}
</> </>
); );

View File

@ -97,7 +97,7 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, ToolbarButtonProps>(
); );
return tooltip ? ( return tooltip ? (
<Tooltip content={tooltip} placement="bottom"> <Tooltip ref={ref} content={tooltip} placement="bottom">
{body} {body}
</Tooltip> </Tooltip>
) : ( ) : (

View File

@ -1,11 +1,23 @@
import React, { useCallback, useEffect, useId, useState } from 'react'; import {
import { usePopperTooltip } from 'react-popper-tooltip'; arrow,
autoUpdate,
flip,
FloatingArrow,
offset,
shift,
useDismiss,
useFloating,
useFocus,
useHover,
useInteractions,
} from '@floating-ui/react';
import React, { useCallback, useId, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../themes/ThemeContext'; import { useStyles2 } from '../../themes/ThemeContext';
import { buildTooltipTheme } from '../../utils/tooltipUtils'; import { buildTooltipTheme, getPlacement } from '../../utils/tooltipUtils';
import { Portal } from '../Portal/Portal'; import { Portal } from '../Portal/Portal';
import { PopoverContent, TooltipPlacement } from './types'; import { PopoverContent, TooltipPlacement } from './types';
@ -24,53 +36,55 @@ export interface TooltipProps {
export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>( export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
({ children, theme, interactive, show, placement, content }, forwardedRef) => { ({ children, theme, interactive, show, placement, content }, forwardedRef) => {
const arrowRef = useRef(null);
const [controlledVisible, setControlledVisible] = useState(show); const [controlledVisible, setControlledVisible] = useState(show);
const isOpen = show ?? controlledVisible;
// 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(),
arrow({
element: arrowRef,
}),
];
const { context, refs, floatingStyles } = useFloating({
open: isOpen,
placement: getPlacement(placement),
onOpenChange: setControlledVisible,
middleware,
whileElementsMounted: autoUpdate,
});
const tooltipId = useId(); const tooltipId = useId();
useEffect(() => { const hover = useHover(context, {
if (controlledVisible !== false) { delay: {
const handleKeyDown = (enterKey: KeyboardEvent) => { close: interactive ? 100 : 0,
if (enterKey.key === 'Escape') { },
setControlledVisible(false); move: false,
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
} else {
return;
}
}, [controlledVisible]);
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible, update } = usePopperTooltip({
visible: show ?? controlledVisible,
placement,
interactive,
delayHide: interactive ? 100 : 0,
offset: [0, 8],
trigger: ['hover', 'focus'],
onVisibleChange: setControlledVisible,
}); });
const focus = useFocus(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, hover, focus]);
const contentIsFunction = typeof content === 'function'; const contentIsFunction = typeof content === 'function';
/**
* If content is a function we need to call popper update function to make sure the tooltip is positioned correctly
* if it's close to the viewport boundary
**/
useEffect(() => {
if (update && contentIsFunction) {
update();
}
}, [visible, update, contentIsFunction]);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const style = styles[theme ?? 'info']; const style = styles[theme ?? 'info'];
const handleRef = useCallback( const handleRef = useCallback(
(ref: HTMLElement | null) => { (ref: HTMLElement | null) => {
setTriggerRef(ref); refs.setReference(ref);
if (typeof forwardedRef === 'function') { if (typeof forwardedRef === 'function') {
forwardedRef(ref); forwardedRef(ref);
@ -78,33 +92,35 @@ export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
forwardedRef.current = ref; forwardedRef.current = ref;
} }
}, },
[forwardedRef, setTriggerRef] [forwardedRef, refs]
); );
// if the child has a matching aria-label, this should take precedence over the tooltip content
// otherwise we end up double announcing things in e.g. IconButton
const childHasMatchingAriaLabel = 'aria-label' in children.props && children.props['aria-label'] === content;
return ( return (
<> <>
{React.cloneElement(children, { {React.cloneElement(children, {
ref: handleRef, ref: handleRef,
tabIndex: 0, // tooltip trigger should be keyboard focusable tabIndex: 0, // tooltip trigger should be keyboard focusable
'aria-describedby': visible ? tooltipId : undefined, 'aria-describedby': !childHasMatchingAriaLabel && isOpen ? tooltipId : undefined,
...getReferenceProps(),
})} })}
{visible && ( {isOpen && (
<Portal> <Portal>
<div <div ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()}>
data-testid={selectors.components.Tooltip.container} <FloatingArrow className={style.arrow} ref={arrowRef} context={context} />
ref={setTooltipRef} <div
id={tooltipId} data-testid={selectors.components.Tooltip.container}
role="tooltip" id={tooltipId}
{...getTooltipProps({ className: style.container })} role="tooltip"
> className={style.container}
<div {...getArrowProps({ className: style.arrow })} /> >
{typeof content === 'string' && content} {typeof content === 'string' && content}
{React.isValidElement(content) && React.cloneElement(content)} {React.isValidElement(content) && React.cloneElement(content)}
{contentIsFunction && {contentIsFunction && content({})}
update && </div>
content({
updatePopperPosition: update,
})}
</div> </div>
</Portal> </Portal>
)} )}

View File

@ -1,3 +1,4 @@
import { Placement } from '@floating-ui/react';
/** /**
* This API allows popovers to update Popper's position when e.g. popover content changes * This API allows popovers to update Popper's position when e.g. popover content changes
* updatePopperPosition is delivered to content by react-popper. * updatePopperPosition is delivered to content by react-popper.
@ -9,19 +10,4 @@ export interface PopoverContentProps {
export type PopoverContent = string | React.ReactElement | ((props: PopoverContentProps) => JSX.Element); export type PopoverContent = string | React.ReactElement | ((props: PopoverContentProps) => JSX.Element);
export type TooltipPlacement = export type TooltipPlacement = Placement | 'auto' | 'auto-start' | 'auto-end';
| 'auto-start'
| 'auto'
| 'auto-end'
| 'top-start'
| 'top'
| 'top-end'
| 'right-start'
| 'right'
| 'right-end'
| 'bottom-end'
| 'bottom'
| 'bottom-start'
| 'left-end'
| 'left'
| 'left-start';

View File

@ -1,7 +1,23 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Placement } from '@floating-ui/react';
import { colorManipulator, GrafanaTheme2 } from '@grafana/data'; import { colorManipulator, GrafanaTheme2 } from '@grafana/data';
import { TooltipPlacement } from '../components/Tooltip';
export function getPlacement(placement?: TooltipPlacement): Placement {
switch (placement) {
case 'auto':
return 'bottom';
case 'auto-start':
return 'bottom-start';
case 'auto-end':
return 'bottom-end';
default:
return placement ?? 'bottom';
}
}
export function buildTooltipTheme( export function buildTooltipTheme(
theme: GrafanaTheme2, theme: GrafanaTheme2,
tooltipBg: string, tooltipBg: string,
@ -11,29 +27,7 @@ export function buildTooltipTheme(
) { ) {
return { return {
arrow: css({ arrow: css({
height: '1rem', fill: tooltipBg,
width: '1rem',
position: 'absolute',
pointerEvents: 'none',
'&::before': {
borderStyle: 'solid',
content: '""',
display: 'block',
height: 0,
margin: 'auto',
width: 0,
},
'&::after': {
borderStyle: 'solid',
content: '""',
display: 'block',
height: 0,
margin: 'auto',
position: 'absolute',
width: 0,
},
}), }),
container: css({ container: css({
backgroundColor: tooltipBg, backgroundColor: tooltipBg,
@ -52,81 +46,12 @@ export function buildTooltipTheme(
pointerEvents: 'none', pointerEvents: 'none',
}, },
"&[data-popper-placement*='bottom'] > div[data-popper-arrow='true']": {
left: 0,
marginTop: '-7px',
top: 0,
'&::before': {
borderColor: `transparent transparent ${toggletipBorder} transparent`,
borderWidth: '0 8px 7px 8px',
position: 'absolute',
top: '-1px',
},
'&::after': {
borderColor: `transparent transparent ${tooltipBg} transparent`,
borderWidth: '0 8px 7px 8px',
},
},
"&[data-popper-placement*='top'] > div[data-popper-arrow='true']": {
bottom: 0,
left: 0,
marginBottom: '-14px',
'&::before': {
borderColor: `${toggletipBorder} transparent transparent transparent`,
borderWidth: '7px 8px 0 7px',
position: 'absolute',
top: '1px',
},
'&::after': {
borderColor: `${tooltipBg} transparent transparent transparent`,
borderWidth: '7px 8px 0 7px',
},
},
"&[data-popper-placement*='right'] > div[data-popper-arrow='true']": {
left: 0,
marginLeft: '-10px',
'&::before': {
borderColor: `transparent ${toggletipBorder} transparent transparent`,
borderWidth: '7px 6px 7px 0',
},
'&::after': {
borderColor: `transparent ${tooltipBg} transparent transparent`,
borderWidth: '6px 7px 7px 0',
left: '2px',
top: '1px',
},
},
"&[data-popper-placement*='left'] > div[data-popper-arrow='true']": {
marginRight: '-11px',
right: 0,
'&::before': {
borderColor: `transparent transparent transparent ${toggletipBorder}`,
borderWidth: '7px 0 6px 7px',
},
'&::after': {
borderColor: `transparent transparent transparent ${tooltipBg}`,
borderWidth: '6px 0 5px 5px',
left: '1px',
top: '1px',
},
},
code: { code: {
border: 'none', border: 'none',
display: 'inline', display: 'inline',
background: colorManipulator.darken(tooltipBg, 0.1), background: colorManipulator.darken(tooltipBg, 0.1),
color: tooltipText, color: tooltipText,
whiteSpace: 'normal',
}, },
pre: { pre: {

View File

@ -57,7 +57,7 @@ export function AppChromeMenu({}: Props) {
classNames={animationStyles.overlay} classNames={animationStyles.overlay}
timeout={{ enter: animationSpeed, exit: 0 }} timeout={{ enter: animationSpeed, exit: 0 }}
> >
<FocusScope contain autoFocus> <FocusScope contain autoFocus restoreFocus>
<MegaMenu className={styles.menu} onClose={onClose} ref={ref} {...overlayProps} {...dialogProps} /> <MegaMenu className={styles.menu} onClose={onClose} ref={ref} {...overlayProps} {...dialogProps} />
</FocusScope> </FocusScope>
</CSSTransition> </CSSTransition>

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { autoUpdate, flip, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react';
import React, { useCallback, useId, useMemo, useState } from 'react'; import React, { useCallback, useId, useMemo, useState } from 'react';
import { usePopperTooltip } from 'react-popper-tooltip';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
@ -85,13 +85,19 @@ export function NestedFolderPicker({
const rootCollection = useSelector(rootItemsSelector); const rootCollection = useSelector(rootItemsSelector);
const childrenCollections = useSelector(childrenByParentUIDSelector); const childrenCollections = useSelector(childrenByParentUIDSelector);
const { getTooltipProps, setTooltipRef, setTriggerRef, visible, triggerRef } = usePopperTooltip({ // the order of middleware is important!
visible: overlayOpen, const middleware = [
flip({
// see https://floating-ui.com/docs/flip#combining-with-shift
crossAxis: false,
boundary: document.body,
}),
];
const { context, refs, floatingStyles, elements } = useFloating({
open: overlayOpen,
placement: 'bottom', placement: 'bottom',
interactive: true, onOpenChange: (value) => {
offset: [0, 0],
trigger: 'click',
onVisibleChange: (value: boolean) => {
// ensure state is clean on opening the overlay // ensure state is clean on opening the overlay
if (value) { if (value) {
setSearch(''); setSearch('');
@ -99,8 +105,15 @@ export function NestedFolderPicker({
} }
setOverlayOpen(value); setOverlayOpen(value);
}, },
middleware,
whileElementsMounted: autoUpdate,
}); });
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
const handleFolderExpand = useCallback( const handleFolderExpand = useCallback(
async (uid: string, newOpenState: boolean) => { async (uid: string, newOpenState: boolean) => {
setFolderOpenState((old) => ({ ...old, [uid]: newOpenState })); setFolderOpenState((old) => ({ ...old, [uid]: newOpenState }));
@ -209,7 +222,7 @@ export function NestedFolderPicker({
handleFolderExpand, handleFolderExpand,
idPrefix: overlayId, idPrefix: overlayId,
search, search,
visible, visible: overlayOpen,
}); });
let label = selectedFolder.data?.title; let label = selectedFolder.data?.title;
@ -217,14 +230,14 @@ export function NestedFolderPicker({
label = 'Dashboards'; label = 'Dashboards';
} }
if (!visible) { if (!overlayOpen) {
return ( return (
<Trigger <Trigger
label={label} label={label}
invalid={invalid} invalid={invalid}
isLoading={selectedFolder.isLoading} isLoading={selectedFolder.isLoading}
autoFocus={autoFocusButton} autoFocus={autoFocusButton}
ref={setTriggerRef} ref={refs.setReference}
aria-label={ aria-label={
label label
? t('browse-dashboards.folder-picker.accessible-label', 'Select folder: {{ label }} currently selected', { ? t('browse-dashboards.folder-picker.accessible-label', 'Select folder: {{ label }} currently selected', {
@ -232,6 +245,7 @@ export function NestedFolderPicker({
}) })
: undefined : undefined
} }
{...getReferenceProps()}
/> />
); );
} }
@ -239,14 +253,13 @@ export function NestedFolderPicker({
return ( return (
<> <>
<Input <Input
ref={setTriggerRef} ref={refs.setReference}
autoFocus autoFocus
prefix={label ? <Icon name="folder" /> : null} prefix={label ? <Icon name="folder" /> : null}
placeholder={label ?? t('browse-dashboards.folder-picker.search-placeholder', 'Search folders')} placeholder={label ?? t('browse-dashboards.folder-picker.search-placeholder', 'Search folders')}
value={search} value={search}
invalid={invalid} invalid={invalid}
className={styles.search} className={styles.search}
onKeyDown={handleKeyDown}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
aria-autocomplete="list" aria-autocomplete="list"
aria-expanded aria-expanded
@ -256,16 +269,18 @@ export function NestedFolderPicker({
aria-activedescendant={getDOMId(overlayId, flatTree[focusedItemIndex]?.item.uid)} aria-activedescendant={getDOMId(overlayId, flatTree[focusedItemIndex]?.item.uid)}
role="combobox" role="combobox"
suffix={<Icon name="search" />} suffix={<Icon name="search" />}
{...getReferenceProps()}
onKeyDown={handleKeyDown}
/> />
<fieldset <fieldset
ref={setTooltipRef} ref={refs.setFloating}
id={overlayId} id={overlayId}
{...getTooltipProps({ className={styles.tableWrapper}
className: styles.tableWrapper, style={{
style: { ...floatingStyles,
width: triggerRef?.clientWidth, width: elements.domReference?.clientWidth,
}, }}
})} {...getFloatingProps()}
> >
{error ? ( {error ? (
<Alert <Alert

View File

@ -33,14 +33,9 @@ function renderTraceViewContainer(frames = [frameOld]) {
describe('TraceViewContainer', () => { describe('TraceViewContainer', () => {
let user: ReturnType<typeof userEvent.setup>; let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); user = userEvent.setup();
// Need to use delay: null here to work with fakeTimers
// see https://github.com/testing-library/user-event/issues/833
user = userEvent.setup({ delay: null });
});
afterEach(() => {
jest.useRealTimers();
}); });
it('toggles children visibility', async () => { it('toggles children visibility', async () => {

View File

@ -109,8 +109,9 @@ const addFilter = async (
await waitFor(() => expect(screen.getByText(property)).toBeInTheDocument()); await waitFor(() => expect(screen.getByText(property)).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Property')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Property')).toBeInTheDocument());
const operationSelect = await screen.getAllByText('='); const operationSelect = await screen.findAllByText('=');
selectOptionInTest(operationSelect[index], operationLabel); await userEvent.click(operationSelect[index]);
await userEvent.click(screen.getByRole('menuitemradio', { name: operationLabel }));
await waitFor(() => expect(screen.getByText(operationLabel)).toBeInTheDocument()); await waitFor(() => expect(screen.getByText(operationLabel)).toBeInTheDocument());
const valueSelect = await screen.findByText('Value'); const valueSelect = await screen.findByText('Value');

View File

@ -1,6 +1,15 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import {
autoUpdate,
flip,
offset,
shift,
useClick,
useDismiss,
useFloating,
useInteractions,
} from '@floating-ui/react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { usePopperTooltip } from 'react-popper-tooltip';
import { GrafanaTheme2, renderMarkdown } from '@grafana/data'; import { GrafanaTheme2, renderMarkdown } from '@grafana/data';
import { FlexItem } from '@grafana/experimental'; import { FlexItem } from '@grafana/experimental';
@ -16,28 +25,46 @@ export interface Props {
export const OperationInfoButton = React.memo<Props>(({ def, operation }) => { export const OperationInfoButton = React.memo<Props>(({ def, operation }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const { getTooltipProps, setTooltipRef, setTriggerRef, visible } = usePopperTooltip({
// the order of middleware is important!
const middleware = [
offset(16),
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: show,
placement: 'top', placement: 'top',
visible: show, onOpenChange: setShow,
offset: [0, 16], middleware,
onVisibleChange: setShow, whileElementsMounted: autoUpdate,
interactive: true,
trigger: ['click'],
}); });
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
return ( return (
<> <>
<Button <Button
title="Click to show description" title="Click to show description"
ref={setTriggerRef} ref={refs.setReference}
icon="info-circle" icon="info-circle"
size="sm" size="sm"
variant="secondary" variant="secondary"
fill="text" fill="text"
{...getReferenceProps()}
/> />
{visible && ( {show && (
<Portal> <Portal>
<div ref={setTooltipRef} {...getTooltipProps()} className={styles.docBox}> <div ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()} className={styles.docBox}>
<div className={styles.docBoxHeader}> <div className={styles.docBoxHeader}>
<span>{def.renderer(operation, def, '<expr>')}</span> <span>{def.renderer(operation, def, '<expr>')}</span>
<FlexItem grow={1} /> <FlexItem grow={1} />
@ -86,14 +113,6 @@ const getStyles = (theme: GrafanaTheme2) => {
marginBottom: theme.spacing(-1), marginBottom: theme.spacing(-1),
color: theme.colors.text.secondary, color: theme.colors.text.secondary,
}), }),
signature: css({
fontSize: theme.typography.bodySmall.fontSize,
fontFamily: theme.typography.fontFamilyMonospace,
}),
dropdown: css({
opacity: 0,
color: theme.colors.text.secondary,
}),
}; };
}; };
function getOperationDocs(def: QueryBuilderOperationDef, op: QueryBuilderOperation): string { function getOperationDocs(def: QueryBuilderOperationDef, op: QueryBuilderOperation): string {

View File

@ -2760,6 +2760,32 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@floating-ui/react-dom@npm:^2.0.3":
version: 2.0.4
resolution: "@floating-ui/react-dom@npm:2.0.4"
dependencies:
"@floating-ui/dom": "npm:^1.5.1"
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 4240a718502c797fd2e174cd06dcd7321a6eda9c8966dbaf61864b9e16445e95649a59bfe7c19ee13f68c11f3693724d7970c7e618089a3d3915bd343639cfae
languageName: node
linkType: hard
"@floating-ui/react@npm:0.26.4":
version: 0.26.4
resolution: "@floating-ui/react@npm:0.26.4"
dependencies:
"@floating-ui/react-dom": "npm:^2.0.3"
"@floating-ui/utils": "npm:^0.1.5"
tabbable: "npm:^6.0.1"
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: dda933ec2abee0b09360eaec889462e072967340837fd55a91ef76f78a034c57b73f8174fa9f37d62141037ab651b1e02dcd996ee12b66dcc6a8b0e8516821c9
languageName: node
linkType: hard
"@floating-ui/utils@npm:^0.1.3": "@floating-ui/utils@npm:^0.1.3":
version: 0.1.4 version: 0.1.4
resolution: "@floating-ui/utils@npm:0.1.4" resolution: "@floating-ui/utils@npm:0.1.4"
@ -2767,6 +2793,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@floating-ui/utils@npm:^0.1.5":
version: 0.1.6
resolution: "@floating-ui/utils@npm:0.1.6"
checksum: 450ec4ecc1dd8161b1904d4e1e9d95e653cc06f79af6c3b538b79efb10541d90bcc88646ab3cdffc5b92e00c4804ca727b025d153ad285f42dbbb39aec219ec9
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:1.12.0": "@formatjs/ecma402-abstract@npm:1.12.0":
version: 1.12.0 version: 1.12.0
resolution: "@formatjs/ecma402-abstract@npm:1.12.0" resolution: "@formatjs/ecma402-abstract@npm:1.12.0"
@ -3376,6 +3409,7 @@ __metadata:
"@babel/core": "npm:7.23.2" "@babel/core": "npm:7.23.2"
"@emotion/css": "npm:11.11.2" "@emotion/css": "npm:11.11.2"
"@emotion/react": "npm:11.11.1" "@emotion/react": "npm:11.11.1"
"@floating-ui/react": "npm:0.26.4"
"@grafana/data": "npm:10.3.0-pre" "@grafana/data": "npm:10.3.0-pre"
"@grafana/e2e-selectors": "npm:10.3.0-pre" "@grafana/e2e-selectors": "npm:10.3.0-pre"
"@grafana/faro-web-sdk": "npm:^1.3.5" "@grafana/faro-web-sdk": "npm:^1.3.5"
@ -3481,7 +3515,6 @@ __metadata:
react-inlinesvg: "npm:3.0.2" react-inlinesvg: "npm:3.0.2"
react-loading-skeleton: "npm:3.3.1" react-loading-skeleton: "npm:3.3.1"
react-popper: "npm:2.3.0" react-popper: "npm:2.3.0"
react-popper-tooltip: "npm:4.4.2"
react-router-dom: "npm:5.3.3" react-router-dom: "npm:5.3.3"
react-select: "npm:5.7.4" react-select: "npm:5.7.4"
react-select-event: "npm:^5.1.0" react-select-event: "npm:^5.1.0"
@ -5203,7 +5236,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@popperjs/core@npm:2.11.8, @popperjs/core@npm:^2.11.5": "@popperjs/core@npm:2.11.8":
version: 2.11.8 version: 2.11.8
resolution: "@popperjs/core@npm:2.11.8" resolution: "@popperjs/core@npm:2.11.8"
checksum: ddd16090cde777aaf102940f05d0274602079a95ad9805bd20bc55dcc7c3a2ba1b99dd5c73e5cc2753c3d31250ca52a67d58059459d7d27debb983a9f552936c checksum: ddd16090cde777aaf102940f05d0274602079a95ad9805bd20bc55dcc7c3a2ba1b99dd5c73e5cc2753c3d31250ca52a67d58059459d7d27debb983a9f552936c
@ -17328,6 +17361,7 @@ __metadata:
"@emotion/eslint-plugin": "npm:11.11.0" "@emotion/eslint-plugin": "npm:11.11.0"
"@emotion/react": "npm:11.11.1" "@emotion/react": "npm:11.11.1"
"@fingerprintjs/fingerprintjs": "npm:^3.4.2" "@fingerprintjs/fingerprintjs": "npm:^3.4.2"
"@floating-ui/react": "npm:0.26.4"
"@glideapps/glide-data-grid": "npm:^5.2.1" "@glideapps/glide-data-grid": "npm:^5.2.1"
"@grafana-plugins/grafana-testdata-datasource": "workspace:*" "@grafana-plugins/grafana-testdata-datasource": "workspace:*"
"@grafana-plugins/parca": "workspace:*" "@grafana-plugins/parca": "workspace:*"
@ -17594,7 +17628,6 @@ __metadata:
react-loading-skeleton: "npm:3.3.1" react-loading-skeleton: "npm:3.3.1"
react-moveable: "npm:0.46.1" react-moveable: "npm:0.46.1"
react-popper: "npm:2.3.0" react-popper: "npm:2.3.0"
react-popper-tooltip: "npm:4.4.2"
react-redux: "npm:8.1.3" react-redux: "npm:8.1.3"
react-refresh: "npm:0.14.0" react-refresh: "npm:0.14.0"
react-resizable: "npm:3.0.5" react-resizable: "npm:3.0.5"
@ -25743,21 +25776,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-popper-tooltip@npm:4.4.2": "react-popper@npm:2.3.0":
version: 4.4.2
resolution: "react-popper-tooltip@npm:4.4.2"
dependencies:
"@babel/runtime": "npm:^7.18.3"
"@popperjs/core": "npm:^2.11.5"
react-popper: "npm:^2.3.0"
peerDependencies:
react: ">=16.6.0"
react-dom: ">=16.6.0"
checksum: 87192cd6a42f2d04826da82b4a55130a0bc50ef5fbf83328e0560b47a27e363dbe5538a66b835fb0309e2f2c011e20b09029c36c8a241fb790605638f212e6aa
languageName: node
linkType: hard
"react-popper@npm:2.3.0, react-popper@npm:^2.3.0":
version: 2.3.0 version: 2.3.0
resolution: "react-popper@npm:2.3.0" resolution: "react-popper@npm:2.3.0"
dependencies: dependencies:
@ -28773,6 +28792,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tabbable@npm:^6.0.1":
version: 6.2.0
resolution: "tabbable@npm:6.2.0"
checksum: 980fa73476026e99dcacfc0d6e000d41d42c8e670faf4682496d30c625495e412c4369694f2a15cf1e5252d22de3c396f2b62edbe8d60b5dadc40d09e3f2dde3
languageName: node
linkType: hard
"table@npm:^6.8.1": "table@npm:^6.8.1":
version: 6.8.1 version: 6.8.1
resolution: "table@npm:6.8.1" resolution: "table@npm:6.8.1"