From aa51092c234a093c48648e67df1a56c5815dea92 Mon Sep 17 00:00:00 2001
From: Riddhi Bansal <41935566+riddhybansal@users.noreply.github.com>
Date: Mon, 3 Jun 2024 21:37:59 +0530
Subject: [PATCH] fix: floating ui on combobutton (#16586)
---
.../ComboButton/ComboButton.stories.js | 16 ++++
.../src/components/ComboButton/index.tsx | 83 ++++++++++++-------
2 files changed, 70 insertions(+), 29 deletions(-)
diff --git a/packages/react/src/components/ComboButton/ComboButton.stories.js b/packages/react/src/components/ComboButton/ComboButton.stories.js
index d4323b6ac49f..fed5415c0d8e 100644
--- a/packages/react/src/components/ComboButton/ComboButton.stories.js
+++ b/packages/react/src/components/ComboButton/ComboButton.stories.js
@@ -36,6 +36,22 @@ export const Default = () => (
);
+export const ExperimentalAutoAlign = () => (
+
+);
+
export const WithDanger = () => (
diff --git a/packages/react/src/components/ComboButton/index.tsx b/packages/react/src/components/ComboButton/index.tsx
index a2123440fa9e..c1e1adf6fe1f 100644
--- a/packages/react/src/components/ComboButton/index.tsx
+++ b/packages/react/src/components/ComboButton/index.tsx
@@ -5,25 +5,36 @@
* LICENSE file in the root directory of this source tree.
*/
-import React, { useRef, useState } from 'react';
+import React, { useLayoutEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-
import { ChevronDown } from '@carbon/icons-react';
import { IconButton } from '../IconButton';
import Button from '../Button';
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 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];
}
@@ -52,7 +63,7 @@ interface ComboButtonProps {
/**
* Experimental property. Specify how the menu should align with the button element
*/
- menuAlignment?: React.ComponentProps['menuAlignment'];
+ menuAlignment?: MenuAlignment;
/**
* Provide an optional function to be called when the primary action element is clicked.
@@ -95,22 +106,49 @@ const ComboButton = React.forwardRef(
const id = useId('combobutton');
const prefix = usePrefix();
const containerRef = useRef(null);
- const menuRef = useRef>(null);
- const ref = useMergedRefs([forwardRef, containerRef]);
- 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, containerRef, refs.setReference);
const {
open,
- x,
- y,
handleClick: hookOnClick,
handleMousedown: handleTriggerMousedown,
handleClose,
} = useAttachedMenu(containerRef);
+ useLayoutEffect(() => {
+ Object.keys(floatingStyles).forEach((style) => {
+ if (refs.floating.current) {
+ refs.floating.current.style[style] = floatingStyles[style];
+ }
+ });
+ }, [floatingStyles, refs.floating, middlewareData, placement, open]);
function handleTriggerClick() {
if (containerRef.current) {
- const { width: w } = containerRef.current.getBoundingClientRect();
- setWidth(w);
hookOnClick();
}
}
@@ -121,17 +159,6 @@ const ComboButton = React.forwardRef(
}
}
- 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}--combo-button__container`,
`${prefix}--combo-button__container--${size}`,
@@ -164,6 +191,7 @@ const ComboButton = React.forwardRef(
(
containerRef={containerRef}
menuAlignment={menuAlignment}
className={menuClasses}
- ref={menuRef}
+ ref={refs.setFloating}
id={id}
label={t('carbon.combo-button.additional-actions')}
mode="basic"
size={size}
open={open}
- onClose={handleClose}
- onOpen={handleOpen}
- x={x}
- y={[y[0] - spacing, y[1] + spacing]}>
+ onClose={handleClose}>
{children}