-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement new Popover draft (#1969)
- Loading branch information
Showing
5 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import React from 'react'; | ||
|
||
import { | ||
safePolygon, | ||
useClick, | ||
useDismiss, | ||
useFloatingRootContext, | ||
useHover, | ||
useInteractions, | ||
useRole, | ||
} from '@floating-ui/react'; | ||
|
||
import {useControlledState, useForkRef} from '../../../hooks'; | ||
import {Popup} from '../../Popup'; | ||
import type {PopupProps} from '../../Popup'; | ||
import type {DOMProps, QAProps} from '../../types'; | ||
import {block} from '../../utils/cn'; | ||
import {getElementRef} from '../../utils/getElementRef'; | ||
|
||
export interface PopoverProps | ||
extends QAProps, | ||
DOMProps, | ||
Pick< | ||
PopupProps, | ||
| 'middlewares' | ||
| 'strategy' | ||
| 'placement' | ||
| 'offset' | ||
| 'keepMounted' | ||
| 'disablePortal' | ||
| 'hasArrow' | ||
| 'contentClassName' | ||
| 'disableEscapeKeyDown' | ||
| 'disableOutsideClick' | ||
| 'disableLayer' | ||
> { | ||
children: React.ReactElement; | ||
open?: boolean; | ||
onOpenChange?: (open: boolean) => void; | ||
disabled?: boolean; | ||
content?: React.ReactNode; | ||
trigger?: 'click'; | ||
delay?: number | {open?: number; close?: number}; | ||
enableSafePolygon?: boolean; | ||
} | ||
|
||
const b = block('popover2'); | ||
const DEFAULT_DELAY = 500; | ||
|
||
export function Popover({ | ||
children, | ||
open, | ||
onOpenChange, | ||
disabled, | ||
content, | ||
trigger, | ||
delay = DEFAULT_DELAY, | ||
enableSafePolygon, | ||
className, | ||
contentClassName, | ||
disableEscapeKeyDown, | ||
disableOutsideClick, | ||
...restProps | ||
}: PopoverProps) { | ||
const child = React.Children.only(children); | ||
const childRef = getElementRef(child); | ||
|
||
const [anchorElement, setAnchorElement] = React.useState<HTMLButtonElement | null>(null); | ||
const [floatingElement, setFloatingElement] = React.useState<HTMLDivElement | null>(null); | ||
const anchorRef = useForkRef(setAnchorElement, childRef); | ||
|
||
const [isOpen, setIsOpen] = useControlledState(open, false, onOpenChange); | ||
|
||
const context = useFloatingRootContext({ | ||
open: isOpen, | ||
onOpenChange: setIsOpen, | ||
elements: { | ||
reference: anchorElement, | ||
floating: floatingElement, | ||
}, | ||
}); | ||
|
||
const hover = useHover(context, { | ||
enabled: !disabled && trigger !== 'click', | ||
delay: | ||
typeof delay === 'number' | ||
? delay | ||
: {open: delay.open ?? DEFAULT_DELAY, close: delay.close ?? DEFAULT_DELAY}, | ||
move: false, | ||
handleClose: enableSafePolygon ? safePolygon() : undefined, | ||
}); | ||
const click = useClick(context, {enabled: !disabled}); | ||
const dismiss = useDismiss(context, { | ||
escapeKey: !disableEscapeKeyDown, | ||
outsidePress: !disableOutsideClick, | ||
}); | ||
const role = useRole(context, {role: 'dialog'}); | ||
|
||
const {getReferenceProps, getFloatingProps} = useInteractions([hover, click, dismiss, role]); | ||
|
||
return ( | ||
<React.Fragment> | ||
{React.cloneElement(child, {ref: anchorRef, ...getReferenceProps(child.props)})} | ||
<Popup | ||
{...restProps} | ||
open={isOpen} | ||
floatingContext={context} | ||
floatingRef={setFloatingElement} | ||
floatingProps={getFloatingProps()} | ||
className={b(null, className)} | ||
contentClassName={b('content', contentClassName)} | ||
> | ||
{content} | ||
</Popup> | ||
</React.Fragment> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<!--GITHUB_BLOCK--> | ||
|
||
# Popover | ||
|
||
<!--/GITHUB_BLOCK--> | ||
|
||
```tsx | ||
import {Popover} from '@gravity-ui/uikit'; | ||
``` | ||
|
||
The `Popover` component is technically the [`Popup`](./TODO) with some trigger interactivity built-in. The `Popover` uses passed `ReactElement` | ||
from `children` property as a trigger, and opens whenever trigger is hovered or clicked. Content of the `Popover` might contain | ||
interactive elements like links or buttons. | ||
|
||
## Usage | ||
|
||
Wrap HTML element or any component that accepts native DOM handlers and ARIA attributes in properties (i.e. `Button`) with `Popover` component. Put your content | ||
into `content` property. | ||
|
||
```jsx | ||
import React from 'react'; | ||
import {Button, Popover} from '@gravity-ui/uikit'; | ||
|
||
<Popover content="Content"> | ||
<Button>Click or hover me</Button> | ||
</Popover>; | ||
``` | ||
|
||
## Properties | ||
|
||
| Name | Description | Type | Default | | ||
| :------------------- | :------------------------------------------------------------------------------------------------------------ | :-----------------------------------------------------------------: | :----------: | | ||
| children | `ReactNode` which accepts DOM handlers | `React.ReactNode` | | | ||
| className | HTML `class` attribute for root node | `string` | | | ||
| content | Any content to render inside the `Popover` | `React.ReactNode` | | | ||
| contentClassName | HTML `class` attribute for content node | `string` | | | ||
| delay | Wait specified time in milliseconds before changing `open` state | `number` `{open?: number; close?: number}` | | | ||
| disableEscapeKeyDown | Do not dismiss on `Esc` keydown | `boolean` | `false` | | ||
| disableLayer | Do not use `LayerManager` on stacking floating elements | `boolean` | `false` | | ||
| disableOutsideClick | Do not dismiss on outside click | `boolean` | `false` | | ||
| disablePortal | Do not use `Portal` for children | `boolean` | `false` | | ||
| disabled | Do not open on any event | `boolean` | `false` | | ||
| enableSafePolygon | Use dynamic polygon area when moving the pointer from trigger to `Popover` content to prevent it from closing | `boolean` | `false` | | ||
| hasArrow | Render an arrow pointing to the trigger | `boolean` | `false` | | ||
| keepMounted | `Popover` will not be removed from the DOM upon hiding | `boolean` | `false` | | ||
| middlewares | `Floating UI` middlewares. If set, they will completely overwrite the default middlewares. | `Array<Middleware>` | | | ||
| offset | `Floating UI` offset value | `PopoverOffset` | `4` | | ||
| onOpenChange | Function that is called when the `open` state changes | `Function` | | | ||
| open | Manually control the `open` state | `boolean` | | | ||
| placement | `Floating UI` placement | `Placement` `Array<Placement>` `"auto"` `"auto-start"` `"auto-end"` | `"top"` | | ||
| qa | Test attribute (`data-qa`) | `string` | | | ||
| strategy | `Floating UI` positioning strategy | `"absolute"` `"fixed"` | `"absolute"` | | ||
| style | HTML `style` attribute for root node | `string` | | | ||
| trigger | Which event should open the `Popover`. By default, `click` and `hover` both do | `"click"` | | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import {Meta, Markdown} from '@storybook/addon-docs'; | ||
import * as Stories from './Popover.stories'; | ||
import Readme from '../README.md?raw'; | ||
|
||
<Meta of={Stories} /> | ||
|
||
<Markdown>{Readme}</Markdown> |
74 changes: 74 additions & 0 deletions
74
src/components/lab/Popover/__stories__/Popover.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import React from 'react'; | ||
|
||
import {action} from '@storybook/addon-actions'; | ||
import type {Meta, StoryObj} from '@storybook/react'; | ||
|
||
import {Button} from '../../../Button'; | ||
import {Flex} from '../../../layout'; | ||
import {Popover} from '../Popover'; | ||
|
||
const meta: Meta<typeof Popover> = { | ||
title: 'Lab/Popover', | ||
component: Popover, | ||
parameters: { | ||
layout: 'centered', | ||
}, | ||
}; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof Popover>; | ||
|
||
export const Default: Story = { | ||
render: (args) => ( | ||
<Popover {...args}> | ||
<Button>Anchor</Button> | ||
</Popover> | ||
), | ||
args: { | ||
content: <div style={{padding: 10}}>Content</div>, | ||
onOpenChange: action('onOpenChange'), | ||
}, | ||
}; | ||
|
||
export const Delay: Story = { | ||
render: (args) => ( | ||
<Flex gap={3}> | ||
<Popover {...args} delay={{open: 1000}}> | ||
<Button>Open Delay: 1000ms</Button> | ||
</Popover> | ||
<Popover {...args} delay={{close: 2000}}> | ||
<Button>Close Delay: 2000ms</Button> | ||
</Popover> | ||
</Flex> | ||
), | ||
args: { | ||
...Default.args, | ||
}, | ||
}; | ||
|
||
export const OnlyClick: Story = { | ||
...Default, | ||
args: { | ||
...Default.args, | ||
trigger: 'click', | ||
}, | ||
}; | ||
|
||
export const Disabled: Story = { | ||
...Default, | ||
args: { | ||
...Default.args, | ||
disabled: true, | ||
}, | ||
}; | ||
|
||
export const SafePolygon: Story = { | ||
...Default, | ||
args: { | ||
...Default.args, | ||
delay: 0, | ||
offset: 50, | ||
enableSafePolygon: true, | ||
}, | ||
}; |