Skip to content

Commit

Permalink
fix: added floating UI to menu and menuButton (#16543)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
riddhybansal authored Jun 11, 2024
1 parent cb30e67 commit 6ef7440
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 57 deletions.
9 changes: 1 addition & 8 deletions packages/react/src/components/ComboButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down
35 changes: 20 additions & 15 deletions packages/react/src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ interface MenuProps extends React.HTMLAttributes<HTMLUListElement> {
* 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<HTMLUListElement, MenuProps>(function Menu(
Expand All @@ -112,6 +114,7 @@ const Menu = forwardRef<HTMLUListElement, MenuProps>(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,
Expand Down Expand Up @@ -179,22 +182,23 @@ const Menu = forwardRef<HTMLUListElement, MenuProps>(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) {
Expand Down Expand Up @@ -416,7 +420,8 @@ const Menu = forwardRef<HTMLUListElement, MenuProps>(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,
}
);
Expand Down
16 changes: 15 additions & 1 deletion packages/react/src/components/MenuButton/MenuButton.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,21 @@ export const Default = () => (
<MenuItem label="Third action" disabled />
</MenuButton>
);

export const ExperimentalAutoAlign = () => (
<div style={{ width: '5000px', height: '5000px' }}>
<div
style={{
position: 'absolute',
bottom: '20px',
}}>
<MenuButton label="Actions">
<MenuItem label="First action" />
<MenuItem label="Second action that is a longer item to test overflow and title." />
<MenuItem label="Third action" disabled />
</MenuButton>
</div>
</div>
);
export const WithDanger = () => (
<MenuButton label="Actions">
<MenuItem label="First action" />
Expand Down
88 changes: 55 additions & 33 deletions packages/react/src/components/MenuButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import React, {
ComponentProps,
forwardRef,
ReactNode,
useLayoutEffect,
useRef,
useState,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -93,38 +99,55 @@ const MenuButton = forwardRef<HTMLDivElement, MenuButtonProps>(
) {
const id = useId('MenuButton');
const prefix = usePrefix();

const triggerRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLUListElement>(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
Expand All @@ -143,6 +166,7 @@ const MenuButton = forwardRef<HTMLDivElement, MenuButtonProps>(
aria-owns={open ? id : undefined}
className={containerClasses}>
<Button
ref={refs.setReference}
className={triggerClasses}
size={size}
tabIndex={tabIndex}
Expand All @@ -160,16 +184,14 @@ const MenuButton = forwardRef<HTMLDivElement, MenuButtonProps>(
containerRef={triggerRef}
menuAlignment={menuAlignment}
className={menuClasses}
ref={menuRef}
ref={refs.setFloating}
id={id}
legacyAutoalign={false}
label={label}
mode="basic"
size={size}
open={open}
onClose={handleClose}
onOpen={handleOpen}
x={x}
y={[y[0] - spacing, y[1] + spacing]}>
onClose={handleClose}>
{children}
</Menu>
</div>
Expand Down

0 comments on commit 6ef7440

Please sign in to comment.