mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9c9a055c3e
commit
9de79fb5e9
@ -241,6 +241,7 @@
|
||||
"@emotion/css": "11.11.2",
|
||||
"@emotion/react": "11.11.1",
|
||||
"@fingerprintjs/fingerprintjs": "^3.4.2",
|
||||
"@floating-ui/react": "0.26.4",
|
||||
"@glideapps/glide-data-grid": "^5.2.1",
|
||||
"@grafana-plugins/grafana-testdata-datasource": "workspace:*",
|
||||
"@grafana-plugins/parca": "workspace:*",
|
||||
@ -382,7 +383,6 @@
|
||||
"react-loading-skeleton": "3.3.1",
|
||||
"react-moveable": "0.46.1",
|
||||
"react-popper": "2.3.0",
|
||||
"react-popper-tooltip": "4.4.2",
|
||||
"react-redux": "8.1.3",
|
||||
"react-resizable": "3.0.5",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
|
@ -49,6 +49,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.11.2",
|
||||
"@emotion/react": "11.11.1",
|
||||
"@floating-ui/react": "0.26.4",
|
||||
"@grafana/data": "10.3.0-pre",
|
||||
"@grafana/e2e-selectors": "10.3.0-pre",
|
||||
"@grafana/faro-web-sdk": "^1.3.5",
|
||||
@ -96,7 +97,6 @@
|
||||
"react-inlinesvg": "3.0.2",
|
||||
"react-loading-skeleton": "3.3.1",
|
||||
"react-popper": "2.3.0",
|
||||
"react-popper-tooltip": "4.4.2",
|
||||
"react-router-dom": "5.3.3",
|
||||
"react-select": "5.7.4",
|
||||
"react-table": "7.8.0",
|
||||
|
@ -151,7 +151,7 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
|
||||
{isOpen && (
|
||||
<div data-testid={selectors.components.TimePicker.overlayContent}>
|
||||
<div role="presentation" className={cx(modalBackdrop, styles.backdrop)} {...underlayProps} />
|
||||
<FocusScope contain autoFocus>
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<section className={styles.content} ref={overlayRef} {...overlayProps} {...dialogProps}>
|
||||
<TimePickerContent
|
||||
timeZone={timeZone}
|
||||
|
@ -1,14 +1,20 @@
|
||||
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 { useMenuTrigger } from '@react-aria/menu';
|
||||
import { useMenuTriggerState } from '@react-stately/menu';
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
import React, { HTMLAttributes, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
import { MenuItem } from '../Menu/MenuItem';
|
||||
import { ToolbarButton, ToolbarButtonVariant } from '../ToolbarButton';
|
||||
@ -33,53 +39,72 @@ export interface Props<T> extends HTMLAttributes<HTMLButtonElement> {
|
||||
const ButtonSelectComponent = <T,>(props: Props<T>) => {
|
||||
const { className, options, value, onChange, narrow, variant, ...restProps } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
const state = useMenuTriggerState({});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const ref = React.useRef(null);
|
||||
const { menuTriggerProps, menuProps } = useMenuTrigger({}, state, ref);
|
||||
const { buttonProps } = useButton(menuTriggerProps, ref);
|
||||
// the order of middleware is important!
|
||||
const middleware = [
|
||||
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>) => {
|
||||
onChange(item);
|
||||
state.close();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<ToolbarButton
|
||||
className={className}
|
||||
isOpen={state.isOpen}
|
||||
isOpen={isOpen}
|
||||
narrow={narrow}
|
||||
variant={variant}
|
||||
ref={ref}
|
||||
{...buttonProps}
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
{...restProps}
|
||||
>
|
||||
{value?.label || (value?.value != null ? String(value?.value) : null)}
|
||||
</ToolbarButton>
|
||||
{state.isOpen && (
|
||||
<div className={styles.menuWrapper}>
|
||||
<ClickOutsideWrapper onClick={state.close} parent={document} includeButtonPress={false}>
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
{/*
|
||||
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
|
||||
*/}
|
||||
<Menu tabIndex={-1} onClose={state.close} {...menuProps} autoFocus={!!menuProps.autoFocus}>
|
||||
{options.map((item) => (
|
||||
<MenuItem
|
||||
key={`${item.value}`}
|
||||
label={item.label ?? String(item.value)}
|
||||
onClick={() => onChangeInternal(item)}
|
||||
active={item.value === value?.value}
|
||||
ariaChecked={item.value === value?.value}
|
||||
ariaLabel={item.ariaLabel || item.label}
|
||||
role="menuitemradio"
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</FocusScope>
|
||||
</ClickOutsideWrapper>
|
||||
{isOpen && (
|
||||
<div className={styles.menuWrapper} ref={refs.setFloating} {...getFloatingProps()} style={floatingStyles}>
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
{/*
|
||||
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
|
||||
*/}
|
||||
<Menu tabIndex={-1} onClose={() => setIsOpen(false)}>
|
||||
{options.map((item) => (
|
||||
<MenuItem
|
||||
key={`${item.value}`}
|
||||
label={item.label ?? String(item.value)}
|
||||
onClick={() => onChangeInternal(item)}
|
||||
active={item.value === value?.value}
|
||||
ariaChecked={item.value === value?.value}
|
||||
ariaLabel={item.ariaLabel || item.label}
|
||||
role="menuitemradio"
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</FocusScope>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -100,10 +125,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
display: 'inline-flex',
|
||||
}),
|
||||
menuWrapper: css({
|
||||
position: 'absolute',
|
||||
zIndex: theme.zIndex.dropdown,
|
||||
top: theme.spacing(4),
|
||||
right: 0,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -1,10 +1,20 @@
|
||||
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 React, { useEffect, useRef, useState } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
import { ReactUtils } from '../../utils';
|
||||
import { getPlacement } from '../../utils/tooltipUtils';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
import { TooltipPlacement } from '../Tooltip/types';
|
||||
|
||||
@ -25,17 +35,33 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
|
||||
onVisibleChange?.(show);
|
||||
}, [onVisibleChange, show]);
|
||||
|
||||
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible } = usePopperTooltip({
|
||||
visible: show,
|
||||
placement: placement,
|
||||
onVisibleChange: setShow,
|
||||
interactive: true,
|
||||
delayHide: 0,
|
||||
delayShow: 0,
|
||||
offset: offset ?? [0, 8],
|
||||
trigger: ['click'],
|
||||
// the order of middleware is important!
|
||||
const middleware = [
|
||||
floatingUIOffset({
|
||||
mainAxis: offset?.[0] ?? 8,
|
||||
crossAxis: offset?.[1] ?? 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: 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 animationStyles = getStyles(animationDuration);
|
||||
|
||||
@ -44,7 +70,7 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
|
||||
};
|
||||
|
||||
const handleKeys = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Escape' || event.key === 'Tab') {
|
||||
if (event.key === 'Tab') {
|
||||
setShow(false);
|
||||
}
|
||||
};
|
||||
@ -52,9 +78,10 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, {
|
||||
ref: setTriggerRef,
|
||||
ref: refs.setReference,
|
||||
...getReferenceProps(),
|
||||
})}
|
||||
{visible && (
|
||||
{show && (
|
||||
<Portal>
|
||||
<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
|
||||
*/}
|
||||
{/* 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 {...getArrowProps({ className: 'tooltip-arrow' })} />
|
||||
<div ref={refs.setFloating} style={floatingStyles} onClick={onOverlayClicked} onKeyDown={handleKeys}>
|
||||
<CSSTransition
|
||||
nodeRef={transitionRef}
|
||||
appear={true}
|
||||
@ -71,7 +97,7 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
|
||||
timeout={{ appear: animationDuration, exit: 0, enter: 0 }}
|
||||
classNames={animationStyles}
|
||||
>
|
||||
<div ref={transitionRef}>{ReactUtils.renderOrCallToRender(overlay, {})}</div>
|
||||
<div ref={transitionRef}>{ReactUtils.renderOrCallToRender(overlay, { ...getFloatingProps() })}</div>
|
||||
</CSSTransition>
|
||||
</div>
|
||||
</FocusScope>
|
||||
|
@ -88,7 +88,7 @@ export function Modal(props: PropsWithChildren<Props>) {
|
||||
name="times"
|
||||
size="xl"
|
||||
onClick={onDismiss}
|
||||
tooltip={t('grafana-ui.modal.close-tooltip', 'Close')}
|
||||
aria-label={t('grafana-ui.modal.close-tooltip', 'Close')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@ const meta: Meta<typeof Toggletip> = {
|
||||
page: mdx,
|
||||
},
|
||||
controls: {
|
||||
exclude: ['onClose', 'children'],
|
||||
exclude: ['children'],
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
|
@ -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 React from 'react';
|
||||
|
||||
@ -48,16 +48,15 @@ describe('Toggletip', () => {
|
||||
|
||||
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
|
||||
const closeButton = screen.getByTestId('toggletip-header-close');
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
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);
|
||||
|
||||
// Either way, the toggletip should still be visible
|
||||
@ -162,7 +161,7 @@ describe('Toggletip', () => {
|
||||
const button = screen.getByTestId('myButton');
|
||||
const afterButton = screen.getByText(afterInDom);
|
||||
await userEvent.click(button);
|
||||
await userEvent.tab();
|
||||
|
||||
const closeButton = screen.getByTestId('toggletip-header-close');
|
||||
expect(closeButton).toHaveFocus();
|
||||
|
||||
@ -183,14 +182,7 @@ describe('Toggletip', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
// 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();
|
||||
user = userEvent.setup();
|
||||
});
|
||||
|
||||
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');
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
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 () => {
|
||||
@ -239,9 +230,6 @@ describe('Toggletip', () => {
|
||||
afterButton.focus();
|
||||
|
||||
await user.keyboard('{escape}');
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(afterButton).toHaveFocus();
|
||||
});
|
||||
|
@ -1,12 +1,24 @@
|
||||
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 React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { buildTooltipTheme } from '../../utils/tooltipUtils';
|
||||
import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
|
||||
import { buildTooltipTheme, getPlacement } from '../../utils/tooltipUtils';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
import { ToggletipContent } from './types';
|
||||
@ -19,7 +31,7 @@ export interface ToggletipProps {
|
||||
/** determine whether to show or not the close button **/
|
||||
closeButton?: boolean;
|
||||
/** Callback function to be called when the toggletip is closed */
|
||||
onClose?: Function;
|
||||
onClose?: () => void;
|
||||
/** The preferred placement of the toggletip */
|
||||
placement?: Placement;
|
||||
/** The text or component that houses the content of the toggleltip */
|
||||
@ -50,94 +62,100 @@ export const Toggletip = React.memo(
|
||||
onOpen,
|
||||
show,
|
||||
}: ToggletipProps) => {
|
||||
const arrowRef = useRef(null);
|
||||
const grafanaTheme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const style = styles[theme];
|
||||
const contentRef = useRef(null);
|
||||
const [controlledVisible, setControlledVisible] = React.useState(show);
|
||||
const [controlledVisible, setControlledVisible] = useState(show);
|
||||
const isOpen = show ?? controlledVisible;
|
||||
|
||||
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible, update, tooltipRef, triggerRef } =
|
||||
usePopperTooltip(
|
||||
{
|
||||
visible: show ?? controlledVisible,
|
||||
placement: placement,
|
||||
interactive: true,
|
||||
offset: [0, 8],
|
||||
// If show is undefined, the toggletip will be shown on click
|
||||
trigger: 'click',
|
||||
onVisibleChange: (visible: boolean) => {
|
||||
if (show === undefined) {
|
||||
setControlledVisible(visible);
|
||||
}
|
||||
if (!visible) {
|
||||
onClose?.();
|
||||
} else {
|
||||
onOpen?.();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
strategy: 'fixed',
|
||||
// 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: (open) => {
|
||||
if (show === undefined) {
|
||||
setControlledVisible(open);
|
||||
}
|
||||
);
|
||||
|
||||
const closeToggletip = useCallback(
|
||||
(event: KeyboardEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
setControlledVisible(false);
|
||||
onClose?.();
|
||||
|
||||
if (event.target instanceof Node && tooltipRef?.contains(event.target)) {
|
||||
triggerRef?.focus();
|
||||
if (!open) {
|
||||
onClose?.();
|
||||
} else {
|
||||
onOpen?.();
|
||||
}
|
||||
},
|
||||
[onClose, tooltipRef, triggerRef]
|
||||
);
|
||||
middleware,
|
||||
whileElementsMounted: autoUpdate,
|
||||
strategy: 'fixed',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (controlledVisible) {
|
||||
const handleKeyDown = (enterKey: KeyboardEvent) => {
|
||||
if (enterKey.key === 'Escape') {
|
||||
closeToggletip(enterKey);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [controlledVisible, closeToggletip]);
|
||||
const click = useClick(context);
|
||||
const dismiss = useDismiss(context);
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, {
|
||||
ref: setTriggerRef,
|
||||
ref: refs.setReference,
|
||||
tabIndex: 0,
|
||||
'aria-expanded': visible,
|
||||
'aria-expanded': isOpen,
|
||||
...getReferenceProps(),
|
||||
})}
|
||||
{visible && (
|
||||
<div
|
||||
data-testid="toggletip-content"
|
||||
ref={setTooltipRef}
|
||||
{...getTooltipProps({ className: cx(style.container, fitContent && styles.fitContent) })}
|
||||
>
|
||||
{Boolean(title) && <div className={style.header}>{title}</div>}
|
||||
{closeButton && (
|
||||
<div className={style.headerClose}>
|
||||
<IconButton
|
||||
tooltip="Close"
|
||||
name="times"
|
||||
data-testid="toggletip-header-close"
|
||||
onClick={closeToggletip}
|
||||
/>
|
||||
{isOpen && (
|
||||
<FloatingFocusManager context={context} modal={false} closeOnFocusOut={false}>
|
||||
<div
|
||||
data-testid="toggletip-content"
|
||||
className={cx(style.container, {
|
||||
[styles.fitContent]: fitContent,
|
||||
})}
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<FloatingArrow
|
||||
strokeWidth={0.3}
|
||||
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 ref={contentRef} {...getArrowProps({ className: style.arrow })} />
|
||||
<div className={style.body}>
|
||||
{(typeof content === 'string' || React.isValidElement(content)) && content}
|
||||
{typeof content === 'function' && update && content({ update })}
|
||||
{Boolean(footer) && <div className={style.footer}>{footer}</div>}
|
||||
</div>
|
||||
{Boolean(footer) && <div className={style.footer}>{footer}</div>}
|
||||
</div>
|
||||
</FloatingFocusManager>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -97,7 +97,7 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, ToolbarButtonProps>(
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<Tooltip content={tooltip} placement="bottom">
|
||||
<Tooltip ref={ref} content={tooltip} placement="bottom">
|
||||
{body}
|
||||
</Tooltip>
|
||||
) : (
|
||||
|
@ -1,11 +1,23 @@
|
||||
import React, { useCallback, useEffect, useId, useState } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
import {
|
||||
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 { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { buildTooltipTheme } from '../../utils/tooltipUtils';
|
||||
import { buildTooltipTheme, getPlacement } from '../../utils/tooltipUtils';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
|
||||
import { PopoverContent, TooltipPlacement } from './types';
|
||||
@ -24,53 +36,55 @@ export interface TooltipProps {
|
||||
|
||||
export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
|
||||
({ children, theme, interactive, show, placement, content }, forwardedRef) => {
|
||||
const arrowRef = useRef(null);
|
||||
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();
|
||||
|
||||
useEffect(() => {
|
||||
if (controlledVisible !== false) {
|
||||
const handleKeyDown = (enterKey: KeyboardEvent) => {
|
||||
if (enterKey.key === 'Escape') {
|
||||
setControlledVisible(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 hover = useHover(context, {
|
||||
delay: {
|
||||
close: interactive ? 100 : 0,
|
||||
},
|
||||
move: false,
|
||||
});
|
||||
const focus = useFocus(context);
|
||||
const dismiss = useDismiss(context);
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, hover, focus]);
|
||||
|
||||
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 style = styles[theme ?? 'info'];
|
||||
|
||||
const handleRef = useCallback(
|
||||
(ref: HTMLElement | null) => {
|
||||
setTriggerRef(ref);
|
||||
refs.setReference(ref);
|
||||
|
||||
if (typeof forwardedRef === 'function') {
|
||||
forwardedRef(ref);
|
||||
@ -78,33 +92,35 @@ export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
|
||||
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 (
|
||||
<>
|
||||
{React.cloneElement(children, {
|
||||
ref: handleRef,
|
||||
tabIndex: 0, // tooltip trigger should be keyboard focusable
|
||||
'aria-describedby': visible ? tooltipId : undefined,
|
||||
'aria-describedby': !childHasMatchingAriaLabel && isOpen ? tooltipId : undefined,
|
||||
...getReferenceProps(),
|
||||
})}
|
||||
{visible && (
|
||||
{isOpen && (
|
||||
<Portal>
|
||||
<div
|
||||
data-testid={selectors.components.Tooltip.container}
|
||||
ref={setTooltipRef}
|
||||
id={tooltipId}
|
||||
role="tooltip"
|
||||
{...getTooltipProps({ className: style.container })}
|
||||
>
|
||||
<div {...getArrowProps({ className: style.arrow })} />
|
||||
{typeof content === 'string' && content}
|
||||
{React.isValidElement(content) && React.cloneElement(content)}
|
||||
{contentIsFunction &&
|
||||
update &&
|
||||
content({
|
||||
updatePopperPosition: update,
|
||||
})}
|
||||
<div ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()}>
|
||||
<FloatingArrow className={style.arrow} ref={arrowRef} context={context} />
|
||||
<div
|
||||
data-testid={selectors.components.Tooltip.container}
|
||||
id={tooltipId}
|
||||
role="tooltip"
|
||||
className={style.container}
|
||||
>
|
||||
{typeof content === 'string' && content}
|
||||
{React.isValidElement(content) && React.cloneElement(content)}
|
||||
{contentIsFunction && content({})}
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
|
@ -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
|
||||
* 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 TooltipPlacement =
|
||||
| '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';
|
||||
export type TooltipPlacement = Placement | 'auto' | 'auto-start' | 'auto-end';
|
||||
|
@ -1,7 +1,23 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Placement } from '@floating-ui/react';
|
||||
|
||||
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(
|
||||
theme: GrafanaTheme2,
|
||||
tooltipBg: string,
|
||||
@ -11,29 +27,7 @@ export function buildTooltipTheme(
|
||||
) {
|
||||
return {
|
||||
arrow: css({
|
||||
height: '1rem',
|
||||
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,
|
||||
},
|
||||
fill: tooltipBg,
|
||||
}),
|
||||
container: css({
|
||||
backgroundColor: tooltipBg,
|
||||
@ -52,81 +46,12 @@ export function buildTooltipTheme(
|
||||
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: {
|
||||
border: 'none',
|
||||
display: 'inline',
|
||||
background: colorManipulator.darken(tooltipBg, 0.1),
|
||||
color: tooltipText,
|
||||
whiteSpace: 'normal',
|
||||
},
|
||||
|
||||
pre: {
|
||||
|
@ -57,7 +57,7 @@ export function AppChromeMenu({}: Props) {
|
||||
classNames={animationStyles.overlay}
|
||||
timeout={{ enter: animationSpeed, exit: 0 }}
|
||||
>
|
||||
<FocusScope contain autoFocus>
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<MegaMenu className={styles.menu} onClose={onClose} ref={ref} {...overlayProps} {...dialogProps} />
|
||||
</FocusScope>
|
||||
</CSSTransition>
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 { usePopperTooltip } from 'react-popper-tooltip';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
@ -85,13 +85,19 @@ export function NestedFolderPicker({
|
||||
const rootCollection = useSelector(rootItemsSelector);
|
||||
const childrenCollections = useSelector(childrenByParentUIDSelector);
|
||||
|
||||
const { getTooltipProps, setTooltipRef, setTriggerRef, visible, triggerRef } = usePopperTooltip({
|
||||
visible: overlayOpen,
|
||||
// the order of middleware is important!
|
||||
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',
|
||||
interactive: true,
|
||||
offset: [0, 0],
|
||||
trigger: 'click',
|
||||
onVisibleChange: (value: boolean) => {
|
||||
onOpenChange: (value) => {
|
||||
// ensure state is clean on opening the overlay
|
||||
if (value) {
|
||||
setSearch('');
|
||||
@ -99,8 +105,15 @@ export function NestedFolderPicker({
|
||||
}
|
||||
setOverlayOpen(value);
|
||||
},
|
||||
middleware,
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
const click = useClick(context);
|
||||
const dismiss = useDismiss(context);
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
|
||||
|
||||
const handleFolderExpand = useCallback(
|
||||
async (uid: string, newOpenState: boolean) => {
|
||||
setFolderOpenState((old) => ({ ...old, [uid]: newOpenState }));
|
||||
@ -209,7 +222,7 @@ export function NestedFolderPicker({
|
||||
handleFolderExpand,
|
||||
idPrefix: overlayId,
|
||||
search,
|
||||
visible,
|
||||
visible: overlayOpen,
|
||||
});
|
||||
|
||||
let label = selectedFolder.data?.title;
|
||||
@ -217,14 +230,14 @@ export function NestedFolderPicker({
|
||||
label = 'Dashboards';
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
if (!overlayOpen) {
|
||||
return (
|
||||
<Trigger
|
||||
label={label}
|
||||
invalid={invalid}
|
||||
isLoading={selectedFolder.isLoading}
|
||||
autoFocus={autoFocusButton}
|
||||
ref={setTriggerRef}
|
||||
ref={refs.setReference}
|
||||
aria-label={
|
||||
label
|
||||
? t('browse-dashboards.folder-picker.accessible-label', 'Select folder: {{ label }} currently selected', {
|
||||
@ -232,6 +245,7 @@ export function NestedFolderPicker({
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
{...getReferenceProps()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -239,14 +253,13 @@ export function NestedFolderPicker({
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
ref={setTriggerRef}
|
||||
ref={refs.setReference}
|
||||
autoFocus
|
||||
prefix={label ? <Icon name="folder" /> : null}
|
||||
placeholder={label ?? t('browse-dashboards.folder-picker.search-placeholder', 'Search folders')}
|
||||
value={search}
|
||||
invalid={invalid}
|
||||
className={styles.search}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
aria-autocomplete="list"
|
||||
aria-expanded
|
||||
@ -256,16 +269,18 @@ export function NestedFolderPicker({
|
||||
aria-activedescendant={getDOMId(overlayId, flatTree[focusedItemIndex]?.item.uid)}
|
||||
role="combobox"
|
||||
suffix={<Icon name="search" />}
|
||||
{...getReferenceProps()}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<fieldset
|
||||
ref={setTooltipRef}
|
||||
ref={refs.setFloating}
|
||||
id={overlayId}
|
||||
{...getTooltipProps({
|
||||
className: styles.tableWrapper,
|
||||
style: {
|
||||
width: triggerRef?.clientWidth,
|
||||
},
|
||||
})}
|
||||
className={styles.tableWrapper}
|
||||
style={{
|
||||
...floatingStyles,
|
||||
width: elements.domReference?.clientWidth,
|
||||
}}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
{error ? (
|
||||
<Alert
|
||||
|
@ -33,14 +33,9 @@ function renderTraceViewContainer(frames = [frameOld]) {
|
||||
|
||||
describe('TraceViewContainer', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
// 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();
|
||||
user = userEvent.setup();
|
||||
});
|
||||
|
||||
it('toggles children visibility', async () => {
|
||||
|
@ -109,8 +109,9 @@ const addFilter = async (
|
||||
await waitFor(() => expect(screen.getByText(property)).toBeInTheDocument());
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Property')).toBeInTheDocument());
|
||||
const operationSelect = await screen.getAllByText('=');
|
||||
selectOptionInTest(operationSelect[index], operationLabel);
|
||||
const operationSelect = await screen.findAllByText('=');
|
||||
await userEvent.click(operationSelect[index]);
|
||||
await userEvent.click(screen.getByRole('menuitemradio', { name: operationLabel }));
|
||||
await waitFor(() => expect(screen.getByText(operationLabel)).toBeInTheDocument());
|
||||
|
||||
const valueSelect = await screen.findByText('Value');
|
||||
|
@ -1,6 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
useClick,
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useInteractions,
|
||||
} from '@floating-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
|
||||
import { GrafanaTheme2, renderMarkdown } from '@grafana/data';
|
||||
import { FlexItem } from '@grafana/experimental';
|
||||
@ -16,28 +25,46 @@ export interface Props {
|
||||
export const OperationInfoButton = React.memo<Props>(({ def, operation }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
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',
|
||||
visible: show,
|
||||
offset: [0, 16],
|
||||
onVisibleChange: setShow,
|
||||
interactive: true,
|
||||
trigger: ['click'],
|
||||
onOpenChange: setShow,
|
||||
middleware,
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
const click = useClick(context);
|
||||
const dismiss = useDismiss(context);
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
title="Click to show description"
|
||||
ref={setTriggerRef}
|
||||
ref={refs.setReference}
|
||||
icon="info-circle"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
{...getReferenceProps()}
|
||||
/>
|
||||
{visible && (
|
||||
{show && (
|
||||
<Portal>
|
||||
<div ref={setTooltipRef} {...getTooltipProps()} className={styles.docBox}>
|
||||
<div ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()} className={styles.docBox}>
|
||||
<div className={styles.docBoxHeader}>
|
||||
<span>{def.renderer(operation, def, '<expr>')}</span>
|
||||
<FlexItem grow={1} />
|
||||
@ -86,14 +113,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
marginBottom: theme.spacing(-1),
|
||||
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 {
|
||||
|
62
yarn.lock
62
yarn.lock
@ -2760,6 +2760,32 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.1.4
|
||||
resolution: "@floating-ui/utils@npm:0.1.4"
|
||||
@ -2767,6 +2793,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.12.0
|
||||
resolution: "@formatjs/ecma402-abstract@npm:1.12.0"
|
||||
@ -3376,6 +3409,7 @@ __metadata:
|
||||
"@babel/core": "npm:7.23.2"
|
||||
"@emotion/css": "npm:11.11.2"
|
||||
"@emotion/react": "npm:11.11.1"
|
||||
"@floating-ui/react": "npm:0.26.4"
|
||||
"@grafana/data": "npm:10.3.0-pre"
|
||||
"@grafana/e2e-selectors": "npm:10.3.0-pre"
|
||||
"@grafana/faro-web-sdk": "npm:^1.3.5"
|
||||
@ -3481,7 +3515,6 @@ __metadata:
|
||||
react-inlinesvg: "npm:3.0.2"
|
||||
react-loading-skeleton: "npm:3.3.1"
|
||||
react-popper: "npm:2.3.0"
|
||||
react-popper-tooltip: "npm:4.4.2"
|
||||
react-router-dom: "npm:5.3.3"
|
||||
react-select: "npm:5.7.4"
|
||||
react-select-event: "npm:^5.1.0"
|
||||
@ -5203,7 +5236,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@popperjs/core@npm:2.11.8, @popperjs/core@npm:^2.11.5":
|
||||
"@popperjs/core@npm:2.11.8":
|
||||
version: 2.11.8
|
||||
resolution: "@popperjs/core@npm:2.11.8"
|
||||
checksum: ddd16090cde777aaf102940f05d0274602079a95ad9805bd20bc55dcc7c3a2ba1b99dd5c73e5cc2753c3d31250ca52a67d58059459d7d27debb983a9f552936c
|
||||
@ -17328,6 +17361,7 @@ __metadata:
|
||||
"@emotion/eslint-plugin": "npm:11.11.0"
|
||||
"@emotion/react": "npm:11.11.1"
|
||||
"@fingerprintjs/fingerprintjs": "npm:^3.4.2"
|
||||
"@floating-ui/react": "npm:0.26.4"
|
||||
"@glideapps/glide-data-grid": "npm:^5.2.1"
|
||||
"@grafana-plugins/grafana-testdata-datasource": "workspace:*"
|
||||
"@grafana-plugins/parca": "workspace:*"
|
||||
@ -17594,7 +17628,6 @@ __metadata:
|
||||
react-loading-skeleton: "npm:3.3.1"
|
||||
react-moveable: "npm:0.46.1"
|
||||
react-popper: "npm:2.3.0"
|
||||
react-popper-tooltip: "npm:4.4.2"
|
||||
react-redux: "npm:8.1.3"
|
||||
react-refresh: "npm:0.14.0"
|
||||
react-resizable: "npm:3.0.5"
|
||||
@ -25743,21 +25776,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-popper-tooltip@npm:4.4.2":
|
||||
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":
|
||||
"react-popper@npm:2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "react-popper@npm:2.3.0"
|
||||
dependencies:
|
||||
@ -28773,6 +28792,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.8.1
|
||||
resolution: "table@npm:6.8.1"
|
||||
|
Loading…
Reference in New Issue
Block a user