Skip to content

Commit

Permalink
feat!: refactor components to use Floating UI API (#1979)
Browse files Browse the repository at this point in the history
Co-authored-by: oynikishin <[email protected]>
Co-authored-by: oynikishin <[email protected]>
  • Loading branch information
3 people committed Dec 27, 2024
1 parent b41e9ce commit 8f218d8
Show file tree
Hide file tree
Showing 64 changed files with 1,674 additions and 1,709 deletions.
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 * as 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)}
/>
);
}
57 changes: 41 additions & 16 deletions src/components/ActionTooltip/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,56 @@

<!--/GITHUB_BLOCK-->

This is a simple text tip that uses its child node as an anchor. To work correctly, 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 | Anchor element for a `Tooltip`. It 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` once the hover starts | `number` | `250` |
| placement | `Tooltip` position relative to its anchor | [`PopupPlacement`](../Popup/README.md#placement) | |
| qa | `data-qa` HTML attribute, used for testing | `string` | |
| title | Tooltip title text | `string` | |
| description | Tooltip description text | `string` | |
| hotkey | Hotkeys assigned to an interface action | `string` | |
| id | Used for implementing the accessibility logic | `string` | |
| disablePortal | Disables using Portal for children | `boolean` | |
| contentClassName | HTML class attribute for the content node | `string` | |
| disabled | Prevents the popup from opening | `boolean` | `false` |
| Name | Description | Type | Default |
| :----------- | --------------------------------------------------------------------------- | :----------------------------------------------: | :--------: |
| children | Anchor element for the `ActionTooltip` | `React.ReactElement` `Function` | |
| className | `class` HTML 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 | `data-qa` HTML attribute, used for testing | `string` | |
| strategy | The type of CSS position property to use. | `absolute` `fixed` | `absolute` |
| style | `style` HTML attribute | `React.CSSProperties` | |
| title | Title content | `string` | |
| trigger | Event type that should trigger opening. By default both hover and focus do. | `"focus"` | |
62 changes: 51 additions & 11 deletions src/components/ActionTooltip/__stories__/ActionTooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,62 @@
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',
a11y: {
element: '#storybook-root',
config: {
rules: [
{
id: 'button-name',
// We set aria-attributes dynamically
enabled: false,
},
],
},
},
},
};

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 8f218d8

Please sign in to comment.