From 6ef74406eda447bc80998ff2260beefe8a559048 Mon Sep 17 00:00:00 2001 From: Riddhi Bansal <41935566+riddhybansal@users.noreply.github.com> Date: Tue, 11 Jun 2024 21:51:24 +0530 Subject: [PATCH] fix: added floating UI to menu and menuButton (#16543) * fix: added floating UI to menu and menuButton * fix: update snapshots * fix: cross axis placement * fix: menu alignment * fix: removed unwanted styles and autoalign prop * fix: errors in build * fix: updated snapshots --- .../src/components/ComboButton/index.tsx | 9 +- packages/react/src/components/Menu/Menu.tsx | 35 ++++---- .../MenuButton/MenuButton.stories.js | 16 +++- .../react/src/components/MenuButton/index.tsx | 88 ++++++++++++------- 4 files changed, 91 insertions(+), 57 deletions(-) diff --git a/packages/react/src/components/ComboButton/index.tsx b/packages/react/src/components/ComboButton/index.tsx index c1e1adf6fe1f..9a8d362da8de 100644 --- a/packages/react/src/components/ComboButton/index.tsx +++ b/packages/react/src/components/ComboButton/index.tsx @@ -22,19 +22,12 @@ import { autoUpdate, } from '@floating-ui/react'; import mergeRefs from '../../tools/mergeRefs'; +import { MenuAlignment } from '../MenuButton'; const defaultTranslations = { 'carbon.combo-button.additional-actions': 'Additional actions', }; -export type MenuAlignment = - | 'top' - | 'top-start' - | 'top-end' - | 'bottom' - | 'bottom-start' - | 'bottom-end'; - function defaultTranslateWithId(messageId: string) { return defaultTranslations[messageId]; } diff --git a/packages/react/src/components/Menu/Menu.tsx b/packages/react/src/components/Menu/Menu.tsx index 1e3d14f80305..c93513c8bb11 100644 --- a/packages/react/src/components/Menu/Menu.tsx +++ b/packages/react/src/components/Menu/Menu.tsx @@ -98,6 +98,8 @@ interface MenuProps extends React.HTMLAttributes { * Specify the y position of the Menu. Either pass a single number or an array with two numbers describing your activator's boundaries ([y1, y2]) */ y?: number | [number, number]; + + legacyAutoalign?: boolean; } const Menu = forwardRef(function Menu( @@ -112,6 +114,7 @@ const Menu = forwardRef(function Menu( onOpen, open, size = 'sm', + legacyAutoalign = 'true', // TODO: #16004 // eslint-disable-next-line ssr-friendly/no-dom-globals-in-react-fc target = document.body, @@ -179,22 +182,23 @@ const Menu = forwardRef(function Menu( function handleOpen() { if (menu.current) { focusReturn.current = document.activeElement as HTMLElement; - - const pos = calculatePosition(); - if ( - (document?.dir === 'rtl' || direction === 'rtl') && - !rest?.id?.includes('MenuButton') - ) { - menu.current.style.insetInlineStart = `initial`; - menu.current.style.insetInlineEnd = `${pos[0]}px`; - } else { - menu.current.style.insetInlineStart = `${pos[0]}px`; - menu.current.style.insetInlineEnd = `initial`; + if (legacyAutoalign) { + const pos = calculatePosition(); + if ( + (document?.dir === 'rtl' || direction === 'rtl') && + !rest?.id?.includes('MenuButton') + ) { + menu.current.style.insetInlineStart = `initial`; + menu.current.style.insetInlineEnd = `${pos[0]}px`; + } else { + menu.current.style.insetInlineStart = `${pos[0]}px`; + menu.current.style.insetInlineEnd = `initial`; + } + + menu.current.style.insetBlockStart = `${pos[1]}px`; + setPosition(pos); } - menu.current.style.insetBlockStart = `${pos[1]}px`; - setPosition(pos); - menu.current.focus(); if (onOpen) { @@ -416,7 +420,8 @@ const Menu = forwardRef(function Menu( [`${prefix}--menu--box-shadow-top`]: menuAlignment && menuAlignment.slice(0, 3) === 'top', [`${prefix}--menu--open`]: open, - [`${prefix}--menu--shown`]: position[0] >= 0 && position[1] >= 0, + [`${prefix}--menu--shown`]: + (open && !legacyAutoalign) || (position[0] >= 0 && position[1] >= 0), [`${prefix}--menu--with-icons`]: childContext.state.hasIcons, } ); diff --git a/packages/react/src/components/MenuButton/MenuButton.stories.js b/packages/react/src/components/MenuButton/MenuButton.stories.js index c765bc3cef21..09b52191faa2 100644 --- a/packages/react/src/components/MenuButton/MenuButton.stories.js +++ b/packages/react/src/components/MenuButton/MenuButton.stories.js @@ -36,7 +36,21 @@ export const Default = () => ( ); - +export const ExperimentalAutoAlign = () => ( +
+
+ + + + + +
+
+); export const WithDanger = () => ( diff --git a/packages/react/src/components/MenuButton/index.tsx b/packages/react/src/components/MenuButton/index.tsx index cf2ab9e874de..dfba2ae64bc5 100644 --- a/packages/react/src/components/MenuButton/index.tsx +++ b/packages/react/src/components/MenuButton/index.tsx @@ -9,8 +9,8 @@ import React, { ComponentProps, forwardRef, ReactNode, + useLayoutEffect, useRef, - useState, } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -21,13 +21,25 @@ import { Menu } from '../Menu'; import { useAttachedMenu } from '../../internal/useAttachedMenu'; import { useId } from '../../internal/useId'; -import { useMergedRefs } from '../../internal/useMergedRefs'; import { usePrefix } from '../../internal/usePrefix'; +import { + useFloating, + flip, + size as floatingSize, + autoUpdate, +} from '@floating-ui/react'; +import mergeRefs from '../../tools/mergeRefs'; -const spacing = 0; // top and bottom spacing between the button and the menu. in px const validButtonKinds = ['primary', 'tertiary', 'ghost']; const defaultButtonKind = 'primary'; +export type MenuAlignment = + | 'top' + | 'top-start' + | 'top-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end'; export interface MenuButtonProps extends ComponentProps<'div'> { /** * A collection of MenuItems to be rendered as actions for this MenuButton. @@ -57,13 +69,7 @@ export interface MenuButtonProps extends ComponentProps<'div'> { /** * Experimental property. Specify how the menu should align with the button element */ - menuAlignment: - | 'top' - | 'top-start' - | 'top-end' - | 'bottom' - | 'bottom-start' - | 'bottom-end'; + menuAlignment: MenuAlignment; /** * Specify the size of the button and menu. @@ -93,38 +99,55 @@ const MenuButton = forwardRef( ) { const id = useId('MenuButton'); const prefix = usePrefix(); - const triggerRef = useRef(null); - const menuRef = useRef(null); - const ref = useMergedRefs([forwardRef, triggerRef]); - const [width, setWidth] = useState(0); + const middlewares = [flip({ crossAxis: false })]; + + if (menuAlignment === 'bottom' || menuAlignment === 'top') { + middlewares.push( + floatingSize({ + apply({ rects, elements }) { + Object.assign(elements.floating.style, { + width: `${rects.reference.width}px`, + }); + }, + }) + ); + } + const { refs, floatingStyles, placement, middlewareData } = useFloating({ + 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: middlewares, + whileElementsMounted: autoUpdate, + }); + const ref = mergeRefs(forwardRef, triggerRef); const { open, - x, - y, handleClick: hookOnClick, handleMousedown, handleClose, } = useAttachedMenu(triggerRef); + useLayoutEffect(() => { + Object.keys(floatingStyles).forEach((style) => { + if (refs.floating.current) { + refs.floating.current.style[style] = floatingStyles[style]; + } + }); + }, [floatingStyles, refs.floating, middlewareData, placement, open]); + function handleClick() { if (triggerRef.current) { - const { width: w } = triggerRef.current.getBoundingClientRect(); - setWidth(w); hookOnClick(); } } - function handleOpen() { - if (menuRef.current) { - menuRef.current.style.inlineSize = `${width}px`; - menuRef.current.style.minInlineSize = `${width}px`; - if (menuAlignment !== 'bottom' && menuAlignment !== 'top') { - menuRef.current.style.inlineSize = `fit-content`; - } - } - } - const containerClasses = classNames( `${prefix}--menu-button__container`, className @@ -143,6 +166,7 @@ const MenuButton = forwardRef( aria-owns={open ? id : undefined} className={containerClasses}>