mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GrafanaUI: Support Tooltip as Dropdown child (#68521)
Co-authored-by: eledobleefe <laura.fernandez@grafana.com> Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com
This commit is contained in:
parent
51c15f3a89
commit
7b3221d494
@ -61,8 +61,9 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
});
|
||||
|
||||
// In order to standardise Button please always consider using IconButton when you need a button with an icon only
|
||||
// When using tooltip, ref is forwarded to Tooltip component instead for https://github.com/grafana/grafana/issues/65632
|
||||
const button = (
|
||||
<button className={cx(styles.button, className)} type={type} {...otherProps} ref={ref}>
|
||||
<button className={cx(styles.button, className)} type={type} {...otherProps} ref={tooltip ? undefined : ref}>
|
||||
{icon && <Icon name={icon} size={size} className={styles.icon} />}
|
||||
{children && <span className={styles.content}>{children}</span>}
|
||||
</button>
|
||||
@ -70,7 +71,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip content={tooltip} placement={tooltipPlacement}>
|
||||
<Tooltip ref={ref} content={tooltip} placement={tooltipPlacement}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
@ -123,8 +124,9 @@ export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
||||
className
|
||||
);
|
||||
|
||||
// When using tooltip, ref is forwarded to Tooltip component instead for https://github.com/grafana/grafana/issues/65632
|
||||
const button = (
|
||||
<a className={linkButtonStyles} {...otherProps} tabIndex={disabled ? -1 : 0} ref={ref}>
|
||||
<a className={linkButtonStyles} {...otherProps} tabIndex={disabled ? -1 : 0} ref={tooltip ? undefined : ref}>
|
||||
{icon && <Icon name={icon} size={size} className={styles.icon} />}
|
||||
{children && <span className={styles.content}>{children}</span>}
|
||||
</a>
|
||||
@ -132,7 +134,7 @@ export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip content={tooltip} placement={tooltipPlacement}>
|
||||
<Tooltip ref={ref} content={tooltip} placement={tooltipPlacement}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ import React from 'react';
|
||||
import { StoryExample } from '../../utils/storybook/StoryExample';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { Button } from '../Button';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { VerticalGroup } from '../Layout/Layout';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
|
||||
@ -41,9 +42,10 @@ export function Examples() {
|
||||
<Button variant="secondary">Button</Button>
|
||||
</Dropdown>
|
||||
</StoryExample>
|
||||
|
||||
<StoryExample name="Icon button, placement=bottom-start">
|
||||
<Dropdown overlay={menu} placement="bottom-start">
|
||||
<Button variant="secondary" icon="bars" />
|
||||
<IconButton tooltip="Open menu" variant="secondary" name="bars" />
|
||||
</Dropdown>
|
||||
</StoryExample>
|
||||
</VerticalGroup>
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '../Button';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
|
||||
import { Dropdown } from './Dropdown';
|
||||
|
||||
describe('Dropdown', () => {
|
||||
it('supports buttons with tooltips', async () => {
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="View settings" />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
render(
|
||||
<Dropdown overlay={menu}>
|
||||
<Button tooltip="Tooltip content">Open me</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Open me' });
|
||||
|
||||
await userEvent.hover(button);
|
||||
expect(await screen.findByText('Tooltip content')).toBeVisible(); // tooltip appears on a delay
|
||||
|
||||
await userEvent.click(button);
|
||||
expect(screen.queryByText('View settings')).toBeVisible();
|
||||
});
|
||||
});
|
@ -61,15 +61,21 @@ export const IconButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
const styles = getStyles(theme, limitedIconSize, variant);
|
||||
const tooltipString = typeof tooltip === 'string' ? tooltip : '';
|
||||
|
||||
// When using tooltip, ref is forwarded to Tooltip component instead for https://github.com/grafana/grafana/issues/65632
|
||||
const button = (
|
||||
<button ref={ref} aria-label={ariaLabel || tooltipString} {...restProps} className={cx(styles.button, className)}>
|
||||
<button
|
||||
ref={tooltip ? undefined : ref}
|
||||
aria-label={ariaLabel || tooltipString}
|
||||
{...restProps}
|
||||
className={cx(styles.button, className)}
|
||||
>
|
||||
<Icon name={name} size={limitedIconSize} className={styles.icon} type={iconType} />
|
||||
</button>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip content={tooltip} placement={tooltipPlacement}>
|
||||
<Tooltip ref={ref} content={tooltip} placement={tooltipPlacement}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import React, { MutableRefObject } from 'react';
|
||||
|
||||
import { Tooltip } from './Tooltip';
|
||||
|
||||
@ -14,4 +14,28 @@ describe('Tooltip', () => {
|
||||
);
|
||||
expect(screen.getByText('Link with tooltip')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('forwards the function ref', () => {
|
||||
const refFn = jest.fn();
|
||||
|
||||
render(
|
||||
<Tooltip content="Cooltip content" ref={refFn}>
|
||||
<span>On the page</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
expect(refFn).toBeCalled();
|
||||
});
|
||||
|
||||
it('forwards the mutable ref', () => {
|
||||
const refObj: MutableRefObject<HTMLElement | null> = { current: null };
|
||||
|
||||
render(
|
||||
<Tooltip content="Cooltip content" ref={refObj}>
|
||||
<span>On the page</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
expect(refObj.current).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
@ -21,7 +21,8 @@ export interface TooltipProps {
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
export const Tooltip = React.memo(({ children, theme, interactive, show, placement, content }: TooltipProps) => {
|
||||
export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
|
||||
({ children, theme, interactive, show, placement, content }, forwardedRef) => {
|
||||
const [controlledVisible, setControlledVisible] = React.useState(show);
|
||||
|
||||
useEffect(() => {
|
||||
@ -54,10 +55,23 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme
|
||||
const styles = useStyles2(getStyles);
|
||||
const style = styles[theme ?? 'info'];
|
||||
|
||||
const handleRef = useCallback(
|
||||
(ref: HTMLElement | null) => {
|
||||
setTriggerRef(ref);
|
||||
|
||||
if (typeof forwardedRef === 'function') {
|
||||
forwardedRef(ref);
|
||||
} else if (forwardedRef) {
|
||||
forwardedRef.current = ref;
|
||||
}
|
||||
},
|
||||
[forwardedRef, setTriggerRef]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, {
|
||||
ref: setTriggerRef,
|
||||
ref: handleRef,
|
||||
tabIndex: 0, // tooltip should be keyboard focusable
|
||||
})}
|
||||
{visible && (
|
||||
@ -76,7 +90,8 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Tooltip.displayName = 'Tooltip';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user