Skip to content

Commit

Permalink
refactor Tooltip and ActionTooltip
Browse files Browse the repository at this point in the history
  • Loading branch information
amje committed Dec 20, 2024
1 parent 8a6febd commit 1e14cb2
Show file tree
Hide file tree
Showing 14 changed files with 404 additions and 344 deletions.
16 changes: 5 additions & 11 deletions src/components/ActionTooltip/ActionTooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@
$block: '.#{variables.$ns}action-tooltip';

#{$block} {
--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;
}
--g-tooltip-text-color: var(--g-color-text-light-primary);
--g-tooltip-background-color: var(--g-color-base-float-heavy);
--g-tooltip-padding: var(--g-spacing-2) var(--g-spacing-3);

&__heading {
display: flex;
Expand All @@ -25,11 +19,11 @@ $block: '.#{variables.$ns}action-tooltip';
}

&__hotkey {
margin-inline-start: 8px;
margin-inline-start: var(--g-spacing-2);
}

&__description {
margin-block-start: 4px;
margin-block-start: var(--g-spacing-1);
color: var(--g-color-text-light-secondary);
}
}
111 changes: 42 additions & 69 deletions src/components/ActionTooltip/ActionTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,89 +2,62 @@

import React from 'react';

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

import './ActionTooltip.scss';

export interface ActionTooltipProps extends QAProps, DOMProps, TooltipDelayProps {
id?: string;
disablePortal?: boolean;
contentClassName?: string;
disabled?: boolean;
placement?: PopupPlacement;
children: React.ReactElement;
export interface ActionTooltipProps
extends QAProps,
DOMProps,
Omit<TooltipProps, 'content' | 'role'> {
/** Floating element title */
title: string;
hotkey?: HotkeyProps['value'];
/** Floating element description */
description?: React.ReactNode;
/** Floating element hotkey label */
hotkey?: HotkeyProps['value'];
}

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

export function ActionTooltip(props: ActionTooltipProps) {
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}
anchorElement={anchorElement}
disableEscapeKeyDown
disableOutsideClick
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>}
const DEFAULT_OPEN_DELAY = 500;
const DEFAULT_CLOSE_DELAY = 0;

export function ActionTooltip({
title,
description,
hotkey,
openDelay = DEFAULT_OPEN_DELAY,
closeDelay = DEFAULT_CLOSE_DELAY,
className,
...restProps
}: ActionTooltipProps) {
const content = React.useMemo(
() => (
<React.Fragment>
<div className={b('heading')}>
<div className={b('title')}>{title}</div>
{hotkey && <Hotkey view="dark" value={hotkey} className={b('hotkey')} />}
</div>
</Popup>
);
};

const child = React.Children.only(children);
const childRef = getElementRef(child);

const ref = useForkRef(setAnchorElement, childRef);
{description && <div className={b('description')}>{description}</div>}
</React.Fragment>
),
[title, description, hotkey],
);

return (
<React.Fragment>
{React.cloneElement(child, {ref})}
{anchorElement ? renderPopup() : null}
</React.Fragment>
<Tooltip
{...restProps}
// eslint-disable-next-line jsx-a11y/aria-role
role="label"
content={content}
openDelay={openDelay}
closeDelay={closeDelay}
className={b(null, className)}
/>
);
}
58 changes: 41 additions & 17 deletions src/components/ActionTooltip/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,56 @@

<!--/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.
[`Tooltip`](../Tooltip/README.md) for labeling action buttons without descriptive text (e.g. icon buttons).

## Usage

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

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

## Anchor

In order for `ActionTooltip` to work you should pass a valid `ReactElement` as a children which accepts `ref` property for `HTMLElement`
and other properties for `HTMLElement`.

Alternatively, you can pass function as a children to provide ref and props manually to your underlying components:

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

<ActionTooltip title="Action">
{(props, ref) => <MyCustomButton buttonProps={props} buttonRef={ref} />}
</ActionTooltip>;
```

## Controlled State

By default `ActionTooltip` opens and hides by hovering the anchor. You can change this behaviour to manually set the open state.
Pass your state to the `open` prop and change it from `onOpenChange` callback.
`onOpenChange` callback has the following signature: `(open: boolean, event?: Event, reason: 'hover' | 'focus') => void`.

## 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` |
| Name | Description | Type | Default |
| :----------- | --------------------------------------------------------------------------- | :----------------------------------------------: | :--------: |
| children | An anchor element for the `ActionTooltip` | `React.ReactElement` `Function` | |
| className | HTML class attribute | `string` | |
| closeDelay | Number of ms to delay hiding the `ActionTooltip` after the hover ends | `number` | `0` |
| description | Description content | `React.ReactNode` | |
| disabled | Prevent the `ActionTooltip` from opening | `boolean` | |
| hotkey | Hotkey value to be shown in the top-end corner | [`Hotkey` value](../Hotkey/README.md#value) | |
| offset | `ActionTooltip` offset from its anchor | `number` | `4` |
| onOpenChange | Callback to handle open state change | `Function` | |
| open | Controlled open state | `boolean` | |
| openDelay | Number of ms to delay showing the `ActionTooltip` after the hover begins | `number` | `1000` |
| placement | `ActionTooltip` position relative to its anchor | [`PopupPlacement`](../Popup/README.md#placement) | `bottom` |
| qa | HTML `data-qa` attribute, used in tests | `string` | |
| strategy | The type of CSS position property to use. | `absolute` `fixed` | `absolute` |
| style | HTML style attribute | `React.CSSProperties` | |
| title | Title content | `string` | |
| trigger | Event type that should trigger opening. By default both hover and focus do. | `"focus"` | |
50 changes: 39 additions & 11 deletions src/components/ActionTooltip/__stories__/ActionTooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,52 @@
import React from 'react';

import type {StoryFn} from '@storybook/react';
import {FloppyDisk} from '@gravity-ui/icons';
import type {Meta, StoryObj} from '@storybook/react';

import {Button} from '../../Button';
import {Icon} from '../../Icon';
import {ActionTooltip} from '../ActionTooltip';
import type {ActionTooltipProps} from '../ActionTooltip';

export default {
const meta: Meta<typeof ActionTooltip> = {
title: 'Components/Overlays/ActionTooltip',
component: ActionTooltip,
parameters: {
layout: 'centered',
},
};

const DefaultTemplate: StoryFn<ActionTooltipProps> = (args) => <ActionTooltip {...args} />;
export default meta;

export const Default = DefaultTemplate.bind({});
type Story = StoryObj<typeof ActionTooltip>;

Default.args = {
title: 'Tooltip text',
hotkey: 'mod+s',
description:
'Lorem ipsum is placeholder text commonly used in the graphic, print, and publishing industries for previewing layouts and visual mockups.',
children: <Button>Hover to see tooltip</Button>,
export const Default: Story = {
render: (args) => {
return (
<ActionTooltip {...args}>
<Button>
<Icon data={FloppyDisk} size={16} />
</Button>
</ActionTooltip>
);
},
args: {
title: 'Save',
},
};

export const Hotkey: Story = {
...Default,
args: {
...Default.args,
hotkey: 'mod+s',
},
};

export const Description: Story = {
...Default,
args: {
...Default.args,
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
},
};
Loading

0 comments on commit 1e14cb2

Please sign in to comment.