diff --git a/packages/react/src/components/OverflowMenu/OverflowMenu.featureflag.stories.js b/packages/react/src/components/OverflowMenu/OverflowMenu.featureflag.stories.js index a2fa40c5f05a..35049b334d9b 100644 --- a/packages/react/src/components/OverflowMenu/OverflowMenu.featureflag.stories.js +++ b/packages/react/src/components/OverflowMenu/OverflowMenu.featureflag.stories.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; +import React, { useRef, useEffect } from 'react'; import { action } from '@storybook/addon-actions'; import { ArrowsVertical } from '@carbon/icons-react'; @@ -62,6 +62,36 @@ export const _OverflowMenu = () => { ); }; +export const AutoAlign = () => { + const ref = useRef(); + + useEffect(() => { + console.log(ref); + ref?.current?.scrollIntoView({ block: 'center', inline: 'center' }); + }); + + return ( +
+
+ + + + + + + + +
+
+ ); +}; + export const Nested = () => { return ( diff --git a/packages/react/src/components/OverflowMenu/next/index.tsx b/packages/react/src/components/OverflowMenu/next/index.tsx index 73d70931d2d9..4262b53df804 100644 --- a/packages/react/src/components/OverflowMenu/next/index.tsx +++ b/packages/react/src/components/OverflowMenu/next/index.tsx @@ -9,13 +9,16 @@ import React, { type ComponentType, type FunctionComponent, useRef, + useEffect, } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { OverflowMenuVertical } from '@carbon/icons-react'; +import { useFloating, flip, autoUpdate } from '@floating-ui/react'; import { IconButton } from '../../IconButton'; import { Menu } from '../../Menu'; +import mergeRefs from '../../../tools/mergeRefs'; import { useId } from '../../../internal/useId'; import { usePrefix } from '../../../internal/usePrefix'; @@ -24,6 +27,11 @@ import { useAttachedMenu } from '../../../internal/useAttachedMenu'; const defaultSize = 'md'; interface OverflowMenuProps { + /** + * **Experimental**: Will attempt to automatically align the floating element to avoid collisions with the viewport and being clipped by ancestor elements. + */ + autoAlign?: boolean; + /** * A collection of MenuItems to be rendered within this OverflowMenu. */ @@ -71,6 +79,7 @@ interface OverflowMenuProps { const OverflowMenu = React.forwardRef( function OverflowMenu( { + autoAlign = false, children, className, label = 'Options', @@ -82,10 +91,39 @@ const OverflowMenu = React.forwardRef( }, forwardRef ) { + const { refs, floatingStyles, placement, middlewareData } = useFloating( + autoAlign + ? { + placement: menuAlignment, + + // The floating element is positioned relative to its nearest + // containing block (usually the viewport). It will in many cases also + // “break” the floating element out of a clipping ancestor. + // https://floating-ui.com/docs/misc#clipping + strategy: 'fixed', + + // Middleware order matters, arrow should be last + middleware: [ + flip({ + fallbackAxisSideDirection: 'start', + fallbackPlacements: [ + 'top-start', + 'top-end', + 'bottom-start', + 'bottom-end', + ], + }), + ], + whileElementsMounted: autoUpdate, + } + : {} // When autoAlign is turned off, floating-ui will not be used + ); + const id = useId('overflowmenu'); const prefix = usePrefix(); const triggerRef = useRef(null); + const { open, x, @@ -94,6 +132,22 @@ const OverflowMenu = React.forwardRef( handleMousedown, handleClose, } = useAttachedMenu(triggerRef); + useEffect(() => { + if (autoAlign) { + Object.keys(floatingStyles).forEach((style) => { + if (refs.floating.current) { + refs.floating.current.style[style] = floatingStyles[style]; + } + }); + } + }, [ + floatingStyles, + autoAlign, + refs.floating, + open, + placement, + middlewareData, + ]); function handleTriggerClick() { if (triggerRef.current) { @@ -118,6 +172,8 @@ const OverflowMenu = React.forwardRef( size !== defaultSize && `${prefix}--overflow-menu--${size}` ); + const floatingRef = mergeRefs(triggerRef, refs.setReference); + return (
( className={triggerClasses} onClick={handleTriggerClick} onMouseDown={handleMousedown} - ref={triggerRef} + ref={floatingRef} label={label} align={tooltipAlignment}> ( } ); OverflowMenu.propTypes = { + /** + * **Experimental**: Will attempt to automatically align the floating element to avoid collisions with the viewport and being clipped by ancestor elements. + */ + autoAlign: PropTypes.bool, /** * A collection of MenuItems to be rendered within this OverflowMenu. */