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/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",
|
||||||
|
@ -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",
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -15,7 +15,7 @@ const meta: Meta<typeof Toggletip> = {
|
|||||||
page: mdx,
|
page: mdx,
|
||||||
},
|
},
|
||||||
controls: {
|
controls: {
|
||||||
exclude: ['onClose', 'children'],
|
exclude: ['children'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
argTypes: {
|
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 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();
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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';
|
|
||||||
|
@ -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: {
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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');
|
||||||
|
@ -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 {
|
||||||
|
62
yarn.lock
62
yarn.lock
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user