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. */