diff --git a/src/components/ActionTooltip/ActionTooltip.scss b/src/components/ActionTooltip/ActionTooltip.scss index 38742e3995..efa726b514 100644 --- a/src/components/ActionTooltip/ActionTooltip.scss +++ b/src/components/ActionTooltip/ActionTooltip.scss @@ -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; } diff --git a/src/components/ActionTooltip/ActionTooltip.tsx b/src/components/ActionTooltip/ActionTooltip.tsx index 29f8ec2cbb..1d13cc670e 100644 --- a/src/components/ActionTooltip/ActionTooltip.tsx +++ b/src/components/ActionTooltip/ActionTooltip.tsx @@ -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 ( - + const [anchorElement, setAnchorElement] = React.useState(null); + const tooltipVisible = useTooltipVisible(anchorElement, delayProps); + + const renderPopup = () => { + return ( + +
{title}
{hotkey && }
{description &&
{description}
} - - } - > - {children} - +
+
+ ); + }; + + const child = React.Children.only(children); + const childRef = (child as any).ref; + + const ref = useForkRef(setAnchorElement, childRef); + + return ( + + {React.cloneElement(child, {ref})} + {anchorElement ? renderPopup() : null} + ); } diff --git a/src/components/ActionTooltip/README.md b/src/components/ActionTooltip/README.md new file mode 100644 index 0000000000..5b40131568 --- /dev/null +++ b/src/components/ActionTooltip/README.md @@ -0,0 +1,35 @@ + + +# ActionTooltip + + + +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'; + + +
Anchor
+
; +``` + +## 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` | diff --git a/src/components/ActionTooltip/__tests__/ActionTooltip.tsx b/src/components/ActionTooltip/__tests__/ActionTooltip.tsx new file mode 100644 index 0000000000..d443fb5cd5 --- /dev/null +++ b/src/components/ActionTooltip/__tests__/ActionTooltip.tsx @@ -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( + + +
); }, diff --git a/src/components/Tooltip/__tests__/Tooltip.test.tsx b/src/components/Tooltip/__tests__/Tooltip.test.tsx index 4b7846b78b..68e83b4155 100644 --- a/src/components/Tooltip/__tests__/Tooltip.test.tsx +++ b/src/components/Tooltip/__tests__/Tooltip.test.tsx @@ -49,7 +49,7 @@ test('should show tooltip on hover and hide on un hover', async () => { expect(tooltip).not.toBeInTheDocument(); }); -test('should show tooltip on focus and hide on blur', async () => { +test('should not show tooltip on focus', async () => { const user = userEvent.setup(); render( @@ -62,15 +62,8 @@ test('should show tooltip on focus and hide on blur', async () => { await user.tab(); expect(button).toHaveFocus(); - const tooltip = await screen.findByRole('tooltip'); - - expect(tooltip).toBeVisible(); - - await user.tab(); + const tooltip = screen.queryByRole('tooltip'); - fireAnimationEndEvent(tooltip); - - expect(button).not.toHaveFocus(); expect(tooltip).not.toBeInTheDocument(); }); @@ -85,7 +78,7 @@ test('should hide on press Escape', async () => { const button = await screen.findByRole('button'); await user.tab(); - expect(button).toHaveFocus(); + await user.hover(button); const tooltip = await screen.findByRole('tooltip'); @@ -98,31 +91,3 @@ test('should hide on press Escape', async () => { expect(button).toHaveFocus(); expect(tooltip).not.toBeInTheDocument(); }); - -test('should show on focus and hide on un hover', async () => { - const user = userEvent.setup(); - render( - -