Skip to content

Commit

Permalink
Merge branch 'next' into table/style
Browse files Browse the repository at this point in the history
  • Loading branch information
ArturAbdullin authored Dec 22, 2023
2 parents be1370d + 56aa587 commit b801498
Show file tree
Hide file tree
Showing 43 changed files with 891 additions and 424 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();
});
12 changes: 6 additions & 6 deletions src/components/Alert/__snapshots__/Alert.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
exports[`Alert has predicted styles if inline layout rendered 1`] = `
<div>
<div
class="yc-card yc-card_theme_danger yc-card_view_filled yc-card_type_container yc-card_size_m yc-alert yc-s__py_4 yc-s__px_5"
class="yc-box yc-card yc-card_theme_danger yc-card_view_filled yc-card_type_container yc-card_size_m yc-alert yc-s__py_4 yc-s__px_5"
>
<div
class="yc-flex"
class="yc-box yc-flex"
style="column-gap: 12px; row-gap: 12px; align-items: center;"
>
<div
Expand Down Expand Up @@ -37,15 +37,15 @@ exports[`Alert has predicted styles if inline layout rendered 1`] = `
</svg>
</div>
<div
class="yc-flex"
class="yc-box yc-flex"
style="flex-direction: row; flex-grow: 1; column-gap: 20px; row-gap: 20px;"
>
<div
class="yc-flex yc-alert__text-content"
class="yc-box yc-flex yc-alert__text-content"
style="flex-grow: 1; column-gap: 8px; row-gap: 8px;"
>
<div
class="yc-flex"
class="yc-box yc-flex"
style="flex-direction: column; flex-grow: 1; column-gap: 4px; row-gap: 4px; justify-content: center;"
>
<span
Expand All @@ -56,7 +56,7 @@ exports[`Alert has predicted styles if inline layout rendered 1`] = `
</div>
</div>
<div
class="yc-flex yc-alert__actions yc-alert__actions_minContent"
class="yc-box yc-flex yc-alert__actions yc-alert__actions_minContent"
style="flex-direction: row; flex-wrap: wrap; column-gap: 12px; row-gap: 12px; align-items: center;"
>
<button
Expand Down
18 changes: 7 additions & 11 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import {useActionHandlers} from '../../hooks';
import type {QAProps} from '../types';
import {Box, BoxProps} from '../layout';
import {block} from '../utils/cn';

import './Card.scss';
Expand All @@ -16,10 +16,8 @@ export type CardTheme = 'normal' | 'info' | 'success' | 'warning' | 'danger' | '
export type CardView = SelectionCardView | ContainerCardView;
export type CardSize = 'm' | 'l';

export interface CardProps extends QAProps {
export interface CardProps extends Omit<BoxProps<'div'>, 'as' | 'onClick'> {
children: React.ReactNode;
style?: React.CSSProperties;
className?: string;
/** Card click handler. Available for type: 'selection', 'action' */
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
/** Disabled card. Available for type: 'selection', 'action' */
Expand Down Expand Up @@ -47,8 +45,7 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(function Card(pr
onClick,
disabled,
selected,
style,
qa,
...restProps
} = props;

const isTypeAction = type === 'action';
Expand All @@ -68,8 +65,7 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(function Card(pr
const {onKeyDown} = useActionHandlers(onClick);

return (
<div
style={style}
<Box
ref={ref}
role={isClickable ? 'button' : undefined}
className={b(
Expand All @@ -84,12 +80,12 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(function Card(pr
},
className,
)}
onClick={handleClick}
onClick={handleClick as BoxProps['onClick']}
onKeyDown={isClickable ? onKeyDown : undefined}
tabIndex={isClickable ? 0 : undefined}
data-qa={qa}
{...restProps}
>
{children}
</div>
</Box>
);
});
2 changes: 1 addition & 1 deletion src/components/CopyToClipboard/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './CopyToClipboard';
export {CopyToClipboardStatus} from './types';
export type {CopyToClipboardStatus} from './types';
Loading

0 comments on commit b801498

Please sign in to comment.