Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: merge old Tooltip with ActionTooltip and introduce new Tooltip #1189

Merged
merged 4 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/components/ActionTooltip/ActionTooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@
$block: '.#{variables.$ns}action-tooltip';

#{$block} {
&__layout {
--g-popup-border-width: 0;
--g-popup-background-color: var(--g-color-base-float-heavy);

&__popup-content {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems this class is not exists anymore

// prevent glitch between two nearby tooltip refs
pointer-events: none;
}

&__content {
padding: 6px 12px;
color: var(--g-color-text-light-primary);
max-width: 300px;
box-sizing: border-box;
}
Expand Down
101 changes: 73 additions & 28 deletions src/components/ActionTooltip/ActionTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,89 @@
import React from 'react';

import {Hotkey} from '../Hotkey';
import type {HotkeyProps} from '../Hotkey';
import {Tooltip} from '../Tooltip';
import type {TooltipProps} from '../Tooltip';
import {useForkRef} from '../../hooks';
import {type TooltipDelayProps, useTooltipVisible} from '../../hooks/private';
import {Hotkey, type HotkeyProps} from '../Hotkey';
import {Popup, type PopupPlacement} from '../Popup';
import type {DOMProps, QAProps} from '../types';
import {block} from '../utils/cn';

import './ActionTooltip.scss';

const b = block('action-tooltip');

export interface ActionTooltipProps
extends Pick<
TooltipProps,
'children' | 'disabled' | 'placement' | 'openDelay' | 'closeDelay' | 'className' | 'qa'
> {
export interface ActionTooltipProps extends QAProps, DOMProps, TooltipDelayProps {
id?: string;
disablePortal?: boolean;
contentClassName?: string;
disabled?: boolean;
placement?: PopupPlacement;
children: React.ReactElement;
title: string;
hotkey?: HotkeyProps['value'];
description?: React.ReactNode;
}

const DEFAULT_PLACEMENT: PopupPlacement = ['bottom', 'top'];
const b = block('action-tooltip');

export function ActionTooltip(props: ActionTooltipProps) {
const {title, hotkey, description, children, ...tooltipProps} = props;
const {
placement = DEFAULT_PLACEMENT,
title,
hotkey,
children,
className,
contentClassName,
description,
disabled = false,
style,
qa,
id,
disablePortal,
...delayProps
} = props;

const [anchorElement, setAnchorElement] = React.useState<HTMLElement | null>(null);
const tooltipVisible = useTooltipVisible(anchorElement, delayProps);

const renderPopup = () => {
return (
<Popup
id={id}
disablePortal={disablePortal}
role="tooltip"
className={b(null, className)}
style={style}
open={tooltipVisible && !disabled}
placement={placement}
anchorRef={{current: anchorElement}}
disableEscapeKeyDown
disableOutsideClick
disableLayer
qa={qa}
>
<div className={b('content', contentClassName)}>
<React.Fragment>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fragment is not necessary here

<div className={b('heading')}>
<div className={b('title')}>{title}</div>
{hotkey && (
<Hotkey view="dark" value={hotkey} className={b('hotkey')} />
)}
</div>
{description && <div className={b('description')}>{description}</div>}
</React.Fragment>
</div>
</Popup>
);
};

const child = React.Children.only(children);
const childRef = (child as any).ref;

Check warning on line 79 in src/components/ActionTooltip/ActionTooltip.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected any. Specify a different type

const ref = useForkRef(setAnchorElement, childRef);

return (
<Tooltip
{...tooltipProps}
className={b(null, tooltipProps.className)}
contentClassName={b('layout')}
content={
<React.Fragment>
<div className={b('heading')}>
<div className={b('title')}>{title}</div>
{hotkey && <Hotkey view="dark" value={hotkey} className={b('hotkey')} />}
</div>
{description && <div className={b('description')}>{description}</div>}
</React.Fragment>
}
>
{children}
</Tooltip>
<React.Fragment>
{React.cloneElement(child, {ref})}
{anchorElement ? renderPopup() : null}
</React.Fragment>
);
}
35 changes: 35 additions & 0 deletions src/components/ActionTooltip/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!--GITHUB_BLOCK-->

# ActionTooltip

<!--/GITHUB_BLOCK-->

A simple text tip that uses its children node as an anchor. For correct functioning, the anchor node
must be able to handle mouse events and focus or blur events.

## Usage

```tsx
import {ActionTooltip} from '@gravity-ui/uikit';

<ActionTooltip title="Content">
<div tabIndex={0}>Anchor</div>
</ActionTooltip>;
```

## Properties

| Name | Description | Type | Default |
| :--------------- | --------------------------------------------------------------------------------------- | :----------------------------------------------: | :-----: |
| children | An anchor element for a `Tooltip`. Must accept a `ref` that will provide a DOM element. | `React.ReactElement` | |
| closeDelay | Number of ms to delay hiding the `Tooltip` after the hover ends | `number` | `0` |
| openDelay | Number of ms to delay showing the `Tooltip` after the hover begins | `number` | `250` |
| placement | `Tooltip` position relative to its anchor | [`PopupPlacement`](../Popup/README.md#placement) | |
| qa | HTML `data-qa` attribute, used in tests | `string` | |
| title | Tooltip title text | `string` | |
| description | Tooltip description text | `string` | |
| hotkey | Hot keys that are assigned to an interface action. | `string` | |
| id | This prop is used to help implement the accessibility logic. | `string` | |
| disablePortal | Do not use Portal for children | `boolean` | |
| contentClassName | HTML class attribute for content node | `string` | |
| disabled | Prevent popup from opening | `boolean` | `false` |
128 changes: 128 additions & 0 deletions src/components/ActionTooltip/__tests__/ActionTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from 'react';

import {createEvent, fireEvent, render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import {ActionTooltip} from '../ActionTooltip';

export function fireAnimationEndEvent(el: Node | Window, animationName = 'animation') {
const ev = createEvent.animationEnd(el, {animationName});
Object.assign(ev, {
animationName,
});

fireEvent(el, ev);
}

test('should preserve ref on anchor element', () => {
const ref = jest.fn();
render(
<ActionTooltip title="text">
<button ref={ref} />
</ActionTooltip>,
);

expect(ref).toHaveBeenCalledTimes(1);
});

test('should show tooltip on hover and hide on un hover', async () => {
const user = userEvent.setup();

render(
<ActionTooltip title="test content">
<button />
</ActionTooltip>,
);

const button = await screen.findByRole('button');

await user.hover(button);

const tooltip = await screen.findByRole('tooltip');

expect(tooltip).toBeVisible();

await user.unhover(button);

fireAnimationEndEvent(tooltip);

expect(tooltip).not.toBeInTheDocument();
});

test('should show tooltip on focus and hide on blur', async () => {
const user = userEvent.setup();
render(
<ActionTooltip title="test content">
<button />
</ActionTooltip>,
);

const button = await screen.findByRole('button');

await user.tab();
expect(button).toHaveFocus();

const tooltip = await screen.findByRole('tooltip');

expect(tooltip).toBeVisible();

await user.tab();

fireAnimationEndEvent(tooltip);

expect(button).not.toHaveFocus();
expect(tooltip).not.toBeInTheDocument();
});

test('should hide on press Escape', async () => {
const user = userEvent.setup();
render(
<ActionTooltip title="test content">
<button />
</ActionTooltip>,
);

const button = await screen.findByRole('button');

await user.tab();
expect(button).toHaveFocus();

const tooltip = await screen.findByRole('tooltip');

expect(tooltip).toBeVisible();

await user.keyboard('[Escape]');

fireAnimationEndEvent(tooltip);

expect(button).toHaveFocus();
expect(tooltip).not.toBeInTheDocument();
});

test('should show on focus and hide on un hover', async () => {
const user = userEvent.setup();
render(
<ActionTooltip title="test content">
<button />
</ActionTooltip>,
);

const button = screen.getByRole('button');

button.focus();

const tooltip = await screen.findByRole('tooltip');

expect(tooltip).toBeVisible();

await user.hover(button);

expect(tooltip).toBeVisible();

await user.unhover(button);

fireAnimationEndEvent(tooltip);

expect(button).toHaveFocus();
expect(tooltip).not.toBeInTheDocument();
});
26 changes: 16 additions & 10 deletions src/components/Tooltip/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

<!--/GITHUB_BLOCK-->

A simple text tip that uses its children node as an anchor. To function correctly, the anchor node
must be able to handle mouse events and focus or blur events.
A simple text tip that uses its children node as an anchor. This component accepts only text content and may be an excellent alternative to the browser title with its small size and increased appearance delay.

Tooltip has a light and dark theme.

## Usage

Expand All @@ -19,11 +20,16 @@ import {Tooltip} from '@gravity-ui/uikit';

## Properties

| Name | Description | Type | Default |
| :--------- | --------------------------------------------------------------------------------------- | :----------------------------------------------: | :-----: |
| children | An anchor element for a `Tooltip`. Must accept a `ref` that will provide a DOM element. | `React.ReactElement` | |
| content | Content that will be shown in the `Tooltip` | `React.ReactNode` | |
| closeDelay | Number of ms to delay hiding the `Tooltip` after the hover ends | `number` | `0` |
| openDelay | Number of ms to delay showing the `Tooltip` after the hover begins | `number` | `250` |
| placement | `Tooltip` position relative to its anchor | [`PopupPlacement`](../Popup/README.md#placement) | |
| qa | HTML `data-qa` attribute, used in tests | `string` | |
| Name | Description | Type | Default |
| :--------------- | --------------------------------------------------------------------------------------- | :----------------------------------------------: | :-----: |
| children | An anchor element for a `Tooltip`. Must accept a `ref` that will provide a DOM element. | `React.ReactElement` | |
| closeDelay | Number of ms to delay hiding the `Tooltip` after the hover ends | `number` | `0` |
| openDelay | Number of ms to delay showing the `Tooltip` after the hover begins | `number` | `1000` |
| placement | `Tooltip` position relative to its anchor | [`PopupPlacement`](../Popup/README.md#placement) | |
| qa | HTML `data-qa` attribute, used in tests | `string` | |
| content | Text content that will be shown in the `Tooltip` | `string` | |
| id | This prop is used to help implement the accessibility logic. | `string` | |
| disablePortal | Do not use Portal for children | `boolean` | |
| contentClassName | HTML class attribute for content node | `string` | |
| className | HTML class attribute for popup | `string` | |
| disabled | Prevent popup from opening | `boolean` | `false` |
33 changes: 29 additions & 4 deletions src/components/Tooltip/Tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,41 @@
$block: '.#{variables.$ns}tooltip';

#{$block} {
--g-popup-border-width: 0;
--g-popup-background-color: var(--g-color-base-float-heavy);
// [class] for increasing specificity
&[class] {
--g-popup-border-width: 0;

> div {
padding: 4px 8px;
max-width: 360px;
box-sizing: border-box;
box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.15);

animation-duration: unset;
animation-timing-function: unset;
animation-fill-mode: unset;
}
}

&__popup-content {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for ActionTooltip

// prevent glitch between two nearby tooltip refs
pointer-events: none;
}

&__content {
padding: 6px 12px;
color: var(--g-color-text-light-primary);
// -webkit-line-clamp will not work without display: -webkit-box;
/* stylelint-disable-next-line */
display: -webkit-box;

-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-ms-box-orient: vertical;

-webkit-line-clamp: 20;
-moz-line-clamp: 20;
-ms-line-clamp: 20;

overflow: hidden;
text-overflow: ellipsis;
}
}
Loading
Loading