diff --git a/package.json b/package.json index 727bcd454c..766d3a3e74 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,16 @@ "default": "./build/cjs/i18n/index.js" } }, + "./legacy": { + "import": { + "types": "./build/esm/legacy.d.ts", + "default": "./build/esm/legacy.js" + }, + "require": { + "types": "./build/cjs/legacy.d.ts", + "default": "./build/cjs/legacy.js" + } + }, "./unstable": { "import": { "types": "./build/esm/unstable.d.ts", diff --git a/src/components/HelpMark/HelpMark.tsx b/src/components/HelpMark/HelpMark.tsx index 730f29397a..8cf373667b 100644 --- a/src/components/HelpMark/HelpMark.tsx +++ b/src/components/HelpMark/HelpMark.tsx @@ -3,8 +3,8 @@ import type * as React from 'react'; import {CircleQuestion} from '@gravity-ui/icons'; import {Icon} from '../Icon'; -import {Popover} from '../Popover'; import type {PopupPlacement} from '../Popup'; +import {Popover} from '../legacy'; import type {QAProps} from '../types'; import {block} from '../utils/cn'; diff --git a/src/components/Popover/Popover.classname.ts b/src/components/Popover/Popover.classname.ts deleted file mode 100644 index 517754e74f..0000000000 --- a/src/components/Popover/Popover.classname.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {block} from '../utils/cn'; - -export const cnPopover = block('popover'); diff --git a/src/components/Popover/Popover.tsx b/src/components/Popover/Popover.tsx index 36410501d6..0022c76885 100644 --- a/src/components/Popover/Popover.tsx +++ b/src/components/Popover/Popover.tsx @@ -1,249 +1,130 @@ -'use client'; - import * as React from 'react'; -import {Xmark} from '@gravity-ui/icons'; +import { + safePolygon, + useClick, + useFloatingRootContext, + useHover, + useInteractions, +} from '@floating-ui/react'; +import type {UseInteractionsReturn} from '@floating-ui/react'; -import {useUniqId} from '../../hooks/useUniqId'; -import {Button} from '../Button'; -import {Icon} from '../Icon'; -import type {PopupPlacement} from '../Popup'; +import {useControlledState, useForkRef} from '../../hooks'; import {Popup} from '../Popup'; -import {useDirection} from '../theme'; -import type {QAProps} from '../types'; -import {warnOnce} from '../utils/warn'; - -import {cnPopover} from './Popover.classname'; -import {Buttons} from './components/Buttons/Buttons'; -import {Content} from './components/Content/Content'; -import {Links} from './components/Links/Links'; -import {Trigger} from './components/Trigger/Trigger'; -import {PopoverBehavior} from './config'; -import {useOpen} from './hooks/useOpen'; -import type {PopoverInstanceProps, PopoverProps} from './types'; - -import './Popover.scss'; - -export const Popover = React.forwardRef(function ( - { - initialOpen = false, - disabled = false, - autoclosable = true, - openOnHover = true, - delayOpening, - delayClosing, - behavior = PopoverBehavior.Delayed, - placement, - offset = {}, - tooltipOffset, - tooltipClassName, - theme = 'info', - size = 's', - hasArrow = true, - hasClose = false, - className, - children, - title, - content, - htmlContent, - contentClassName, - links, - forceLinksAppearance = false, - tooltipActionButton, - tooltipCancelButton, - onOpenChange, - onCloseClick, - onClick, - anchorRef, - anchorElement, - strategy, - qa, - disablePortal = false, - tooltipId, - focusTrap, - autoFocus, - restoreFocusRef, - }, - ref, -) { - const direction = useDirection(); - const controlRef = React.useRef(null); - const closedManually = React.useRef(false); - const shouldBeOpen = React.useRef(initialOpen); - - const { - isOpen, - closingTimeout, - openTooltip, - openTooltipDelayed, - unsetOpeningTimeout, - closeTooltip, - closeTooltipDelayed, - unsetClosingTimeout, - } = useOpen({ - initialOpen, - disabled, - autoclosable, - onOpenChange, - delayOpening, - delayClosing, - behavior, - shouldBeOpen, - }); - - const popupPlacement = React.useMemo(() => { - if (placement) { - return placement; - } - - return direction === 'rtl' ? ['left', 'bottom'] : ['right', 'bottom']; - }, [direction, placement]); - - React.useImperativeHandle( - ref, - () => ({ - openTooltip, - closeTooltip, - }), - [openTooltip, closeTooltip], +import type {PopupProps} from '../Popup'; +import type {AriaLabelingProps, DOMProps, QAProps} from '../types'; +import {block} from '../utils/cn'; +import {getElementRef} from '../utils/getElementRef'; + +export interface PopoverProps + extends AriaLabelingProps, + QAProps, + DOMProps, + Pick< + PopupProps, + | 'strategy' + | 'placement' + | 'offset' + | 'keepMounted' + | 'hasArrow' + | 'initialFocus' + | 'returnFocus' + | 'disableFocusVisuallyHiddenDismiss' + > { + children: + | ((props: Record, ref: React.Ref) => React.ReactElement) + | React.ReactElement; + open?: boolean; + onOpenChange?: (open: boolean) => void; + disabled?: boolean; + content?: React.ReactNode; + trigger?: 'click'; + openDelay?: number; + closeDelay?: number; + enableSafePolygon?: boolean; +} + +const b = block('popover2'); +const DEFAULT_OPEN_DELAY = 500; +const DEFAULT_CLOSE_DELAY = 250; + +export function Popover({ + children, + open, + onOpenChange, + disabled, + content, + trigger, + openDelay = DEFAULT_OPEN_DELAY, + closeDelay = DEFAULT_CLOSE_DELAY, + enableSafePolygon, + className, + ...restProps +}: PopoverProps) { + const [anchorElement, setAnchorElement] = React.useState(null); + const [floatingElement, setFloatingElement] = React.useState(null); + const [getAnchorProps, setGetAnchorProps] = + React.useState(); + + const handleSetGetAnchorProps = React.useCallback>( + (getAnchorPropsFn) => { + setGetAnchorProps(() => getAnchorPropsFn); + }, + [], ); - const handleCloseClick = async (event: React.MouseEvent) => { - closeTooltip(); - onCloseClick?.(event); - }; + const [isOpen, setIsOpen] = useControlledState(open, false, onOpenChange); - const hasTitle = Boolean(title); + const context = useFloatingRootContext({ + open: isOpen, + onOpenChange: setIsOpen, + elements: { + reference: anchorElement, + floating: floatingElement, + }, + }); - const hasAnchor = Boolean(anchorRef || anchorElement); + const hover = useHover(context, { + enabled: !disabled && trigger !== 'click', + delay: {open: openDelay, close: closeDelay}, + move: false, + handleClose: enableSafePolygon ? safePolygon() : undefined, + }); + const click = useClick(context, {enabled: !disabled}); - const popoverTitleId = `popover-${tooltipId ?? ''}-title-${useUniqId()}`; + const {getReferenceProps, getFloatingProps} = useInteractions([hover, click]); - const tooltip = ( - - - {title && ( -

- {title} -

- )} - - {links && } - - {hasClose && ( -
- -
- )} -
-
+ const anchorRef = useForkRef( + setAnchorElement, + React.isValidElement(children) ? getElementRef(children) : undefined, ); - - if (hasAnchor) { - return tooltip; - } - - const onMouseEnter = () => { - unsetClosingTimeout(); - - if (!isOpen && !disabled && !closedManually.current) { - openTooltipDelayed(); - } else { - shouldBeOpen.current = true; - } - }; - - const onMouseLeave = () => { - if (autoclosable && !closedManually.current && !closingTimeout.current) { - unsetOpeningTimeout(); - closeTooltipDelayed(); - } else { - shouldBeOpen.current = false; - } - - closedManually.current = false; - }; - - if (offset && (typeof offset.top === 'number' || typeof offset.left === 'number')) { - warnOnce( - '[Popover] Physical names (top, left) of "offset" property are deprecated. Use logical names (block, inline) instead.', - ); - } + const anchorProps = React.isValidElement(children) + ? getReferenceProps(getAnchorProps?.(children.props) ?? children.props) + : getReferenceProps(getAnchorProps?.()); + const anchorNode = React.isValidElement(children) + ? React.cloneElement(children, { + ref: anchorRef, + ...anchorProps, + }) + : children(anchorProps, anchorRef); return ( -
- + {anchorNode} + - {children} - - {tooltip} -
+ {content} + + ); -}); - -Popover.displayName = 'Popover'; +} diff --git a/src/components/Popover/README.md b/src/components/Popover/README.md index 5d047f8bcd..8f77a8b8c1 100644 --- a/src/components/Popover/README.md +++ b/src/components/Popover/README.md @@ -8,293 +8,46 @@ import {Popover} from '@gravity-ui/uikit'; ``` -This component allows you to add a section with some pop-up content. +The `Popover` component is technically the [`Popup`](../Popup/README.md) 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. -### Simple usage +## 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. - - -```tsx -Open a tooltip -``` - - - -### With jsx content - - - - - -```tsx -}>Open a tooltip -``` - - - -### With html content - - - - - -```tsx -html content. Learn more here' - } -> - Open a tooltip - -``` - - - -### With links - - - - - -```tsx - alert('The link is clicked'), - }, - ]} -> - Open a tooltip - -``` - - +```jsx +import {Button, Popover} from '@gravity-ui/uikit'; -### With action button - - - - - -```tsx - console.log('Action button was clicked'), - }} -> - Open a tooltip - + + +; ``` - - -### With automatic closing when the cursor is outside for `delayClosing` - - - - - -```tsx - console.log('Action button was clicked'), - }} -> - Open a tooltip - -``` - - - -## Instance usage - -```tsx -import {Popover, PopoverInstanceProps} from '@gravity-ui/uikit'; - -const popoverRef = useRef(); - -const open = () => { - popoverRef.current?.openTooltip(); -}; - -const close = () => { - popoverRef.current?.closeTooltip(); -}; - -<> - - - -; -``` - -### Instance properties - -| Name | Description | Type | Default | -| ------------ | ------------------------------- | :--------: | :-----: | -| openTooltip | Opens the `() => void` tooltip | `Function` | | -| closeTooltip | Closes the `() => void` tooltip | `Function` | | - ## Properties -| Name | Description | Type | Default | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------: | :-------------------: | -| anchorRef | `Popup` anchor element that can also be `VirtualElement` | [`PopupAnchorRef`](../Popup/README.md#anchor) | | -| autoclosable | Enables or disables closing the tooltip automatically when the cursor moves outside it | `boolean` | `true` | -| autoFocus | If true, the focus will be transferred to the first element when the popover opens | `boolean` | | -| behavior | Tooltip open or close behavior with `openOnHover`. `"immediate"`: without any delay, `"delayed"`: with 300ms delay for opening and closing, `"delayedClosing"`: with 300ms delay only for closing. This property will not apply in case `delayOpening` or `delayClosing` are provided. | `"immediate"` `"delayed"` `"delayedClosing"` | `"delayed"` | -| children | Tooltip's trigger content over which the tooltip is shown. It can either be the `(triggerProps: `[`TriggerProps`](#triggerprops))` => React.ReactNode` function or `ReactNode`. | `React.ReactNode` `Function` | | -| className | CSS class for the control | `string` | | -| content | Tooltip content | `React.ReactNode` | | -| contentClassName | CSS class for `content` | `string` | | -| delayClosing | Custom delay for closing if `autoclosable` | `number` | | -| delayOpening | Custom delay for opening if `openOnHover` | `number` | | -| disabled | Disables open state changes | `boolean` | `false` | -| disablePortal | Disable rendering of the popover in a portal | `boolean` | `false` | -| focusTrap | Prevents focus from leaving the popover while open | `boolean` | | -| forceLinksAppearance | Forces styles for links | `boolean` | `false` | -| hasArrow | Enables or disables a tooltip arrow | `boolean` | `true` | -| hasClose | Enables or disables a close button for a tooltip | `boolean` | `false` | -| htmlContent | Tooltip's HTML content to render via `dangerouslySetInnerHTML` | `string` | | -| initialOpen | Enables or disables the tooltip initial opening | `boolean` | `false` | -| links | Links under the content | `[`[`LinkProps`](#linksprops)`]` | | -| offset | Control's offset | `{top: number, left: number}` | | -| onClick | Anchor click callback: `(event: React.MouseEvent) => boolean \| Promise`. If the function returns `true, the tooltip will be open; otherwise, it won't open. | `Function` | | -| onCloseClick | Close button click handler: `(event: React.MouseEvent) => void` | `Function` | | -| onOpenChange | Open state change handler: `(open: boolean) => void`. Might be useful for the delayed rendering of the tooltip content. | `Function` | | -| openOnHover | Enables or disables opening the tooltip when hovered | `boolean` | `true` | -| placement | `Popup` placement | [`PopupPlacement`](../Popup/README.md#placement) | `["right", "bottom"]` | -| qa | `data-qa` HTML attribute, used for testing | `string` | | -| restoreFocusRef | Focused element when the popover closes | `React.RefObject` | | -| size | Tooltip size | `"s"` `"l"` | `"s"` | -| strategy | `Floating UI` positioning [strategy](https://floating-ui.com/docs/computePosition#strategy) | `"absolute"` `"fixed"` | `"absolute"` | -| title | Tooltip title | `string` | | -| theme | Tooltip theme | `"info"` `"special"` `"announcement"` | `"info"` | -| tooltipActionButton | Action button properties. The button won't be rendered without it. | [`PopoverButtonProps`](#popoverbuttonprops) | | -| tooltipCancelButton | Cancel button properties. The button won't be rendered without it. | [`PopoverButtonProps`](#popoverbuttonprops) | | -| tooltipClassName | Tooltip CSS class | `string` | | -| tooltipContentClassName | Tooltip content CSS class | `string` | | -| tooltipOffset | Tooltip offset relative to the control | `[number, number]` | | -| tooltipId | `id` HTML attribute of the popover | `string` | | - -### TriggerProps - -| Name | Description | Type | Default | -| --------- | ---------------------- | :--------------------------: | :-----: | -| onClick | Click event handler | `React.MouseEventHandler` | | -| onKeyDown | Keyboard event handler | `React.KeyboardEventHandler` | | - -### LinkProps - -| Name | Description | Type | Default | -| ------- | --------------------------------------------------------------------------- | :------------------: | :-----: | -| text | Link text | `string` | | -| href | Link href | `string` | | -| target | Where link should be opened | `"_self"` `"_blank"` | | -| onClick | Click event handler: `(event: React.MouseEvent) => void` | `Function` | | - -### PopoverButtonProps - -| Name | Description | Type | Default | -| ------- | ---------------------------------------------------------------------------- | :--------: | :-----: | -| text | Button text | `string` | | -| onClick | Button click handler: `(event: React.MouseEvent) => void` | `Function` | | - -| Name | Description | -| :---------------------- | :---------------- | -| `--g-popover-padding` | Content padding | -| `--g-popover-max-width` | Content max width | +| 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` | | +| 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` `"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"` | | diff --git a/src/components/Popover/__stories__/Popover.stories.tsx b/src/components/Popover/__stories__/Popover.stories.tsx index 7428505e33..0006eddbb2 100644 --- a/src/components/Popover/__stories__/Popover.stories.tsx +++ b/src/components/Popover/__stories__/Popover.stories.tsx @@ -1,261 +1,98 @@ -import * as React from 'react'; +import {action} from '@storybook/addon-actions'; +import type {Meta, StoryObj} from '@storybook/react'; -import type {Meta, StoryFn} from '@storybook/react'; - -import {Popover, PopoverBehavior} from '../'; -import type {PopoverProps} from '../'; import {Button} from '../../Button'; - -import {cnPopoverDemo} from './PopoverDemo.classname'; -import {Base} from './examples/Base/Base'; -import {WithCustomAnchor as WithCustomAnchorExample} from './examples/WithCustomAnchor/WithCustomAnchor'; - -import './PopoverDemo.scss'; +import {Link} from '../../Link'; +import {Flex} from '../../layout'; +import {Popover} from '../Popover'; const meta: Meta = { title: 'Components/Overlays/Popover', component: Popover, - args: { - initialOpen: false, - disabled: false, - autoclosable: true, - openOnHover: true, - hasArrow: true, - hasClose: false, - theme: 'info', - size: 's', - behavior: 'delayed', - }, - argTypes: { - initialOpen: { - control: 'boolean', - }, - disabled: { - control: 'boolean', - }, - autoclosable: {control: 'boolean'}, - openOnHover: {control: 'boolean'}, - offset: {control: 'object'}, - placement: { - control: 'select', - options: [ - 'top', - 'right', - 'left', - 'bottom', - 'top-start', - 'top-end', - 'bottom-start', - 'bottom-end', - 'right-start', - 'right-end', - 'left-start', - 'left-end', - 'auto', - 'auto-start', - 'auto-end', - ], - }, - hasArrow: {control: 'boolean'}, - hasClose: {control: 'boolean'}, - theme: { - control: 'select', - options: ['info', 'special', 'announcement'], - }, - size: {control: 'select', options: ['s', 'l']}, - anchorRef: {}, - children: {control: 'text'}, - title: {control: 'text'}, - content: {control: 'text'}, - htmlContent: {control: 'object'}, - contentClassName: {control: 'text'}, - links: {control: 'object'}, - forceLinksAppearance: {control: 'boolean'}, - tooltipActionButton: {control: 'object'}, - tooltipCancelButton: {control: 'object'}, - tooltipOffset: {control: 'object'}, - tooltipClassName: {control: 'text'}, - className: {control: 'text'}, - onClick: {action: 'onClick'}, - onOpenChange: {action: 'onOpenChange'}, - onCloseClick: {action: 'onCloseClick'}, - behavior: { - control: 'select', - options: ['immediate', 'delayed', 'delayedClosing'], - }, - delayOpening: {control: 'number', min: 0}, - delayClosing: {control: 'number', min: 0}, - disablePortal: {control: 'boolean'}, - tooltipId: {control: 'text'}, + parameters: { + layout: 'centered', }, }; export default meta; -const PlaygroundTemplate: StoryFn = (args) => ( - {args.openOnHover ? 'Move the cursor' : 'Click'} here to show the popover -); -export const Playground = PlaygroundTemplate.bind({}); -Playground.args = { - content: 'Popover content', -}; - -const ThemeTemplate: StoryFn = () => ( -
- - - - - - - - - -
-); -export const Theme = ThemeTemplate.bind({}); - -const SizeTemplate: StoryFn = () => ( -
- - - - - - -
-); -export const Size = SizeTemplate.bind({}); +type Story = StoryObj; -const FullFeaturedTemplate: StoryFn = (args) => ( -
- -
-

- Tooltip on {args.openOnHover ? 'hover' : 'click'} -

-
- -
-); -export const FullFeatured = FullFeaturedTemplate.bind({}); -FullFeatured.args = { - autoclosable: false, - title: 'Title', - tooltipActionButton: { - text: 'Action', - onClick: () => alert('Action button was clicked'), +export const Default: Story = { + render: (args) => ( + + + + ), + args: { + content:
Content
, + onOpenChange: action('onOpenChange'), }, - links: [ - { - text: 'Link with a href', - href: 'https://gravity-ui.com', - }, - ], - className: 'demo-icon-tooltip', - openOnHover: false, - behavior: PopoverBehavior.Delayed, - theme: 'info', - htmlContent: - 'Tooltip\'s html content. Learn more here', }; -export const WithLongActionItems = FullFeaturedTemplate.bind({}); -WithLongActionItems.args = { - autoclosable: false, - content: 'There are two actions', - tooltipActionButton: { - text: 'Action with moderately long text', - onClick: () => alert('Action button was clicked'), - }, - tooltipCancelButton: { - text: 'Cancel with moderately long text', - onClick: () => alert('Cancel button was clicked'), +export const Delay: Story = { + render: (args) => ( + + + + + + + + + ), + args: { + ...Default.args, }, - className: 'demo-icon-tooltip', - openOnHover: false, - behavior: PopoverBehavior.Delayed, - theme: 'info', }; -export const WithAlmostLongActionItems = FullFeaturedTemplate.bind({}); -WithAlmostLongActionItems.args = { - autoclosable: false, - content: 'There are two actions', - tooltipActionButton: { - text: 'Action with more', - onClick: () => alert('Action button was clicked'), - }, - tooltipCancelButton: { - text: 'Action with', - onClick: () => alert('Cancel button was clicked'), +export const OnlyClick: Story = { + ...Default, + args: { + ...Default.args, + trigger: 'click', }, - className: 'demo-icon-tooltip', - openOnHover: false, - behavior: PopoverBehavior.Delayed, - theme: 'info', }; -const WithCustomAnchorTemplate: StoryFn = () => ; -export const WithCustomAnchor = WithCustomAnchorTemplate.bind({}); -WithCustomAnchor.args = { - content: 'Popover content', +export const Disabled: Story = { + ...Default, + args: { + ...Default.args, + disabled: true, + }, }; -const tooltipId = 'tooltipId'; -const popoverId = 'popoverId'; - -const AccessibleTemplate: StoryFn = () => { - const [openPopover, setOpenPopover] = React.useState(false); - const ref = React.useRef(null); - - return ( -
- - {({onClick, open}) => ( - - )} - - alert('Action button was clicked'), - }} - tooltipCancelButton={{ - text: 'Action with', - onClick: () => alert('Cancel button was clicked'), - }} - autoclosable={false} - openOnHover={false} - focusTrap - autoFocus - restoreFocusRef={ref} - > - {({onClick}) => ( - - )} - -
- ); +export const SafePolygon: Story = { + ...Default, + args: { + ...Default.args, + openDelay: 0, + closeDelay: 0, + offset: 50, + enableSafePolygon: true, + }, }; -export const Accessible = AccessibleTemplate.bind({}); -Accessible.args = { - content: 'Popover content', + +export const FocusManagement: Story = { + render: (args) => ( + + + + + + + + + + + + ), + args: { + ...Default.args, + content: ( +
+ Content with Link and +
+ ), + }, }; diff --git a/src/components/Popover/__stories__/PopoverDemo.classname.ts b/src/components/Popover/__stories__/PopoverDemo.classname.ts deleted file mode 100644 index b9f80cc58f..0000000000 --- a/src/components/Popover/__stories__/PopoverDemo.classname.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {block} from '../../utils/cn'; - -export const cnPopoverDemo = block('popover-demo'); diff --git a/src/components/Popover/index.ts b/src/components/Popover/index.ts index 18c9d69577..8f473de4b9 100644 --- a/src/components/Popover/index.ts +++ b/src/components/Popover/index.ts @@ -1,9 +1 @@ export * from './Popover'; -export type { - PopoverButtonProps, - PopoverProps, - PopoverInstanceProps, - PopoverAnchorRef, - PopoverAnchorElement, -} from './types'; -export {PopoverBehavior} from './config'; diff --git a/src/components/Select/components/SelectControl/SelectControl.tsx b/src/components/Select/components/SelectControl/SelectControl.tsx index e58ec2cfa6..09445302eb 100644 --- a/src/components/Select/components/SelectControl/SelectControl.tsx +++ b/src/components/Select/components/SelectControl/SelectControl.tsx @@ -7,7 +7,7 @@ import isEmpty from 'lodash/isEmpty'; import {useUniqId} from '../../../../hooks'; import {Icon} from '../../../Icon'; -import {Popover} from '../../../Popover'; +import {Popover} from '../../../legacy'; import type {CnMods} from '../../../utils/cn'; import {selectControlBlock, selectControlButtonBlock} from '../../constants'; import i18n from '../../i18n'; diff --git a/src/components/controls/TextInput/TextInput.tsx b/src/components/controls/TextInput/TextInput.tsx index 2bc717d1eb..e5bc92263b 100644 --- a/src/components/controls/TextInput/TextInput.tsx +++ b/src/components/controls/TextInput/TextInput.tsx @@ -7,7 +7,7 @@ import {TriangleExclamation} from '@gravity-ui/icons'; import {useControlledState, useForkRef, useUniqId} from '../../../hooks'; import {useElementSize, useFormResetHandler} from '../../../hooks/private'; import {Icon} from '../../Icon'; -import {Popover} from '../../Popover'; +import {Popover} from '../../legacy'; import {block} from '../../utils/cn'; import {ClearButton, mapTextInputSizeToButtonSize} from '../common'; import {OuterAdditionalContent} from '../common/OuterAdditionalContent/OuterAdditionalContent'; diff --git a/src/components/lab/Popover/Popover.tsx b/src/components/lab/Popover/Popover.tsx deleted file mode 100644 index 1d91d8d9ee..0000000000 --- a/src/components/lab/Popover/Popover.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import * as React from 'react'; - -import { - safePolygon, - useClick, - useFloatingRootContext, - useHover, - useInteractions, -} from '@floating-ui/react'; -import type {UseInteractionsReturn} from '@floating-ui/react'; - -import {useControlledState, useForkRef} from '../../../hooks'; -import {Popup} from '../../Popup'; -import type {PopupProps} from '../../Popup'; -import type {AriaLabelingProps, DOMProps, QAProps} from '../../types'; -import {block} from '../../utils/cn'; -import {getElementRef} from '../../utils/getElementRef'; - -export interface PopoverProps - extends AriaLabelingProps, - QAProps, - DOMProps, - Pick< - PopupProps, - | 'strategy' - | 'placement' - | 'offset' - | 'keepMounted' - | 'hasArrow' - | 'initialFocus' - | 'returnFocus' - | 'disableFocusVisuallyHiddenDismiss' - > { - children: - | ((props: Record, ref: React.Ref) => React.ReactElement) - | React.ReactElement; - open?: boolean; - onOpenChange?: (open: boolean) => void; - disabled?: boolean; - content?: React.ReactNode; - trigger?: 'click'; - openDelay?: number; - closeDelay?: number; - enableSafePolygon?: boolean; -} - -const b = block('popover2'); -const DEFAULT_OPEN_DELAY = 500; -const DEFAULT_CLOSE_DELAY = 250; - -export function Popover({ - children, - open, - onOpenChange, - disabled, - content, - trigger, - openDelay = DEFAULT_OPEN_DELAY, - closeDelay = DEFAULT_CLOSE_DELAY, - enableSafePolygon, - className, - ...restProps -}: PopoverProps) { - const [anchorElement, setAnchorElement] = React.useState(null); - const [floatingElement, setFloatingElement] = React.useState(null); - const [getAnchorProps, setGetAnchorProps] = - React.useState(); - - const handleSetGetAnchorProps = React.useCallback>( - (getAnchorPropsFn) => { - setGetAnchorProps(() => getAnchorPropsFn); - }, - [], - ); - - 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: {open: openDelay, close: closeDelay}, - move: false, - handleClose: enableSafePolygon ? safePolygon() : undefined, - }); - const click = useClick(context, {enabled: !disabled}); - - const {getReferenceProps, getFloatingProps} = useInteractions([hover, click]); - - const anchorRef = useForkRef( - setAnchorElement, - React.isValidElement(children) ? getElementRef(children) : undefined, - ); - const anchorProps = React.isValidElement(children) - ? getReferenceProps(getAnchorProps?.(children.props) ?? children.props) - : getReferenceProps(getAnchorProps?.()); - const anchorNode = React.isValidElement(children) - ? React.cloneElement(children, { - ref: anchorRef, - ...anchorProps, - }) - : children(anchorProps, anchorRef); - - return ( - - {anchorNode} - - {content} - - - ); -} diff --git a/src/components/lab/Popover/README.md b/src/components/lab/Popover/README.md deleted file mode 100644 index a7acef1af8..0000000000 --- a/src/components/lab/Popover/README.md +++ /dev/null @@ -1,53 +0,0 @@ - - -# Popover - - - -```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 {Button, Popover} from '@gravity-ui/uikit'; - - - -; -``` - -## 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` | | -| 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` `"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"` | | diff --git a/src/components/lab/Popover/__stories__/Popover.stories.tsx b/src/components/lab/Popover/__stories__/Popover.stories.tsx deleted file mode 100644 index 86dbc110ee..0000000000 --- a/src/components/lab/Popover/__stories__/Popover.stories.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import {action} from '@storybook/addon-actions'; -import type {Meta, StoryObj} from '@storybook/react'; - -import {Button} from '../../../Button'; -import {Link} from '../../../Link'; -import {Flex} from '../../../layout'; -import {Popover} from '../Popover'; - -const meta: Meta = { - title: 'Lab/Popover', - component: Popover, - parameters: { - layout: 'centered', - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - render: (args) => ( - - - - ), - args: { - content:
Content
, - onOpenChange: action('onOpenChange'), - }, -}; - -export const Delay: Story = { - render: (args) => ( - - - - - - - - - ), - 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, - openDelay: 0, - closeDelay: 0, - offset: 50, - enableSafePolygon: true, - }, -}; - -export const FocusManagement: Story = { - render: (args) => ( - - - - - - - - - - - - ), - args: { - ...Default.args, - content: ( -
- Content with Link and -
- ), - }, -}; diff --git a/src/components/legacy/Popover/Popover.classname.ts b/src/components/legacy/Popover/Popover.classname.ts new file mode 100644 index 0000000000..348e6a3638 --- /dev/null +++ b/src/components/legacy/Popover/Popover.classname.ts @@ -0,0 +1,3 @@ +import {block} from '../../utils/cn'; + +export const cnPopover = block('popover-legacy'); diff --git a/src/components/Popover/Popover.scss b/src/components/legacy/Popover/Popover.scss similarity index 97% rename from src/components/Popover/Popover.scss rename to src/components/legacy/Popover/Popover.scss index 415f763b68..5edd3d7e21 100644 --- a/src/components/Popover/Popover.scss +++ b/src/components/legacy/Popover/Popover.scss @@ -1,7 +1,7 @@ -@use '../../../styles/mixins'; -@use '../variables'; +@use '../../../../styles/mixins'; +@use '../../variables'; -$block: '.#{variables.$ns}popover'; +$block: '.#{variables.$ns}popover-legacy'; #{$block} { display: inline-block; diff --git a/src/components/legacy/Popover/Popover.tsx b/src/components/legacy/Popover/Popover.tsx new file mode 100644 index 0000000000..68e2c2afc3 --- /dev/null +++ b/src/components/legacy/Popover/Popover.tsx @@ -0,0 +1,252 @@ +'use client'; + +import * as React from 'react'; + +import {Xmark} from '@gravity-ui/icons'; + +import {useUniqId} from '../../../hooks/useUniqId'; +import {Button} from '../../Button'; +import {Icon} from '../../Icon'; +import type {PopupPlacement} from '../../Popup'; +import {Popup} from '../../Popup'; +import {useDirection} from '../../theme'; +import type {QAProps} from '../../types'; +import {warnOnce} from '../../utils/warn'; + +import {cnPopover} from './Popover.classname'; +import {Buttons} from './components/Buttons/Buttons'; +import {Content} from './components/Content/Content'; +import {Links} from './components/Links/Links'; +import {Trigger} from './components/Trigger/Trigger'; +import {PopoverBehavior} from './config'; +import {useOpen} from './hooks/useOpen'; +import type {PopoverInstanceProps, PopoverProps} from './types'; + +import './Popover.scss'; + +/** + * @deprecated + */ +export const Popover = React.forwardRef(function ( + { + initialOpen = false, + disabled = false, + autoclosable = true, + openOnHover = true, + delayOpening, + delayClosing, + behavior = PopoverBehavior.Delayed, + placement, + offset = {}, + tooltipOffset, + tooltipClassName, + theme = 'info', + size = 's', + hasArrow = true, + hasClose = false, + className, + children, + title, + content, + htmlContent, + contentClassName, + links, + forceLinksAppearance = false, + tooltipActionButton, + tooltipCancelButton, + onOpenChange, + onCloseClick, + onClick, + anchorRef, + anchorElement, + strategy, + qa, + disablePortal = false, + tooltipId, + focusTrap, + autoFocus, + restoreFocusRef, + }, + ref, +) { + const direction = useDirection(); + const controlRef = React.useRef(null); + const closedManually = React.useRef(false); + const shouldBeOpen = React.useRef(initialOpen); + + const { + isOpen, + closingTimeout, + openTooltip, + openTooltipDelayed, + unsetOpeningTimeout, + closeTooltip, + closeTooltipDelayed, + unsetClosingTimeout, + } = useOpen({ + initialOpen, + disabled, + autoclosable, + onOpenChange, + delayOpening, + delayClosing, + behavior, + shouldBeOpen, + }); + + const popupPlacement = React.useMemo(() => { + if (placement) { + return placement; + } + + return direction === 'rtl' ? ['left', 'bottom'] : ['right', 'bottom']; + }, [direction, placement]); + + React.useImperativeHandle( + ref, + () => ({ + openTooltip, + closeTooltip, + }), + [openTooltip, closeTooltip], + ); + + const handleCloseClick = async (event: React.MouseEvent) => { + closeTooltip(); + onCloseClick?.(event); + }; + + const hasTitle = Boolean(title); + + const hasAnchor = Boolean(anchorRef || anchorElement); + + const popoverTitleId = `popover-${tooltipId ?? ''}-title-${useUniqId()}`; + + const tooltip = ( + + + {title && ( +

+ {title} +

+ )} + + {links && } + + {hasClose && ( +
+ +
+ )} +
+
+ ); + + if (hasAnchor) { + return tooltip; + } + + const onMouseEnter = () => { + unsetClosingTimeout(); + + if (!isOpen && !disabled && !closedManually.current) { + openTooltipDelayed(); + } else { + shouldBeOpen.current = true; + } + }; + + const onMouseLeave = () => { + if (autoclosable && !closedManually.current && !closingTimeout.current) { + unsetOpeningTimeout(); + closeTooltipDelayed(); + } else { + shouldBeOpen.current = false; + } + + closedManually.current = false; + }; + + if (offset && (typeof offset.top === 'number' || typeof offset.left === 'number')) { + warnOnce( + '[Popover] Physical names (top, left) of "offset" property are deprecated. Use logical names (block, inline) instead.', + ); + } + + return ( +
+ + {children} + + {tooltip} +
+ ); +}); + +Popover.displayName = 'Popover'; diff --git a/src/components/Popover/README-ru.md b/src/components/legacy/Popover/README-ru.md similarity index 100% rename from src/components/Popover/README-ru.md rename to src/components/legacy/Popover/README-ru.md diff --git a/src/components/legacy/Popover/README.md b/src/components/legacy/Popover/README.md new file mode 100644 index 0000000000..5d047f8bcd --- /dev/null +++ b/src/components/legacy/Popover/README.md @@ -0,0 +1,300 @@ + + +# Popover + + + +```tsx +import {Popover} from '@gravity-ui/uikit'; +``` + +This component allows you to add a section with some pop-up content. + +### Simple usage + + + + + +```tsx +Open a tooltip +``` + + + +### With jsx content + + + + + +```tsx +}>Open a tooltip +``` + + + +### With html content + + + + + +```tsx +html content. Learn more here' + } +> + Open a tooltip + +``` + + + +### With links + + + + + +```tsx + alert('The link is clicked'), + }, + ]} +> + Open a tooltip + +``` + + + +### With action button + + + + + +```tsx + console.log('Action button was clicked'), + }} +> + Open a tooltip + +``` + + + +### With automatic closing when the cursor is outside for `delayClosing` + + + + + +```tsx + console.log('Action button was clicked'), + }} +> + Open a tooltip + +``` + + + +## Instance usage + +```tsx +import {Popover, PopoverInstanceProps} from '@gravity-ui/uikit'; + +const popoverRef = useRef(); + +const open = () => { + popoverRef.current?.openTooltip(); +}; + +const close = () => { + popoverRef.current?.closeTooltip(); +}; + +<> + + + +; +``` + +### Instance properties + +| Name | Description | Type | Default | +| ------------ | ------------------------------- | :--------: | :-----: | +| openTooltip | Opens the `() => void` tooltip | `Function` | | +| closeTooltip | Closes the `() => void` tooltip | `Function` | | + +## Properties + +| Name | Description | Type | Default | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------: | :-------------------: | +| anchorRef | `Popup` anchor element that can also be `VirtualElement` | [`PopupAnchorRef`](../Popup/README.md#anchor) | | +| autoclosable | Enables or disables closing the tooltip automatically when the cursor moves outside it | `boolean` | `true` | +| autoFocus | If true, the focus will be transferred to the first element when the popover opens | `boolean` | | +| behavior | Tooltip open or close behavior with `openOnHover`. `"immediate"`: without any delay, `"delayed"`: with 300ms delay for opening and closing, `"delayedClosing"`: with 300ms delay only for closing. This property will not apply in case `delayOpening` or `delayClosing` are provided. | `"immediate"` `"delayed"` `"delayedClosing"` | `"delayed"` | +| children | Tooltip's trigger content over which the tooltip is shown. It can either be the `(triggerProps: `[`TriggerProps`](#triggerprops))` => React.ReactNode` function or `ReactNode`. | `React.ReactNode` `Function` | | +| className | CSS class for the control | `string` | | +| content | Tooltip content | `React.ReactNode` | | +| contentClassName | CSS class for `content` | `string` | | +| delayClosing | Custom delay for closing if `autoclosable` | `number` | | +| delayOpening | Custom delay for opening if `openOnHover` | `number` | | +| disabled | Disables open state changes | `boolean` | `false` | +| disablePortal | Disable rendering of the popover in a portal | `boolean` | `false` | +| focusTrap | Prevents focus from leaving the popover while open | `boolean` | | +| forceLinksAppearance | Forces styles for links | `boolean` | `false` | +| hasArrow | Enables or disables a tooltip arrow | `boolean` | `true` | +| hasClose | Enables or disables a close button for a tooltip | `boolean` | `false` | +| htmlContent | Tooltip's HTML content to render via `dangerouslySetInnerHTML` | `string` | | +| initialOpen | Enables or disables the tooltip initial opening | `boolean` | `false` | +| links | Links under the content | `[`[`LinkProps`](#linksprops)`]` | | +| offset | Control's offset | `{top: number, left: number}` | | +| onClick | Anchor click callback: `(event: React.MouseEvent) => boolean \| Promise`. If the function returns `true, the tooltip will be open; otherwise, it won't open. | `Function` | | +| onCloseClick | Close button click handler: `(event: React.MouseEvent) => void` | `Function` | | +| onOpenChange | Open state change handler: `(open: boolean) => void`. Might be useful for the delayed rendering of the tooltip content. | `Function` | | +| openOnHover | Enables or disables opening the tooltip when hovered | `boolean` | `true` | +| placement | `Popup` placement | [`PopupPlacement`](../Popup/README.md#placement) | `["right", "bottom"]` | +| qa | `data-qa` HTML attribute, used for testing | `string` | | +| restoreFocusRef | Focused element when the popover closes | `React.RefObject` | | +| size | Tooltip size | `"s"` `"l"` | `"s"` | +| strategy | `Floating UI` positioning [strategy](https://floating-ui.com/docs/computePosition#strategy) | `"absolute"` `"fixed"` | `"absolute"` | +| title | Tooltip title | `string` | | +| theme | Tooltip theme | `"info"` `"special"` `"announcement"` | `"info"` | +| tooltipActionButton | Action button properties. The button won't be rendered without it. | [`PopoverButtonProps`](#popoverbuttonprops) | | +| tooltipCancelButton | Cancel button properties. The button won't be rendered without it. | [`PopoverButtonProps`](#popoverbuttonprops) | | +| tooltipClassName | Tooltip CSS class | `string` | | +| tooltipContentClassName | Tooltip content CSS class | `string` | | +| tooltipOffset | Tooltip offset relative to the control | `[number, number]` | | +| tooltipId | `id` HTML attribute of the popover | `string` | | + +### TriggerProps + +| Name | Description | Type | Default | +| --------- | ---------------------- | :--------------------------: | :-----: | +| onClick | Click event handler | `React.MouseEventHandler` | | +| onKeyDown | Keyboard event handler | `React.KeyboardEventHandler` | | + +### LinkProps + +| Name | Description | Type | Default | +| ------- | --------------------------------------------------------------------------- | :------------------: | :-----: | +| text | Link text | `string` | | +| href | Link href | `string` | | +| target | Where link should be opened | `"_self"` `"_blank"` | | +| onClick | Click event handler: `(event: React.MouseEvent) => void` | `Function` | | + +### PopoverButtonProps + +| Name | Description | Type | Default | +| ------- | ---------------------------------------------------------------------------- | :--------: | :-----: | +| text | Button text | `string` | | +| onClick | Button click handler: `(event: React.MouseEvent) => void` | `Function` | | + +| Name | Description | +| :---------------------- | :---------------- | +| `--g-popover-padding` | Content padding | +| `--g-popover-max-width` | Content max width | diff --git a/src/components/lab/Popover/__stories__/Docs.mdx b/src/components/legacy/Popover/__stories__/Docs.mdx similarity index 100% rename from src/components/lab/Popover/__stories__/Docs.mdx rename to src/components/legacy/Popover/__stories__/Docs.mdx diff --git a/src/components/legacy/Popover/__stories__/Popover.stories.tsx b/src/components/legacy/Popover/__stories__/Popover.stories.tsx new file mode 100644 index 0000000000..e6fb3fd205 --- /dev/null +++ b/src/components/legacy/Popover/__stories__/Popover.stories.tsx @@ -0,0 +1,261 @@ +import * as React from 'react'; + +import type {Meta, StoryFn} from '@storybook/react'; + +import {Popover, PopoverBehavior} from '../'; +import type {PopoverProps} from '../'; +import {Button} from '../../../Button'; + +import {cnPopoverDemo} from './PopoverDemo.classname'; +import {Base} from './examples/Base/Base'; +import {WithCustomAnchor as WithCustomAnchorExample} from './examples/WithCustomAnchor/WithCustomAnchor'; + +import './PopoverDemo.scss'; + +const meta: Meta = { + title: 'Legacy/Popover', + component: Popover, + args: { + initialOpen: false, + disabled: false, + autoclosable: true, + openOnHover: true, + hasArrow: true, + hasClose: false, + theme: 'info', + size: 's', + behavior: 'delayed', + }, + argTypes: { + initialOpen: { + control: 'boolean', + }, + disabled: { + control: 'boolean', + }, + autoclosable: {control: 'boolean'}, + openOnHover: {control: 'boolean'}, + offset: {control: 'object'}, + placement: { + control: 'select', + options: [ + 'top', + 'right', + 'left', + 'bottom', + 'top-start', + 'top-end', + 'bottom-start', + 'bottom-end', + 'right-start', + 'right-end', + 'left-start', + 'left-end', + 'auto', + 'auto-start', + 'auto-end', + ], + }, + hasArrow: {control: 'boolean'}, + hasClose: {control: 'boolean'}, + theme: { + control: 'select', + options: ['info', 'special', 'announcement'], + }, + size: {control: 'select', options: ['s', 'l']}, + anchorRef: {}, + children: {control: 'text'}, + title: {control: 'text'}, + content: {control: 'text'}, + htmlContent: {control: 'object'}, + contentClassName: {control: 'text'}, + links: {control: 'object'}, + forceLinksAppearance: {control: 'boolean'}, + tooltipActionButton: {control: 'object'}, + tooltipCancelButton: {control: 'object'}, + tooltipOffset: {control: 'object'}, + tooltipClassName: {control: 'text'}, + className: {control: 'text'}, + onClick: {action: 'onClick'}, + onOpenChange: {action: 'onOpenChange'}, + onCloseClick: {action: 'onCloseClick'}, + behavior: { + control: 'select', + options: ['immediate', 'delayed', 'delayedClosing'], + }, + delayOpening: {control: 'number', min: 0}, + delayClosing: {control: 'number', min: 0}, + disablePortal: {control: 'boolean'}, + tooltipId: {control: 'text'}, + }, +}; + +export default meta; + +const PlaygroundTemplate: StoryFn = (args) => ( + {args.openOnHover ? 'Move the cursor' : 'Click'} here to show the popover +); +export const Playground = PlaygroundTemplate.bind({}); +Playground.args = { + content: 'Popover content', +}; + +const ThemeTemplate: StoryFn = () => ( +
+ + + + + + + + + +
+); +export const Theme = ThemeTemplate.bind({}); + +const SizeTemplate: StoryFn = () => ( +
+ + + + + + +
+); +export const Size = SizeTemplate.bind({}); + +const FullFeaturedTemplate: StoryFn = (args) => ( +
+ +
+

+ Tooltip on {args.openOnHover ? 'hover' : 'click'} +

+
+ +
+); +export const FullFeatured = FullFeaturedTemplate.bind({}); +FullFeatured.args = { + autoclosable: false, + title: 'Title', + tooltipActionButton: { + text: 'Action', + onClick: () => alert('Action button was clicked'), + }, + links: [ + { + text: 'Link with a href', + href: 'https://gravity-ui.com', + }, + ], + className: 'demo-icon-tooltip', + openOnHover: false, + behavior: PopoverBehavior.Delayed, + theme: 'info', + htmlContent: + 'Tooltip\'s html content. Learn more here', +}; + +export const WithLongActionItems = FullFeaturedTemplate.bind({}); +WithLongActionItems.args = { + autoclosable: false, + content: 'There are two actions', + tooltipActionButton: { + text: 'Action with moderately long text', + onClick: () => alert('Action button was clicked'), + }, + tooltipCancelButton: { + text: 'Cancel with moderately long text', + onClick: () => alert('Cancel button was clicked'), + }, + className: 'demo-icon-tooltip', + openOnHover: false, + behavior: PopoverBehavior.Delayed, + theme: 'info', +}; + +export const WithAlmostLongActionItems = FullFeaturedTemplate.bind({}); +WithAlmostLongActionItems.args = { + autoclosable: false, + content: 'There are two actions', + tooltipActionButton: { + text: 'Action with more', + onClick: () => alert('Action button was clicked'), + }, + tooltipCancelButton: { + text: 'Action with', + onClick: () => alert('Cancel button was clicked'), + }, + className: 'demo-icon-tooltip', + openOnHover: false, + behavior: PopoverBehavior.Delayed, + theme: 'info', +}; + +const WithCustomAnchorTemplate: StoryFn = () => ; +export const WithCustomAnchor = WithCustomAnchorTemplate.bind({}); +WithCustomAnchor.args = { + content: 'Popover content', +}; + +const tooltipId = 'tooltipId'; +const popoverId = 'popoverId'; + +const AccessibleTemplate: StoryFn = () => { + const [openPopover, setOpenPopover] = React.useState(false); + const ref = React.useRef(null); + + return ( +
+ + {({onClick, open}) => ( + + )} + + alert('Action button was clicked'), + }} + tooltipCancelButton={{ + text: 'Action with', + onClick: () => alert('Cancel button was clicked'), + }} + autoclosable={false} + openOnHover={false} + focusTrap + autoFocus + restoreFocusRef={ref} + > + {({onClick}) => ( + + )} + +
+ ); +}; +export const Accessible = AccessibleTemplate.bind({}); +Accessible.args = { + content: 'Popover content', +}; diff --git a/src/components/legacy/Popover/__stories__/PopoverDemo.classname.ts b/src/components/legacy/Popover/__stories__/PopoverDemo.classname.ts new file mode 100644 index 0000000000..2b8326b5dc --- /dev/null +++ b/src/components/legacy/Popover/__stories__/PopoverDemo.classname.ts @@ -0,0 +1,3 @@ +import {block} from '../../../utils/cn'; + +export const cnPopoverDemo = block('popover-legacy-demo'); diff --git a/src/components/Popover/__stories__/PopoverDemo.scss b/src/components/legacy/Popover/__stories__/PopoverDemo.scss similarity index 90% rename from src/components/Popover/__stories__/PopoverDemo.scss rename to src/components/legacy/Popover/__stories__/PopoverDemo.scss index eaaec0c85b..392cf5ca9a 100644 --- a/src/components/Popover/__stories__/PopoverDemo.scss +++ b/src/components/legacy/Popover/__stories__/PopoverDemo.scss @@ -1,6 +1,6 @@ -@use '../../variables'; +@use '../../../variables'; -$block: '.#{variables.$ns}popover-demo'; +$block: '.#{variables.$ns}popover-legacy-demo'; #{$block} { display: flex; diff --git a/src/components/Popover/__stories__/examples/Base/Base.tsx b/src/components/legacy/Popover/__stories__/examples/Base/Base.tsx similarity index 100% rename from src/components/Popover/__stories__/examples/Base/Base.tsx rename to src/components/legacy/Popover/__stories__/examples/Base/Base.tsx diff --git a/src/components/Popover/__stories__/examples/WithCustomAnchor/WithCustomAnchor.tsx b/src/components/legacy/Popover/__stories__/examples/WithCustomAnchor/WithCustomAnchor.tsx similarity index 95% rename from src/components/Popover/__stories__/examples/WithCustomAnchor/WithCustomAnchor.tsx rename to src/components/legacy/Popover/__stories__/examples/WithCustomAnchor/WithCustomAnchor.tsx index eb6cbf7ca4..e18854e546 100644 --- a/src/components/Popover/__stories__/examples/WithCustomAnchor/WithCustomAnchor.tsx +++ b/src/components/legacy/Popover/__stories__/examples/WithCustomAnchor/WithCustomAnchor.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import {Popover} from '../../../'; -import {Button} from '../../../../Button'; -import {Loader} from '../../../../Loader'; +import {Button} from '../../../../../Button'; +import {Loader} from '../../../../../Loader'; import type {PopoverInstanceProps} from '../../../types'; import {cnPopoverDemo} from '../../PopoverDemo.classname'; diff --git a/src/components/Popover/__tests__/Popover.test.tsx b/src/components/legacy/Popover/__tests__/Popover.test.tsx similarity index 98% rename from src/components/Popover/__tests__/Popover.test.tsx rename to src/components/legacy/Popover/__tests__/Popover.test.tsx index 47f70eedaa..35c5d0ceff 100644 --- a/src/components/Popover/__tests__/Popover.test.tsx +++ b/src/components/legacy/Popover/__tests__/Popover.test.tsx @@ -1,5 +1,5 @@ -import {setupTimersMock} from '../../../../test-utils/setupTimersMock'; -import {act, fireEvent, render, screen, waitFor} from '../../../../test-utils/utils'; +import {setupTimersMock} from '../../../../../test-utils/setupTimersMock'; +import {act, fireEvent, render, screen, waitFor} from '../../../../../test-utils/utils'; import {Popover} from '../Popover'; import {PopoverBehavior, delayByBehavior} from '../config'; import type {PopoverProps} from '../types'; diff --git a/src/components/Popover/components/Buttons/Buttons.tsx b/src/components/legacy/Popover/components/Buttons/Buttons.tsx similarity index 97% rename from src/components/Popover/components/Buttons/Buttons.tsx rename to src/components/legacy/Popover/components/Buttons/Buttons.tsx index b1e8403bcf..5c74aa9576 100644 --- a/src/components/Popover/components/Buttons/Buttons.tsx +++ b/src/components/legacy/Popover/components/Buttons/Buttons.tsx @@ -1,4 +1,4 @@ -import {Button} from '../../../Button'; +import {Button} from '../../../../Button'; import {cnPopover} from '../../Popover.classname'; import type {PopoverButtonProps, PopoverTheme} from '../../types'; diff --git a/src/components/Popover/components/Buttons/helpers/getButtonView.ts b/src/components/legacy/Popover/components/Buttons/helpers/getButtonView.ts similarity index 100% rename from src/components/Popover/components/Buttons/helpers/getButtonView.ts rename to src/components/legacy/Popover/components/Buttons/helpers/getButtonView.ts diff --git a/src/components/Popover/components/Content/Content.tsx b/src/components/legacy/Popover/components/Content/Content.tsx similarity index 100% rename from src/components/Popover/components/Content/Content.tsx rename to src/components/legacy/Popover/components/Content/Content.tsx diff --git a/src/components/Popover/components/Links/Links.tsx b/src/components/legacy/Popover/components/Links/Links.tsx similarity index 96% rename from src/components/Popover/components/Links/Links.tsx rename to src/components/legacy/Popover/components/Links/Links.tsx index 3ae781a48b..9c7b6615a8 100644 --- a/src/components/Popover/components/Links/Links.tsx +++ b/src/components/legacy/Popover/components/Links/Links.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {Link} from '../../../Link'; +import {Link} from '../../../../Link'; import {cnPopover} from '../../Popover.classname'; export type LinksProps = { diff --git a/src/components/Popover/components/Trigger/Trigger.tsx b/src/components/legacy/Popover/components/Trigger/Trigger.tsx similarity index 97% rename from src/components/Popover/components/Trigger/Trigger.tsx rename to src/components/legacy/Popover/components/Trigger/Trigger.tsx index e1dd095d43..e0c6586bcd 100644 --- a/src/components/Popover/components/Trigger/Trigger.tsx +++ b/src/components/legacy/Popover/components/Trigger/Trigger.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; -import {useActionHandlers} from '../../../../hooks'; +import {useActionHandlers} from '../../../../../hooks'; interface TriggerArgs { onClick: React.MouseEventHandler; diff --git a/src/components/Popover/config.ts b/src/components/legacy/Popover/config.ts similarity index 100% rename from src/components/Popover/config.ts rename to src/components/legacy/Popover/config.ts diff --git a/src/components/Popover/hooks/useOpen.ts b/src/components/legacy/Popover/hooks/useOpen.ts similarity index 98% rename from src/components/Popover/hooks/useOpen.ts rename to src/components/legacy/Popover/hooks/useOpen.ts index 5bc60fe91b..7db0b8fa9e 100644 --- a/src/components/Popover/hooks/useOpen.ts +++ b/src/components/legacy/Popover/hooks/useOpen.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -import {useUpdateEffect} from '../../../hooks/private'; +import {useUpdateEffect} from '../../../../hooks/private'; import {delayByBehavior} from '../config'; import type {PopoverBehavior} from '../config'; diff --git a/src/components/legacy/Popover/index.ts b/src/components/legacy/Popover/index.ts new file mode 100644 index 0000000000..18c9d69577 --- /dev/null +++ b/src/components/legacy/Popover/index.ts @@ -0,0 +1,9 @@ +export * from './Popover'; +export type { + PopoverButtonProps, + PopoverProps, + PopoverInstanceProps, + PopoverAnchorRef, + PopoverAnchorElement, +} from './types'; +export {PopoverBehavior} from './config'; diff --git a/src/components/Popover/types.ts b/src/components/legacy/Popover/types.ts similarity index 99% rename from src/components/Popover/types.ts rename to src/components/legacy/Popover/types.ts index db2dcfdea6..f43721f51e 100644 --- a/src/components/Popover/types.ts +++ b/src/components/legacy/Popover/types.ts @@ -1,4 +1,4 @@ -import type {PopupAnchorElement, PopupAnchorRef, PopupOffset, PopupProps} from '../Popup'; +import type {PopupAnchorElement, PopupAnchorRef, PopupOffset, PopupProps} from '../../Popup'; import type {ButtonsProps} from './components/Buttons/Buttons'; import type {ContentProps} from './components/Content/Content'; diff --git a/src/components/legacy/index.ts b/src/components/legacy/index.ts new file mode 100644 index 0000000000..8f473de4b9 --- /dev/null +++ b/src/components/legacy/index.ts @@ -0,0 +1 @@ +export * from './Popover'; diff --git a/src/legacy.ts b/src/legacy.ts new file mode 100644 index 0000000000..61dc507740 --- /dev/null +++ b/src/legacy.ts @@ -0,0 +1 @@ +export * from './components/legacy';