diff --git a/src/components/DropdownMenu/DropdownMenu.scss b/src/components/DropdownMenu/DropdownMenu.scss index b4987b5d2..fca94839a 100644 --- a/src/components/DropdownMenu/DropdownMenu.scss +++ b/src/components/DropdownMenu/DropdownMenu.scss @@ -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; + } + } } diff --git a/src/components/DropdownMenu/DropdownMenuPopup.tsx b/src/components/DropdownMenu/DropdownMenuPopup.tsx index 4050490fa..1a474604c 100644 --- a/src/components/DropdownMenu/DropdownMenuPopup.tsx +++ b/src/components/DropdownMenu/DropdownMenuPopup.tsx @@ -54,25 +54,14 @@ export const DropdownMenuPopup = ({ setActiveMenuPath(path.slice(0, path.length - 1)); }, [setActiveMenuPath, path]); - const handleMouseEnter = React.useCallback( - (event: React.MouseEvent) => { - 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) => { - activateParent(); - (popupProps?.floatingProps?.onMouseLeave as React.MouseEventHandler | undefined)?.( - event, - ); - }, - [activateParent, popupProps], - ); const handleSelect = React.useCallback( (activeItem: DropdownMenuListItem, event: KeyboardEvent) => { if (activeItem.items && activeItem.path) { @@ -148,52 +137,55 @@ export const DropdownMenuPopup = ({ onClose={onClose} placement="bottom-start" {...popupProps} - floatingProps={{ - ...popupProps?.floatingProps, - onMouseEnter: handleMouseEnter, - onMouseLeave: handleMouseLeave, - }} > - {children || ( - - {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 ( - - ); - })} - - )} +
+ {children || ( + + {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 ( + + ); + })} + + )} +
); }; diff --git a/src/components/Popover/Popover.tsx b/src/components/Popover/Popover.tsx index 0022c7688..daa727662 100644 --- a/src/components/Popover/Popover.tsx +++ b/src/components/Popover/Popover.tsx @@ -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'; @@ -63,20 +64,11 @@ export function Popover({ }: 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, + open: isOpen && !disabled, onOpenChange: setIsOpen, elements: { reference: anchorElement, @@ -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(children) - ? getReferenceProps(getAnchorProps?.(children.props) ?? children.props) - : getReferenceProps(getAnchorProps?.()); + ? getReferenceProps(children.props) + : getReferenceProps(); const anchorNode = React.isValidElement(children) ? React.cloneElement(children, { ref: anchorRef, @@ -113,14 +112,12 @@ export function Popover({ {anchorNode} {content} diff --git a/src/components/Popup/Popup.tsx b/src/components/Popup/Popup.tsx index cd2523a72..ff19d4996 100644 --- a/src/components/Popup/Popup.tsx +++ b/src/components/Popup/Popup.tsx @@ -16,6 +16,7 @@ import { useTransitionStatus, } from '@floating-ui/react'; import type { + ElementProps, FloatingFocusManagerProps, FloatingRootContext, Middleware, @@ -23,7 +24,6 @@ import type { ReferenceType, Strategy, UseFloatingOptions, - UseInteractionsReturn, UseRoleProps, } from '@floating-ui/react'; @@ -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'; @@ -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; /** Additional floating element props to provide interactions */ - floatingProps?: Record; + floatingInteractions?: ElementProps[]; /** React ref floating element is attached to */ floatingRef?: React.Ref; /** Manage focus when opened */ @@ -145,10 +142,9 @@ export function Popup({ offset: offsetProp = 4, anchorElement, anchorRef, - setGetAnchorProps, floatingMiddlewares, floatingContext, - floatingProps, + floatingInteractions, floatingRef, modalFocus = false, autoFocus = false, @@ -165,7 +161,6 @@ export function Popup({ children, disablePortal = false, qa, - id, role: roleProp, zIndex = 1000, onTransitionIn, @@ -177,7 +172,6 @@ export function Popup({ const contentRef = React.useRef(null); const [arrowElement, setArrowElement] = React.useState(null); - const anchor = useAnchor(anchorElement, anchorRef); const {offset} = getOffsetOptions(offsetProp, hasArrow); const {placement, middleware: placementMiddleware} = getPlacementOptions( placementProp, @@ -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, @@ -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), @@ -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); @@ -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(null); @@ -323,7 +316,6 @@ export function Popup({ data-floating-ui-status={status} aria-modal={modalFocus && isMounted ? true : undefined} {...getFloatingProps({ - ...floatingProps, onTransitionEnd: handleTransitionEnd, })} > @@ -332,7 +324,6 @@ export function Popup({ className={b({open: isMounted}, className)} style={style} data-qa={qa} - id={id} {...filterDOMProps(restProps)} > {hasArrow && ( diff --git a/src/components/Popup/__stories__/Popup.stories.tsx b/src/components/Popup/__stories__/Popup.stories.tsx index f493d4ac5..f56223ecd 100644 --- a/src/components/Popup/__stories__/Popup.stories.tsx +++ b/src/components/Popup/__stories__/Popup.stories.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import type {Meta, StoryObj} from '@storybook/react'; -import {useVirtualElementRef} from '../../../hooks'; +import {useVirtualElement} from '../../../hooks'; import {Button} from '../../Button'; import {Text} from '../../Text'; import {Popup} from '../Popup'; @@ -19,7 +19,7 @@ type Story = StoryObj; export const Default: Story = { render: function PopupStory(props) { - const anchorRef = React.useRef(null); + const [anchor, setAnchor] = React.useState(null); const [open, setOpen] = React.useState(false); return ( @@ -27,8 +27,8 @@ export const Default: Story = { setOpen(false)} + anchorElement={anchor} + onOpenChange={(isOpen) => setOpen(isOpen)} >
Popup content
@@ -41,7 +41,7 @@ export const Default: Story = { justifyContent: 'center', }} > - @@ -52,7 +52,7 @@ export const Default: Story = { export const Placement: Story = { render: function PopupStory(props) { - const anchorRef = React.useRef(null); + const [anchor, setAnchor] = React.useState(null); const [open, setOpen] = React.useState(true); const contentStyle = {padding: 10}; const placements: PopupPlacement = [ @@ -72,7 +72,7 @@ export const Placement: Story = { return (
{placement}
@@ -106,12 +106,8 @@ export const Position: Story = { render: function PopupStory(props) { const [left, setLeft] = React.useState(0); const [top, setTop] = React.useState(0); + const {anchor, setContextElement} = useVirtualElement({left, top}); - const [contextElement, setContextElement] = React.useState(null); - const anchorRef = useVirtualElementRef({ - rect: {top, left}, - contextElement: contextElement ?? undefined, - }); const [open, setOpen] = React.useState(false); const handleMouseMove = (event: React.MouseEvent) => { @@ -146,7 +142,7 @@ export const Position: Story = { >
Move cursor here - +
Popup content
diff --git a/src/hooks/useVirtualElementRef/index.ts b/src/hooks/useVirtualElementRef/index.ts index 47af80691..8105d1522 100644 --- a/src/hooks/useVirtualElementRef/index.ts +++ b/src/hooks/useVirtualElementRef/index.ts @@ -1,4 +1,4 @@ -export {useVirtualElementRef} from './useVirtualElementRef'; +export {useVirtualElementRef, useVirtualElement} from './useVirtualElementRef'; export type { VirtualElementRect, UseVirtualElementRefProps, diff --git a/src/hooks/useVirtualElementRef/useVirtualElementRef.ts b/src/hooks/useVirtualElementRef/useVirtualElementRef.ts index 6ae7f8d42..f8a7a388f 100644 --- a/src/hooks/useVirtualElementRef/useVirtualElementRef.ts +++ b/src/hooks/useVirtualElementRef/useVirtualElementRef.ts @@ -62,3 +62,37 @@ export function useVirtualElementRef( return ref; } + +export function useVirtualElement(rect: VirtualElementRect) { + const rectRef = React.useRef(rect); + const [anchor, setAnchor] = React.useState({ + getBoundingClientRect() { + const {top = 0, left = 0, right = left, bottom = top} = rectRef.current; + return {top, left, bottom, right, width: right - left, height: bottom - top} as DOMRect; + }, + contextElement: undefined, + }); + const setContextElement = React.useCallback((node: HTMLDivElement | null) => { + setAnchor({ + getBoundingClientRect() { + const {top = 0, left = 0, right = left, bottom = top} = rectRef.current; + return { + top, + left, + bottom, + right, + width: right - left, + height: bottom - top, + } as DOMRect; + }, + contextElement: node ?? undefined, + }); + }, []); + + const {top, left, bottom, right} = rect; + React.useEffect(() => { + rectRef.current = {top, left, bottom, right}; + }, [top, left, bottom, right]); + + return {anchor, setContextElement}; +}