Skip to content

Commit

Permalink
[BREAKING CHANGE] feat(Popover): mv to stable (#6129)
Browse files Browse the repository at this point in the history
🎉 Теперь экспортируется как стабильный компонент.

```diff
<Popover
- action="hover"
+ trigger="hover"
- offsetDistance={0}
+ offsetByMainAxis={0}
- offsetSkidding={0}
+ offsetByCrossAxis={0}
- shownDelay={0}
- hideDelay={10}
+ hoverDelay={[0, 10]}
>
  <div>Target</div>
</Popover>
```

- `trigger` – помимо `"click"` и `"hover"`, теперь принимает `"focus"` или комбинацию этих событий.
  Также можно передать `"manual"`, что сделает компонент полностью контролируемым, в `onShownChange`
  будет вызываться при нажатии за пределы целевого и всплывающего элементов, по кнопке ESC или при
  вызове `onClose` из свойства `content`.
- `content` теперь принимает [render prop](https://react.dev/reference/react/cloneElement#passing-data-with-a-render-prop).
  В аргументе функции можно получить метод `onClose`, с помощью которого можно программно закрывать
  всплывающий элемента.
- `onShownChange` – вторым аргументом теперь приходит `reason`, который даёт понять по какой причине
  показался/скрылся всплывающий элемент.
- `hoverDelay` – принимает либо общее число задержки для `trigger="hover"`, либо массив чисел типа
  `[<показ>, <скрытие>]`.
- `autoFocus` – включать ли авто-фокусирование на всплывающий элемент (работает при навигации с клавиатуры).
- `noStyling` – убирает стилизацию по умолчанию.
- `usePortal` – рендерить ли всплывающий элемент в портале. Вместо `boolean`, можно передать
  контейнер, куда должен отрендериться всплывающий элемент.
  • Loading branch information
inomdzhon authored Dec 1, 2023
1 parent 957c891 commit b03d1d5
Show file tree
Hide file tree
Showing 13 changed files with 687 additions and 220 deletions.
16 changes: 4 additions & 12 deletions packages/vkui/src/components/Popover/Popover.module.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
.Popover {
position: relative;
animation: popover-fade-in 0.2s ease;
background: var(--vkui--color_background_modal);
border-radius: var(--vkui--size_border_radius--regular);
box-shadow: var(--vkui--elevation3);
}

/* Создаём "Safe Zone" */
Expand All @@ -17,12 +13,8 @@
position: relative;
}

@keyframes popover-fade-in {
from {
opacity: 0;
}

to {
opacity: 1;
}
.Popover__in--withStyling {
background-color: var(--vkui--color_background_modal);
border-radius: var(--vkui--size_border_radius--regular);
box-shadow: var(--vkui--elevation3);
}
178 changes: 156 additions & 22 deletions packages/vkui/src/components/Popover/Popover.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import * as React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Icon16Clear, Icon28AddOutline, Icon28DeleteOutline } from '@vkontakte/icons';
import { DisableCartesianParam } from '../../storybook/constants';
import { getAvatarUrl } from '../../testing/mock';
import { Avatar } from '../Avatar/Avatar';
import { Button } from '../Button/Button';
import { CellButton } from '../CellButton/CellButton';
import { Checkbox } from '../Checkbox/Checkbox';
import { Div } from '../Div/Div';
import { FormItem } from '../FormItem/FormItem';
import { FormLayout } from '../FormLayout/FormLayout';
import { Group } from '../Group/Group';
import { IconButton } from '../IconButton/IconButton';
import { Input } from '../Input/Input';
import { Text } from '../Typography/Text/Text';
import { Popover, PopoverProps } from './Popover';
import { Popover, type PopoverOnShownChange, type PopoverProps } from './Popover';

const story: Meta<PopoverProps> = {
title: 'Poppers/Popover',
Expand All @@ -23,62 +29,190 @@ type Story = StoryObj<PopoverProps>;
export const Playground: Story = {
render: (args) => (
<Popover
action="hover"
placement="right"
trigger="hover"
placement="bottom"
role="tooltip"
aria-describedby="tooltip-1"
content={
<Div>
<Text>Привет</Text>
</Div>
}
{...args}
>
<Button style={{ margin: 20 }}>Наведи</Button>
<Button id="tooltip-1" mode="outline">
Наведи на меня
</Button>
</Popover>
),
};

export const Example: Story = {
render: function Render() {
const [shown, setShown] = React.useState(true);

return (
<>
const PopoverWithTriggerHover = () => {
return (
<Popover
action="hover"
placement="right"
trigger="hover"
placement="bottom"
role="tooltip"
aria-describedby="tooltip-1"
content={
<Div>
<Text>Привет</Text>
</Div>
}
>
<Button style={{ margin: 20 }}>Наведи</Button>
<Button id="tooltip-1" mode="outline">
Наведи на меня
</Button>
</Popover>
);
};

const PopoverWithTriggerClick = () => {
return (
<Popover
action="click"
shown={shown}
onShownChange={setShown}
content={
noStyling
trigger="click"
id="menupopup"
role="menu"
aria-labelledby="menubutton"
content={({ onClose }) => (
<Group>
<CellButton role="menuitem" before={<Icon28AddOutline />} onClick={onClose}>
Добавить
</CellButton>
<CellButton
role="menuitem"
before={<Icon28DeleteOutline />}
mode="danger"
onClick={onClose}
>
Удалить
</CellButton>
</Group>
)}
>
<Button id="menubutton" aria-controls="menupopup" aria-haspopup="true" mode="outline">
Нажми на меня
</Button>
</Popover>
);
};

const PopoverWithTriggerFocus = () => {
return (
<Popover
trigger="focus"
role="dialog"
aria-describedby="dialog-2"
content={({ onClose }) => (
<FormLayout>
<FormItem htmlFor="name" top="Имя">
<Input id="name" />
<FormItem top="Имя">
<Input />
</FormItem>
<FormItem htmlFor="lastname" top="Фамилия">
<Input id="lastname" />
<FormItem top="Фамилия">
<Input />
</FormItem>
<FormItem top="Соглашение">
<Checkbox name="agreement">Согласен</Checkbox>
</FormItem>
<FormItem>
<Button onClick={() => setShown(false)}>Отправить</Button>
<Button onClick={onClose}>Отправить</Button>
</FormItem>
</FormLayout>
)}
>
<Button id="dialog-2" mode="outline">
Сфокусируйся на меня через Tab (или клик)
</Button>
</Popover>
);
};

const PopoverWithAllTriggers = () => {
return (
<Popover
trigger={['click', 'hover', 'focus']}
placement="right"
role="tooltip"
aria-describedby="tooltip-3"
content={
<Div>
<Avatar src={getAvatarUrl('app_promokot')} alt="Cat" />
</Div>
}
>
<Button style={{ margin: '20px 0 0 0' }}>Кликни</Button>
<Button id="tooltip-3" mode="outline">
Нажми или наведи или сфокусируйся на меня
</Button>
</Popover>
</>
);
};

const PopoverWithTriggerManual = () => {
const [shown, setShown] = React.useState(false);

const handleShownChange: PopoverOnShownChange = React.useCallback((value, reason) => {
if (!value) {
switch (reason) {
case 'callback':
case 'escape-key':
case 'click-outside':
setShown(false);
break;
default:
break;
}
}
}, []);

return (
<Popover
trigger="manual"
shown={shown}
role="dialog"
aria-describedby="dialog-3"
content={({ onClose }) => (
<div style={{ display: 'flex', position: 'relative', width: 180, height: 100 }}>
<div style={{ position: 'absolute', top: 0, right: 0 }}>
<IconButton aria-label="Close dialog" onClick={onClose}>
<Icon16Clear />
</IconButton>
</div>
<div style={{ margin: 'auto', textAlign: 'center' }}>
The cake
<br />
is
<br />a lie
</div>
</div>
)}
onShownChange={handleShownChange}
>
<Button id="dialog-3" onClick={() => setShown((prev) => !prev)}>
Я переключаю состояние через useState
</Button>
</Popover>
);
};

return (
<div
style={{
display: 'flex',
padding: 16,
gap: 16,
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<PopoverWithTriggerHover />
<PopoverWithTriggerClick />
<PopoverWithTriggerFocus />
<PopoverWithAllTriggers />
<PopoverWithTriggerManual />
</div>
);
},
};
51 changes: 51 additions & 0 deletions packages/vkui/src/components/Popover/Popover.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { baselineComponent, waitForFloatingPosition } from '../../testing/utils';
import { Popover, type PopoverProps } from './Popover';

describe(Popover, () => {
baselineComponent((props) => (
<Popover defaultShown {...props}>
<div>Test</div>
</Popover>
));

it('should provide zIndex to popover element', async () => {
const result = render(
<Popover
defaultShown
content="Some popover"
aria-describedby="target"
role="tooltip"
data-testid="popover"
zIndex="100500"
>
<div id="target">Target</div>
</Popover>,
);
await waitForFloatingPosition();
expect(result.getByTestId('popover').parentElement).toHaveStyle('z-index: 100500');
});

it('should injects aria-expanded attr to target element if correct role provided', async () => {
const Fixture = ({ shown }: PopoverProps) => (
<Popover
shown={shown}
id="menu"
role="menu"
aria-labelledby="target"
content={<div role="menuitem">1</div>}
>
<div id="target" aria-haspopup="true" aria-controls="menu" data-testid="target">
Target
</div>
</Popover>
);
const result = render(<Fixture shown />);
await waitForFloatingPosition();
expect(result.getByTestId('target')).toHaveAttribute('aria-expanded', 'true');
result.rerender(<Fixture shown={false} />);
await waitForFloatingPosition();
expect(result.getByTestId('target')).toHaveAttribute('aria-expanded', 'false');
});
});
Loading

0 comments on commit b03d1d5

Please sign in to comment.