Skip to content

Commit

Permalink
feat!: merge old Tooltip with ActionTooltip and introduce new Tooltip (
Browse files Browse the repository at this point in the history
  • Loading branch information
DenisVershkov authored Dec 22, 2023
1 parent 84f8c97 commit 56aa587
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 163 deletions.
7 changes: 6 additions & 1 deletion src/components/ActionTooltip/ActionTooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
$block: '.#{variables.$ns}action-tooltip';

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

&__content {
padding: 6px 12px;
color: var(--g-color-text-light-primary);
max-width: 300px;
box-sizing: border-box;
}
Expand Down
89 changes: 65 additions & 24 deletions src/components/ActionTooltip/ActionTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,85 @@
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;

return (
<Tooltip
{...tooltipProps}
className={b(null, tooltipProps.className)}
contentClassName={b('layout')}
content={
<React.Fragment>
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)}>
<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>
</div>
</Popup>
);
};

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

Check warning on line 75 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 (
<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 | Content that will be shown in the `Tooltip` | `React.ReactNode` | |
| 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` |
34 changes: 27 additions & 7 deletions src/components/Tooltip/Tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,36 @@
$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;

&__popup-content {
// prevent glitch between two nearby tooltip refs
pointer-events: none;
> 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;
}
}

&__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

0 comments on commit 56aa587

Please sign in to comment.