Skip to content

Commit

Permalink
fix(Popup): anchor and replace floatingProps with floatingInteractions (
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS authored Jan 16, 2025
1 parent cc27b98 commit f969ea6
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 117 deletions.
12 changes: 12 additions & 0 deletions src/components/DropdownMenu/DropdownMenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,16 @@ $block: '.#{variables.$ns}dropdown-menu';
}
}
}

&__popup-content {
& > :first-child {
border-start-start-radius: inherit;
border-start-end-radius: inherit;
}

& > :last-child {
border-end-start-radius: inherit;
border-end-end-radius: inherit;
}
}
}
118 changes: 55 additions & 63 deletions src/components/DropdownMenu/DropdownMenuPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,25 +54,14 @@ export const DropdownMenuPopup = <T,>({
setActiveMenuPath(path.slice(0, path.length - 1));
}, [setActiveMenuPath, path]);

const handleMouseEnter = React.useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
setActiveMenuPath(path);
(popupProps?.floatingProps?.onMouseEnter as React.MouseEventHandler | undefined)?.(
event,
);
},
[path, popupProps, setActiveMenuPath],
);
const handleMouseEnter = React.useCallback(() => {
setActiveMenuPath(path);
}, [path, setActiveMenuPath]);

const handleMouseLeave = React.useCallback(() => {
activateParent();
}, [activateParent]);

