diff --git a/packages/react/src/components/ComboBox/ComboBox.stories.js b/packages/react/src/components/ComboBox/ComboBox.stories.js index dde72b5b4618..44551650bd08 100644 --- a/packages/react/src/components/ComboBox/ComboBox.stories.js +++ b/packages/react/src/components/ComboBox/ComboBox.stories.js @@ -74,6 +74,22 @@ export const Default = () => ( ); +export const ExperimentalAutoAlign = () => ( +
+
+ {}} + id="carbon-combobox" + items={items} + itemToString={(item) => (item ? item.text : '')} + titleText="ComboBox title" + helperText="Combobox helper text" + autoAlign={true} + /> +
+
+); + export const AllowCustomValue = () => { const filterItems = (menu) => { return menu?.item?.toLowerCase().includes(menu?.inputValue?.toLowerCase()); diff --git a/packages/react/src/components/ComboBox/ComboBox.tsx b/packages/react/src/components/ComboBox/ComboBox.tsx index a6c85a6323c9..a3963ab1e16c 100644 --- a/packages/react/src/components/ComboBox/ComboBox.tsx +++ b/packages/react/src/components/ComboBox/ComboBox.tsx @@ -43,6 +43,7 @@ import mergeRefs from '../../tools/mergeRefs'; import deprecate from '../../prop-types/deprecate'; import { usePrefix } from '../../internal/usePrefix'; import { FormContext } from '../FluidForm'; +import { useFloating, flip, autoUpdate } from '@floating-ui/react'; const { InputBlur, @@ -150,6 +151,11 @@ export interface ComboBoxProps */ ariaLabel?: string; + /** + * Will auto-align the dropdown on first render if it is not visible. This prop is currently experimental and is subject to future changes. + */ + autoAlign?: boolean; + /** * An optional className to add to the container node */ @@ -313,6 +319,7 @@ const ComboBox = forwardRef( const { ['aria-label']: ariaLabel = 'Choose an item', ariaLabel: deprecatedAriaLabel, + autoAlign = false, className: containerClassName, direction = 'bottom', disabled = false, @@ -342,6 +349,41 @@ const ComboBox = forwardRef( slug, ...rest } = props; + const { refs, floatingStyles } = useFloating( + autoAlign + ? { + // placement: direction, + + // 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: 'none', + }), + ], + whileElementsMounted: autoUpdate, + } + : {} // When autoAlign is turned off, floating-ui will not be used + ); + const parentWidth = (refs?.reference?.current as HTMLElement)?.clientWidth; + useEffect(() => { + if (autoAlign) { + Object.keys(floatingStyles).forEach((style) => { + if (refs.floating.current) { + refs.floating.current.style[style] = floatingStyles[style]; + } + }); + + if (parentWidth && refs.floating.current) { + refs.floating.current.style.width = parentWidth + 'px'; + } + } + }, [floatingStyles, autoAlign, refs.floating, parentWidth]); const prefix = usePrefix(); const { isFluid } = useContext(FormContext); const textInput = useRef(null); @@ -631,6 +673,7 @@ const ComboBox = forwardRef( size={size} warn={warn} warnText={warnText} + ref={refs.setReference} warnTextId={warnTextId}>
{normalizedSlug} @@ -822,6 +866,11 @@ ComboBox.propTypes = { 'This prop syntax has been deprecated. Please use the new `aria-label`.' ), + /** + * Will auto-align the dropdown on first render if it is not visible. This prop is currently experimental and is subject to future changes. + */ + autoAlign: PropTypes.bool, + /** * An optional className to add to the container node */