Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add FloatingMenu to dropdown #7928

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion packages/react/src/components/Dropdown/Dropdown-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -141,3 +144,38 @@ export const Skeleton = () => (
<DropdownSkeleton />
</div>
);

export const Overflow = () => (
<div style={{ width: 300, height: 100, overflow: 'auto', resize: 'both' }}>
<Dropdown
id="default"
titleText="Dropdown label"
helperText="This is some helper text"
label="Dropdown menu options"
items={items}
itemToString={(item) => (item ? item.text : '')}
{...{ detachMenu: boolean('Detach menu (detachMenu)', true) }}
/>
</div>
);
export const OverflowInModal = () => {
const [isOpen, setIsOpen] = useState(true);
return (
<div>
<Button onClick={() => setIsOpen(!isOpen)}>Open</Button>
<Modal open={isOpen} onRequestClose={() => setIsOpen(false)}>
<div style={{ width: 300, height: 100, overflow: 'auto' }}>
<Dropdown
id="default"
titleText="Dropdown label"
helperText="This is some helper text"
label="Dropdown menu options"
items={items}
itemToString={(item) => (item ? item.text : '')}
{...{ detachMenu: boolean('Detach menu (detachMenu)', true) }}
/>
</div>
</Modal>
</div>
);
};
98 changes: 69 additions & 29 deletions packages/react/src/components/Dropdown/Dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) => {
Expand All @@ -33,6 +38,7 @@ const defaultItemToString = (item) => {
const Dropdown = React.forwardRef(function Dropdown(
{
className: containerClassName,
detachMenu,
disabled,
direction,
items,
Expand Down Expand Up @@ -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
) : (
<FloatingMenu
target={() => document.body}
triggerRef={buttonRef}
menuDirection={direction == 'top' ? DIRECTION_TOP : DIRECTION_BOTTOM}
menuRef={() => {}}
menuOffset={() => {}}
styles={{ width: menuBounds.width }}>
<div role="presentation" {...getMenuProps()} onKeyDown={() => {}}>
{menuEl}
</div>
</FloatingMenu>
);

return (
<div className={wrapperClasses} {...other}>
{titleText && (
Expand Down Expand Up @@ -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)}>
<span className={`${prefix}--list-box__label`}>
{selectedItem ? itemToString(selectedItem) : label}
</span>
<ListBox.MenuIcon isOpen={isOpen} translateWithId={translateWithId} />
</button>
<ListBox.Menu {...getMenuProps()}>
{isOpen &&
items.map((item, index) => {
const itemProps = getItemProps({ item, index });
return (
<ListBox.MenuItem
key={itemProps.id}
isActive={selectedItem === item}
isHighlighted={
highlightedIndex === index || selectedItem === item
}
title={itemToElement ? item.text : itemToString(item)}
{...itemProps}>
{itemToElement ? (
<ItemToElement key={itemProps.id} {...item} />
) : (
itemToString(item)
)}
{selectedItem === item && (
<Checkmark16
className={`${prefix}--list-box__menu-item__selected-icon`}
/>
)}
</ListBox.MenuItem>
);
})}
</ListBox.Menu>
{detachMenuWrapper(
<ListBox.Menu
onMouseDown={(e) => e.stopPropagation()}
{...getMenuProps()}>
{isOpen &&
items.map((item, index) => {
const itemProps = getItemProps({ item, index });
return (
<ListBox.MenuItem
key={itemProps.id}
isActive={selectedItem === item}
isHighlighted={
highlightedIndex === index || selectedItem === item
}
title={itemToElement ? item.text : itemToString(item)}
{...itemProps}>
{itemToElement ? (
<ItemToElement key={itemProps.id} {...item} />
) : (
itemToString(item)
)}
{selectedItem === item && (
<Checkmark16
className={`${prefix}--list-box__menu-item__selected-icon`}
/>
)}
</ListBox.MenuItem>
);
})}
</ListBox.Menu>
)}
</ListBox>
{!inline && !invalid && !warn && helper}
</div>
Expand All @@ -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.
*/
Expand Down
22 changes: 21 additions & 1 deletion packages/react/src/components/MultiSelect/MultiSelect-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -76,6 +76,7 @@ const props = () => ({
useTitleInItem: boolean('Show tooltip on hover', false),
type: select('UI type (Only for `<MultiSelect>`) (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),
Expand Down Expand Up @@ -150,6 +151,25 @@ Default.parameters = {
},
};

export const Overflow = withReadme(readme, () => {
const {
listBoxMenuIconTranslationIds,
selectionFeedback,
...multiSelectProps
} = props({ detachMenu: true });
return (
<div style={{ width: 300, height: 100, overflow: 'auto', resize: 'both' }}>
<MultiSelect
{...multiSelectProps}
items={items}
itemToString={(item) => (item ? item.text : '')}
translateWithId={(id) => listBoxMenuIconTranslationIds[id]}
selectionFeedback={selectionFeedback}
/>
</div>
);
});

export const WithInitialSelectedItems = withReadme(readme, () => {
const {
listBoxMenuIconTranslationIds,
Expand Down
Loading