const handleMouseLeave = React.useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
activateParent();
(popupProps?.floatingProps?.onMouseLeave as React.MouseEventHandler | undefined)?.(
event,
);
},
[activateParent, popupProps],
);
const handleSelect = React.useCallback(
(activeItem: DropdownMenuListItem<T>, event: KeyboardEvent) => {
if (activeItem.items && activeItem.path) {
Expand Down Expand Up @@ -148,52 +137,55 @@ export const DropdownMenuPopup = <T,>({
onClose={onClose}
placement="bottom-start"
{...popupProps}
floatingProps={{
...popupProps?.floatingProps,
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
}}
>
{children || (
<Menu className={cnDropdownMenu('menu')} size={size} {...menuProps}>
{items.map((item, index) => {
const isActive = isNavigationActive && activeItemIndex === index;
const activate = () => setActiveItemIndex(index);

const isActiveParent =
open &&
!isActive &&
activeMenuPath.length !== 0 &&
stringifyNavigationPath(item.path) ===
stringifyNavigationPath(activeMenuPath.slice(0, item.path.length));

const extraProps = {
...item.extraProps,
onMouseEnter: activate,
};

return (
<DropdownMenuItem
key={index}
className={cnDropdownMenu(
'menu-item',
{
separator: isSeparator(item),
'active-parent': isActiveParent,
'with-submenu': Boolean(item.items?.length),
},
item.className,
)}
selected={isActive}
popupProps={popupProps}
closeMenu={onClose}
{...item}
extraProps={extraProps}
/>
);
})}
</Menu>
)}
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={cnDropdownMenu('popup-content')}
>
{children || (
<Menu className={cnDropdownMenu('menu')} size={size} {...menuProps}>
{items.map((item, index) => {
const isActive = isNavigationActive && activeItemIndex === index;
const activate = () => setActiveItemIndex(index);

const isActiveParent =
open &&
!isActive &&
activeMenuPath.length !== 0 &&
stringifyNavigationPath(item.path) ===
stringifyNavigationPath(
activeMenuPath.slice(0, item.path.length),
);

const extraProps = {
...item.extraProps,
onMouseEnter: activate,
};

return (
<DropdownMenuItem
key={index}
className={cnDropdownMenu(
'menu-item',
{
separator: isSeparator(item),
'active-parent': isActiveParent,
'with-submenu': Boolean(item.items?.length),
},
item.className,
)}
selected={isActive}
popupProps={popupProps}
closeMenu={onClose}
{...item}
extraProps={extraProps}
/>
);
})}
</Menu>
)}
</div>
</Popup>
);
};
33 changes: 15 additions & 18 deletions src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import * as React from 'react';
import {
safePolygon,
useClick,
useDismiss,
useFloatingRootContext,
useHover,
useInteractions,
useRole,
} from '@floating-ui/react';
import type {UseInteractionsReturn} from '@floating-ui/react';

import {useControlledState, useForkRef} from '../../hooks';
import {Popup} from '../Popup';
Expand Down Expand Up @@ -63,20 +64,11 @@ export function Popover({
}: 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 [isOpen, setIsOpen] = useControlledState(open, false, onOpenChange);

const context = useFloatingRootContext({
open: isOpen,
open: isOpen && !disabled,
onOpenChange: setIsOpen,
elements: {
reference: anchorElement,
Expand All @@ -91,16 +83,23 @@ export function Popover({
handleClose: enableSafePolygon ? safePolygon() : undefined,
});
const click = useClick(context, {enabled: !disabled});
const role = useRole(context, {
role: 'dialog',
});
const dismiss = useDismiss(context, {
enabled: !disabled,
});

const {getReferenceProps, getFloatingProps} = useInteractions([hover, click]);
const interactions = [hover, click, role, dismiss];
const {getReferenceProps} = useInteractions(interactions);

const anchorRef = useForkRef(
setAnchorElement,
React.isValidElement(children) ? getElementRef(children) : undefined,
);
const anchorProps = React.isValidElement<any>(children)
? getReferenceProps(getAnchorProps?.(children.props) ?? children.props)
: getReferenceProps(getAnchorProps?.());
? getReferenceProps(children.props)
: getReferenceProps();
const anchorNode = React.isValidElement<any>(children)
? React.cloneElement(children, {
ref: anchorRef,
Expand All @@ -113,14 +112,12 @@ export function Popover({
{anchorNode}
<Popup
{...restProps}
open={isOpen && !disabled}
setGetAnchorProps={handleSetGetAnchorProps}
open={context.open}
floatingContext={context}
floatingRef={setFloatingElement}
floatingProps={getFloatingProps()}
floatingInteractions={interactions}
autoFocus
modalFocus
role="dialog"
className={b(null, className)}
>
{content}
Expand Down
33 changes: 12 additions & 21 deletions src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import {
useTransitionStatus,
} from '@floating-ui/react';
import type {
ElementProps,
FloatingFocusManagerProps,
FloatingRootContext,
Middleware,
OpenChangeReason,
ReferenceType,
Strategy,
UseFloatingOptions,
UseInteractionsReturn,
UseRoleProps,
} from '@floating-ui/react';

Expand All @@ -36,7 +36,6 @@ import {filterDOMProps} from '../utils/filterDOMProps';

import {PopupArrow} from './PopupArrow';
import {OVERFLOW_PADDING, TRANSITION_DURATION} from './constants';
import {useAnchor} from './hooks';
import i18n from './i18n';
import type {PopupAnchorElement, PopupAnchorRef, PopupOffset, PopupPlacement} from './types';
import {arrowStylesMiddleware, getOffsetOptions, getPlacementOptions} from './utils';
Expand Down Expand Up @@ -69,14 +68,12 @@ export interface PopupProps extends DOMProps, AriaLabelingProps, QAProps {
* @deprecated Use `anchorElement` instead
* */
anchorRef?: PopupAnchorRef;
/** Set up a getter for props that need to be passed to the anchor */
setGetAnchorProps?: (getAnchorProps: UseInteractionsReturn['getReferenceProps']) => void;
/** Floating UI middlewares. If set, they will completely overwrite the default middlewares. */
floatingMiddlewares?: Middleware[];
/** Floating UI context to provide interactions */
floatingContext?: FloatingRootContext<ReferenceType>;
/** Additional floating element props to provide interactions */
floatingProps?: Record<string, unknown>;
floatingInteractions?: ElementProps[];
/** React ref floating element is attached to */
floatingRef?: React.Ref<HTMLDivElement>;
/** Manage focus when opened */
Expand Down Expand Up @@ -145,10 +142,9 @@ export function Popup({
offset: offsetProp = 4,
anchorElement,
anchorRef,
setGetAnchorProps,
floatingMiddlewares,
floatingContext,
floatingProps,
floatingInteractions,
floatingRef,
modalFocus = false,
autoFocus = false,
Expand All @@ -165,7 +161,6 @@ export function Popup({
children,
disablePortal = false,
qa,
id,
role: roleProp,
zIndex = 1000,
onTransitionIn,
Expand All @@ -177,7 +172,6 @@ export function Popup({
const contentRef = React.useRef<HTMLDivElement>(null);
const [arrowElement, setArrowElement] = React.useState<HTMLElement | null>(null);

const anchor = useAnchor(anchorElement, anchorRef);
const {offset} = getOffsetOptions(offsetProp, hasArrow);
const {placement, middleware: placementMiddleware} = getPlacementOptions(
placementProp,
Expand Down Expand Up @@ -221,10 +215,6 @@ export function Popup({
placement: placement,
open,
onOpenChange: handleOpenChange,
elements: {
// @ts-expect-error: Type 'Element | VirtualElement | undefined' is not assignable to type 'Element | null | undefined'.
reference: anchor.element,
},
middleware: floatingMiddlewares ?? [
floatingOffset(offset),
placementMiddleware,
Expand All @@ -239,6 +229,13 @@ export function Popup({
],
});

React.useEffect(() => {
const element = anchorElement === undefined ? anchorRef?.current : anchorElement;
if (element !== undefined && element !== refs.reference.current) {
refs.setReference(element);
}
}, [anchorElement, anchorRef, refs]);

const role = useRole(context, {
enabled: Boolean(roleProp || modalFocus),
role: roleProp ?? (modalFocus ? 'dialog' : undefined),
Expand All @@ -249,11 +246,7 @@ export function Popup({
escapeKey: !disableEscapeKeyDown,
});

const {getReferenceProps, getFloatingProps} = useInteractions([role, dismiss]);

React.useLayoutEffect(() => {
setGetAnchorProps?.(getReferenceProps);
}, [setGetAnchorProps, getReferenceProps]);
const {getFloatingProps} = useInteractions(floatingInteractions ?? [role, dismiss]);

const {isMounted, status} = useTransitionStatus(context, {duration: TRANSITION_DURATION});
const previousStatus = usePrevious(status);
Expand All @@ -262,7 +255,7 @@ export function Popup({
if (isMounted && elements.reference && elements.floating) {
return autoUpdate(elements.reference, elements.floating, update);
}
return;
return undefined;
}, [isMounted, elements, update]);

const initialFocusRef = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -323,7 +316,6 @@ export function Popup({
data-floating-ui-status={status}
aria-modal={modalFocus && isMounted ? true : undefined}
{...getFloatingProps({
...floatingProps,
onTransitionEnd: handleTransitionEnd,
})}
>
Expand All @@ -332,7 +324,6 @@ export function Popup({
className={b({open: isMounted}, className)}
style={style}
data-qa={qa}
id={id}
{...filterDOMProps(restProps)}
>
{hasArrow && (
Expand Down
Loading

0 comments on commit f969ea6

Please sign in to comment.