diff --git a/packages/react/src/components/Dropdown/Dropdown-story.js b/packages/react/src/components/Dropdown/Dropdown-story.js
index 547ff8038464..ddfafe963459 100644
--- a/packages/react/src/components/Dropdown/Dropdown-story.js
+++ b/packages/react/src/components/Dropdown/Dropdown-story.js
@@ -5,12 +5,14 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { withKnobs, boolean, select, text } from '@storybook/addon-knobs';
import Dropdown from '../Dropdown';
import DropdownSkeleton from './Dropdown.Skeleton';
import mdx from './Dropdown.mdx';
+import Modal from '../Modal';
+import Button from '../Button';
const items = [
{
@@ -59,6 +61,7 @@ const types = {
const props = () => ({
id: text('Dropdown ID (id)', 'carbon-dropdown-example'),
size: select('Field size (size)', sizes, undefined) || undefined,
+ detachMenu: boolean('Detach menu (detachMenu)', false),
direction: select('Dropdown direction (direction)', directions, 'bottom'),
label: text('Label (label)', 'Dropdown menu options'),
ariaLabel: text('Aria Label (ariaLabel)', 'Dropdown'),
@@ -141,3 +144,38 @@ export const Skeleton = () => (
);
+
+export const Overflow = () => (
+
+ (item ? item.text : '')}
+ {...{ detachMenu: boolean('Detach menu (detachMenu)', true) }}
+ />
+
+);
+export const OverflowInModal = () => {
+ const [isOpen, setIsOpen] = useState(true);
+ return (
+
+
+
setIsOpen(false)}>
+
+ (item ? item.text : '')}
+ {...{ detachMenu: boolean('Detach menu (detachMenu)', true) }}
+ />
+
+
+
+ );
+};
diff --git a/packages/react/src/components/Dropdown/Dropdown.js b/packages/react/src/components/Dropdown/Dropdown.js
index 1647967d0994..aa280be6c850 100644
--- a/packages/react/src/components/Dropdown/Dropdown.js
+++ b/packages/react/src/components/Dropdown/Dropdown.js
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import { useSelect } from 'downshift';
import { settings } from 'carbon-components';
import cx from 'classnames';
@@ -20,6 +20,11 @@ import { mapDownshiftProps } from '../../tools/createPropAdapter';
import mergeRefs from '../../tools/mergeRefs';
import deprecate from '../../prop-types/deprecate';
+import FloatingMenu, {
+ DIRECTION_TOP,
+ DIRECTION_BOTTOM,
+} from '../../internal/FloatingMenu';
+
const { prefix } = settings;
const defaultItemToString = (item) => {
@@ -33,6 +38,7 @@ const defaultItemToString = (item) => {
const Dropdown = React.forwardRef(function Dropdown(
{
className: containerClassName,
+ detachMenu,
disabled,
direction,
items,
@@ -130,6 +136,31 @@ const Dropdown = React.forwardRef(function Dropdown(
}
}
+ // used for the FloatingMenu
+ const buttonRef = useRef();
+ const [menuBounds, setMenuBounds] = useState({});
+ useEffect(() => {
+ if (buttonRef && buttonRef.current) {
+ setMenuBounds(buttonRef.current.getBoundingClientRect());
+ }
+ }, [isOpen]);
+ const detachMenuWrapper = (menuEl) =>
+ !detachMenu ? (
+ menuEl
+ ) : (
+ document.body}
+ triggerRef={buttonRef}
+ menuDirection={direction == 'top' ? DIRECTION_TOP : DIRECTION_BOTTOM}
+ menuRef={() => {}}
+ menuOffset={() => {}}
+ styles={{ width: menuBounds.width }}>
+ {}}>
+ {menuEl}
+
+
+ );
+
return (
{titleText && (
@@ -162,39 +193,43 @@ const Dropdown = React.forwardRef(function Dropdown(
disabled={disabled}
aria-disabled={disabled}
{...toggleButtonProps}
- ref={mergeRefs(toggleButtonProps.ref, ref)}>
+ ref={mergeRefs(toggleButtonProps.ref, ref, buttonRef)}>
{selectedItem ? itemToString(selectedItem) : label}
-
- {isOpen &&
- items.map((item, index) => {
- const itemProps = getItemProps({ item, index });
- return (
-
- {itemToElement ? (
-
- ) : (
- itemToString(item)
- )}
- {selectedItem === item && (
-
- )}
-
- );
- })}
-
+ {detachMenuWrapper(
+ e.stopPropagation()}
+ {...getMenuProps()}>
+ {isOpen &&
+ items.map((item, index) => {
+ const itemProps = getItemProps({ item, index });
+ return (
+
+ {itemToElement ? (
+
+ ) : (
+ itemToString(item)
+ )}
+ {selectedItem === item && (
+
+ )}
+
+ );
+ })}
+
+ )}
{!inline && !invalid && !warn && helper}
@@ -213,6 +248,11 @@ Dropdown.propTypes = {
*/
className: PropTypes.string,
+ /**
+ * Specify whether the menu should be detached (useful in overflow areas)
+ */
+ detachMenu: PropTypes.bool,
+
/**
* Specify the direction of the dropdown. Can be either top or bottom.
*/
diff --git a/packages/react/src/components/MultiSelect/MultiSelect-story.js b/packages/react/src/components/MultiSelect/MultiSelect-story.js
index 30c6c3648905..61c687e4327b 100644
--- a/packages/react/src/components/MultiSelect/MultiSelect-story.js
+++ b/packages/react/src/components/MultiSelect/MultiSelect-story.js
@@ -67,7 +67,7 @@ const directions = {
'Top ': 'top',
};
-const props = () => ({
+const props = (defaults = {}) => ({
id: text('MultiSelect ID (id)', 'carbon-multiselect-example'),
titleText: text('Title (titleText)', 'Multiselect title'),
helperText: text('Helper text (helperText)', 'This is helper text'),
@@ -76,6 +76,7 @@ const props = () => ({
useTitleInItem: boolean('Show tooltip on hover', false),
type: select('UI type (Only for ``) (type)', types, 'default'),
size: select('Field size (size)', sizes, undefined) || undefined,
+ detachMenu: boolean('Detach menu (detachMenu)', defaults.detachMenu),
direction: select('Dropdown direction (direction)', directions, 'bottom'),
label: text('Label (label)', defaultLabel),
invalid: boolean('Show form validation UI (invalid)', false),
@@ -150,6 +151,25 @@ Default.parameters = {
},
};
+export const Overflow = withReadme(readme, () => {
+ const {
+ listBoxMenuIconTranslationIds,
+ selectionFeedback,
+ ...multiSelectProps
+ } = props({ detachMenu: true });
+ return (
+
+ (item ? item.text : '')}
+ translateWithId={(id) => listBoxMenuIconTranslationIds[id]}
+ selectionFeedback={selectionFeedback}
+ />
+
+ );
+});
+
export const WithInitialSelectedItems = withReadme(readme, () => {
const {
listBoxMenuIconTranslationIds,
diff --git a/packages/react/src/components/MultiSelect/MultiSelect.js b/packages/react/src/components/MultiSelect/MultiSelect.js
index b454718b330e..1b9395c142c6 100644
--- a/packages/react/src/components/MultiSelect/MultiSelect.js
+++ b/packages/react/src/components/MultiSelect/MultiSelect.js
@@ -11,7 +11,7 @@ import cx from 'classnames';
import Downshift, { useSelect } from 'downshift';
import isEqual from 'lodash.isequal';
import PropTypes from 'prop-types';
-import React, { useRef, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import ListBox, { PropTypes as ListBoxPropTypes } from '../ListBox';
import { sortingPropTypes } from './MultiSelectPropTypes';
import { defaultItemToString } from './tools/itemToString';
@@ -21,6 +21,11 @@ import setupGetInstanceId from '../../tools/setupGetInstanceId';
import { mapDownshiftProps } from '../../tools/createPropAdapter';
import mergeRefs from '../../tools/mergeRefs';
+import FloatingMenu, {
+ DIRECTION_TOP,
+ DIRECTION_BOTTOM,
+} from '../../internal/FloatingMenu';
+
const { prefix } = settings;
const noop = () => {};
const getInstanceId = setupGetInstanceId();
@@ -38,6 +43,7 @@ const {
const MultiSelect = React.forwardRef(function MultiSelect(
{
className: containerClassName,
+ detachMenu,
id,
items,
itemToString,
@@ -195,6 +201,35 @@ const MultiSelect = React.forwardRef(function MultiSelect(
const toggleButtonProps = getToggleButtonProps();
+ // used for the FloatingMenu
+ const buttonRef = useRef();
+ const [menuBounds, setMenuBounds] = useState({});
+ useEffect(() => {
+ if (buttonRef && buttonRef.current) {
+ setMenuBounds(buttonRef.current.getBoundingClientRect());
+ }
+ }, [isOpen]);
+ const detachMenuWrapper = (menuEl) =>
+ !detachMenu ? (
+ menuEl
+ ) : (
+ document.body}
+ triggerRef={buttonRef}
+ menuDirection={direction == 'top' ? DIRECTION_TOP : DIRECTION_BOTTOM}
+ menuRef={() => {}}
+ menuOffset={() => {}}
+ styles={{ width: menuBounds.width }}>
+ {}}>
+ {menuEl}
+
+
+ );
+
return (
{titleText && (
@@ -228,7 +263,7 @@ const MultiSelect = React.forwardRef(function MultiSelect(
disabled={disabled}
aria-disabled={disabled}
{...toggleButtonProps}
- ref={mergeRefs(toggleButtonProps.ref, ref)}>
+ ref={mergeRefs(toggleButtonProps.ref, ref, buttonRef)}>
{selectedItems.length > 0 && (
-
- {isOpen &&
- sortItems(items, sortOptions).map((item, index) => {
- const itemProps = getItemProps({
- item,
- // we don't want Downshift to set aria-selected for us
- // we also don't want to set 'false' for reader verbosity's sake
- ['aria-selected']: isChecked ? true : null,
- });
- const itemText = itemToString(item);
- const isChecked =
- selectedItems.filter((selected) => isEqual(selected, item))
- .length > 0;
- return (
-
-
-
- {itemText}
-
-
-
- );
- })}
-
+ {detachMenuWrapper(
+
+ {isOpen &&
+ sortItems(items, sortOptions).map((item, index) => {
+ const itemProps = getItemProps({
+ item,
+ // we don't want Downshift to set aria-selected for us
+ // we also don't want to set 'false' for reader verbosity's sake
+ ['aria-selected']: isChecked ? true : null,
+ });
+ const itemText = itemToString(item);
+ const isChecked =
+ selectedItems.filter((selected) => isEqual(selected, item))
+ .length > 0;
+ return (
+
+
+
+ {itemText}
+
+
+
+ );
+ })}
+
+ )}
{!inline && !invalid && !warn && helperText && (
@@ -290,6 +327,11 @@ MultiSelect.displayName = 'MultiSelect';
MultiSelect.propTypes = {
...sortingPropTypes,
+ /**
+ * Specify whether the menu should be detached (useful in overflow areas)
+ */
+ detachMenu: PropTypes.bool,
+
/**
* Specify the direction of the multiselect dropdown. Can be either top or bottom.
*/