Skip to content

Commit

Permalink
feat(Popover)!: replace legacy Popover with the new one (#2031)
Browse files Browse the repository at this point in the history
  • Loading branch information
amje authored Jan 9, 2025
1 parent 0b9e240 commit ccdf22e
Show file tree
Hide file tree
Showing 36 changed files with 1,080 additions and 1,064 deletions.
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/components/HelpMark/HelpMark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
3 changes: 0 additions & 3 deletions src/components/Popover/Popover.classname.ts

This file was deleted.

349 changes: 115 additions & 234 deletions src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -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<PopoverInstanceProps, PopoverProps & QAProps>(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<HTMLDivElement>(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<PopupPlacement>(() => {
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<string, unknown>, ref: React.Ref<HTMLElement>) => 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<HTMLElement | null>(null);
const [floatingElement, setFloatingElement] = React.useState<HTMLDivElement | null>(null);
const [getAnchorProps, setGetAnchorProps] =
React.useState<UseInteractionsReturn['getReferenceProps']>();

const handleSetGetAnchorProps = React.useCallback<NonNullable<PopupProps['setGetAnchorProps']>>(
(getAnchorPropsFn) => {
setGetAnchorProps(() => getAnchorPropsFn);
},
[],
);

const handleCloseClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
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 = (
<Popup
id={tooltipId}
role={openOnHover ? 'tooltip' : 'dialog'}
strategy={strategy}
anchorElement={anchorElement}
anchorRef={anchorRef || controlRef}
className={cnPopover(
'tooltip',
{
theme,
size,
['with-close']: hasClose,
'force-links-appearance': forceLinksAppearance,
},
tooltipClassName,
)}
open={isOpen}
placement={popupPlacement}
hasArrow={hasArrow}
offset={tooltipOffset}
onClose={hasAnchor ? undefined : closeTooltip}
qa={qa ? `${qa}-tooltip` : ''}
disablePortal={disablePortal}
autoFocus={autoFocus}
modalFocus={focusTrap}
returnFocus={restoreFocusRef}
aria-labelledby={title ? popoverTitleId : undefined}
>
<React.Fragment>
{title && (
<h3 id={popoverTitleId} className={cnPopover('tooltip-title')}>
{title}
</h3>
)}
<Content
secondary={hasTitle ? theme !== 'announcement' : false}
content={content}
htmlContent={htmlContent}
className={contentClassName}
/>
{links && <Links links={links} />}
<Buttons
theme={theme}
tooltipActionButton={tooltipActionButton}
tooltipCancelButton={tooltipCancelButton}
/>
{hasClose && (
<div className={cnPopover('tooltip-close')}>
<Button
size="s"
view="flat-secondary"
onClick={handleCloseClick}
aria-label="Close"
>
<Icon data={Xmark} size={16} />
</Button>
</div>
)}
</React.Fragment>
</Popup>
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<any>(children)
? getReferenceProps(getAnchorProps?.(children.props) ?? children.props)
: getReferenceProps(getAnchorProps?.());
const anchorNode = React.isValidElement<any>(children)
? React.cloneElement(children, {
ref: anchorRef,
...anchorProps,
})
: children(anchorProps, anchorRef);

return (
<div
ref={controlRef}
className={cnPopover({disabled}, className)}
onMouseEnter={openOnHover ? onMouseEnter : undefined}
onMouseLeave={openOnHover ? onMouseLeave : undefined}
onFocus={openOnHover ? onMouseEnter : undefined}
onBlur={openOnHover ? onMouseLeave : undefined}
style={{
top: offset.top,
left: offset.left,
insetBlockStart: offset.block,
insetInlineStart: offset.inline,
}}
data-qa={qa}
>
<Trigger
closeTooltip={closeTooltip}
openTooltip={openTooltip}
open={isOpen}
openOnHover={openOnHover}
className={cnPopover('handler')}
disabled={disabled}
onClick={onClick}
closedManually={closedManually}
<React.Fragment>
{anchorNode}
<Popup
{...restProps}
open={isOpen && !disabled}
setGetAnchorProps={handleSetGetAnchorProps}
floatingContext={context}
floatingRef={setFloatingElement}
floatingProps={getFloatingProps()}
autoFocus
modalFocus
role="dialog"
className={b(null, className)}
>
{children}
</Trigger>
{tooltip}
</div>
{content}
</Popup>
</React.Fragment>
);
});

Popover.displayName = 'Popover';
}
Loading

0 comments on commit ccdf22e

Please sign in to comment.