Skip to content

Commit

Permalink
fix: floating ui on combobutton
Browse files Browse the repository at this point in the history
  • Loading branch information
riddhybansal committed May 28, 2024
1 parent cfb682c commit 53397bc
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 29 deletions.
16 changes: 16 additions & 0 deletions packages/react/src/components/ComboButton/ComboButton.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ export const Default = () => (
</ComboButton>
);

export const ExperimentalAutoAlign = () => (
<div style={{ width: '5000px', height: '5000px' }}>
<div
style={{
position: 'absolute',
bottom: '20px',
}}>
<ComboButton label="Primary action">
<MenuItem label="Second action with a long label description" />
<MenuItem label="Third action" />
<MenuItem label="Fourth action" disabled />
</ComboButton>
</div>{' '}
</div>
);

export const WithDanger = () => (
<ComboButton label="Primary action">
<MenuItem label="Second action with a long label description" />
Expand Down
83 changes: 54 additions & 29 deletions packages/react/src/components/ComboButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down Expand Up @@ -52,7 +63,7 @@ interface ComboButtonProps {
/**
* Experimental property. Specify how the menu should align with the button element
*/
menuAlignment?: React.ComponentProps<typeof Menu>['menuAlignment'];
menuAlignment?: MenuAlignment;

/**
* Provide an optional function to be called when the primary action element is clicked.
Expand Down Expand Up @@ -95,22 +106,49 @@ const ComboButton = React.forwardRef<HTMLDivElement, ComboButtonProps>(
const id = useId('combobutton');
const prefix = usePrefix();
const containerRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<React.ComponentRef<typeof Menu>>(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();
}
}
Expand All @@ -121,17 +159,6 @@ const ComboButton = React.forwardRef<HTMLDivElement, ComboButtonProps>(
}
}

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}`,
Expand Down Expand Up @@ -164,6 +191,7 @@ const ComboButton = React.forwardRef<HTMLDivElement, ComboButtonProps>(
</Button>
</div>
<IconButton
ref={refs.setReference}
className={triggerClasses}
label={t('carbon.combo-button.additional-actions')}
size={size}
Expand All @@ -180,16 +208,13 @@ const ComboButton = React.forwardRef<HTMLDivElement, ComboButtonProps>(
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}
</Menu>
</div>
Expand Down

0 comments on commit 53397bc

Please sign in to comment.