Skip to content

Commit

Permalink
feat: Add floating ui to new overflow menu (#16786)
Browse files Browse the repository at this point in the history
* feat: initial commit

* feat: added floating ui

* feat: added floating ui

* fix: adding alignments

* fix: fixed menualignment proptypes

* fix: update fallback placements as per PR suggestions

---------

Co-authored-by: Taylor Jones <[email protected]>
  • Loading branch information
preetibansalui and tay1orjones authored Jun 21, 2024
1 parent 854c889 commit 3dc88a8
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<div style={{ width: '5000px', height: '5000px' }}>
<div
style={{
position: 'absolute',
top: '2500px',
left: '2500px',
}}
ref={ref}>
<OverflowMenu autoAlign={true}>
<MenuItem label="Stop app" />
<MenuItem label="Restart app" />
<MenuItem label="Rename app" />
<MenuItem label="Edit routes and access" />
<MenuItemDivider />
<MenuItem label="Delete app" kind="danger" />
</OverflowMenu>
</div>
</div>
);
};

export const Nested = () => {
return (
<OverflowMenu>
Expand Down
64 changes: 63 additions & 1 deletion packages/react/src/components/OverflowMenu/next/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
*/
Expand Down Expand Up @@ -71,6 +79,7 @@ interface OverflowMenuProps {
const OverflowMenu = React.forwardRef<HTMLDivElement, OverflowMenuProps>(
function OverflowMenu(
{
autoAlign = false,
children,
className,
label = 'Options',
Expand All @@ -82,10 +91,39 @@ const OverflowMenu = React.forwardRef<HTMLDivElement, OverflowMenuProps>(
},
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<HTMLDivElement>(null);

const {
open,
x,
Expand All @@ -94,6 +132,22 @@ const OverflowMenu = React.forwardRef<HTMLDivElement, OverflowMenuProps>(
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) {
Expand All @@ -118,6 +172,8 @@ const OverflowMenu = React.forwardRef<HTMLDivElement, OverflowMenuProps>(
size !== defaultSize && `${prefix}--overflow-menu--${size}`
);

const floatingRef = mergeRefs(triggerRef, refs.setReference);

return (
<div
{...rest}
Expand All @@ -131,17 +187,19 @@ const OverflowMenu = React.forwardRef<HTMLDivElement, OverflowMenuProps>(
className={triggerClasses}
onClick={handleTriggerClick}
onMouseDown={handleMousedown}
ref={triggerRef}
ref={floatingRef}
label={label}
align={tooltipAlignment}>
<IconElement className={`${prefix}--overflow-menu__icon`} />
</IconButton>
<Menu
containerRef={triggerRef}
ref={refs.setFloating}
menuAlignment={menuAlignment}
className={menuClasses}
id={id}
size={size}
legacyAutoalign={!autoAlign}
open={open}
onClose={handleClose}
x={x}
Expand All @@ -154,6 +212,10 @@ const OverflowMenu = React.forwardRef<HTMLDivElement, OverflowMenuProps>(
}
);
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.
*/
Expand Down

0 comments on commit 3dc88a8

Please sign in to comment.