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!: refactor components to use Floating UI API #1979

Draft
wants to merge 8 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
22 changes: 7 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,11 @@
},
"dependencies": {
"@bem-react/classname": "^1.6.0",
"@floating-ui/react": "^0.26.28",
"@floating-ui/react": "^0.27.0",
"@gravity-ui/i18n": "^1.7.0",
"@gravity-ui/icons": "^2.11.0",
"@tanstack/react-virtual": "^3.10.8",
"blueimp-md5": "^2.19.0",
"focus-trap": "^7.6.2",
"lodash": "^4.17.21",
"rc-slider": "^11.1.7",
"react-beautiful-dnd": "^13.1.1",
Expand Down
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);
}
}
112 changes: 42 additions & 70 deletions src/components/ActionTooltip/ActionTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +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
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>}
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"` | |
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
Loading