Skip to content

Commit

Permalink
feat(FloatingMenu): support for specifying target container
Browse files Browse the repository at this point in the history
This change makes `<FloatingMenu>` support an element to put the menu into,
which is, the ancestor of the trigger button with `data-floating-menu-container` attribute.

This change also removes code for React15-, which we dropped the support from 9.x codebase.

Refs carbon-design-system/carbon#910.
Refs carbon-design-system/carbon#911.
  • Loading branch information
asudoh committed Jun 28, 2018
1 parent 1eb620d commit 08d0a1b
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 68 deletions.
6 changes: 6 additions & 0 deletions .storybook/Container.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class Container extends Component {
return (
<React.StrictMode>
<div
data-floating-menu-container
role="main"
style={{
padding: '3em',
Expand All @@ -18,6 +19,11 @@ export default class Container extends Component {
}}>
{story()}
</div>
<input
aria-label="inpute-text-offleft"
type="text"
class="bx--visually-hidden"
/>
</React.StrictMode>
);
}
Expand Down
28 changes: 28 additions & 0 deletions src/components/OverflowMenu/OverflowMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ const on = (element, ...args) => {
};
};

/**
* @param {Element} elem An element.
* @param {string} selector An query selector.
* @returns {Element} The ancestor of the given element matching the given selector.
* @private
*/
const closest = (elem, selector) => {
const doc = elem.ownerDocument;
for (
let traverse = elem;
traverse && traverse !== doc;
traverse = traverse.parentNode
) {
if (traverse.matches(selector)) {
return traverse;
}
}
return null;
};

/**
* @param {Element} menuBody The menu body with the menu arrow.
* @returns {FloatingMenu~offset} The adjustment of the floating menu position, upon the position of the menu arrow.
Expand Down Expand Up @@ -352,6 +372,13 @@ export default class OverflowMenu extends Component {
}
};

/**
* @returns {Element} The DOM element where the floating menu is placed in.
*/
_getTarget = () =>
(this.menuEl && closest(this.menuEl, '[data-floating-menu-container]')) ||
document.body;

render() {
const {
id,
Expand Down Expand Up @@ -415,6 +442,7 @@ export default class OverflowMenu extends Component {
menuPosition={this.state.menuPosition}
menuOffset={flipped ? menuOffsetFlip : menuOffset}
menuRef={this._bindMenuBody}
target={this._getTarget}
onPlace={this._handlePlace}>
{menuBody}
</FloatingMenu>
Expand Down
39 changes: 34 additions & 5 deletions src/components/Tooltip/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ import FloatingMenu, {
} from '../../internal/FloatingMenu';
import ClickListener from '../../internal/ClickListener';

/**
* @param {Element} elem An element.
* @param {string} selector An query selector.
* @returns {Element} The ancestor of the given element matching the given selector.
* @private
*/
const closest = (elem, selector) => {
const doc = elem.ownerDocument;
for (
let traverse = elem;
traverse && traverse !== doc;
traverse = traverse.parentNode
) {
if (traverse.matches(selector)) {
return traverse;
}
}
return null;
};

/**
* @param {Element} menuBody The menu body with the menu arrow.
* @param {string} menuDirection Where the floating menu menu should be placed relative to the trigger button.
Expand Down Expand Up @@ -214,6 +234,14 @@ export default class Tooltip extends Component {
*/
_debouncedHandleHover = debounce(this._handleHover, 200);

/**
* @returns {Element} The DOM element where the floating menu is placed in.
*/
_getTarget = () =>
(this.triggerEl &&
closest(this.triggerEl, '[data-floating-menu-container]')) ||
document.body;

handleMouse = evt => {
const state =
typeof evt === 'string'
Expand Down Expand Up @@ -345,9 +373,13 @@ export default class Tooltip extends Component {
</ClickListener>
{open && (
<FloatingMenu
target={this._getTarget}
menuPosition={this.state.triggerPosition}
menuDirection={direction}
menuOffset={menuOffset}>
menuOffset={menuOffset}
menuRef={node => {
this._tooltipEl = node;
}}>
<div
id={tooltipId}
className={tooltipClasses}
Expand All @@ -358,10 +390,7 @@ export default class Tooltip extends Component {
onMouseOut={evt => this.handleMouse(evt)}
onFocus={evt => this.handleMouse(evt)}
onBlur={evt => this.handleMouse(evt)}
onContextMenu={evt => this.handleMouse(evt)}
ref={node => {
this._tooltipEl = node;
}}>
onContextMenu={evt => this.handleMouse(evt)}>
<span className="bx--tooltip__caret" />
{children}
</div>
Expand Down
84 changes: 21 additions & 63 deletions src/internal/FloatingMenu.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import warning from 'warning';
import PropTypes from 'prop-types';
import React from 'react';
import React, { createRef } from 'react';
import ReactDOM from 'react-dom';
import window from 'window-or-global';

Expand Down Expand Up @@ -32,8 +32,6 @@ export const DIRECTION_TOP = 'top';
export const DIRECTION_RIGHT = 'right';
export const DIRECTION_BOTTOM = 'bottom';

const hasCreatePortal = typeof ReactDOM.createPortal === 'function';

/**
* @param {FloatingMenu~offset} [oldMenuOffset={}] The old value.
* @param {FloatingMenu~offset} [menuOffset={}] The new value.
Expand Down Expand Up @@ -115,6 +113,11 @@ class FloatingMenu extends React.Component {
*/
children: PropTypes.object,

/**
* The query selector indicating where the floating menu body should be placed.
*/
target: PropTypes.func,

/**
* The position in the viewport of the trigger button.
*/
Expand Down Expand Up @@ -189,17 +192,10 @@ class FloatingMenu extends React.Component {

/**
* The cached refernce to the menu body.
* @type {Element}
* @type {React.RefObject}
* @private
*/
_menuBody = null;

constructor(props) {
super(props);
if (typeof document !== 'undefined' && hasCreatePortal) {
this.el = document.createElement('div');
}
}
_menuBody = createRef();

/**
* Calculates the position in the viewport of floating menu,
Expand All @@ -212,7 +208,7 @@ class FloatingMenu extends React.Component {
* @private
*/
_updateMenuSize = (prevProps = {}) => {
const menuBody = this._menuBody;
const menuBody = this._menuBody.current;
warning(
menuBody,
'The DOM node for menu body for calculating its position is not available. Skipping...'
Expand Down Expand Up @@ -263,72 +259,29 @@ class FloatingMenu extends React.Component {
};

componentDidUpdate(prevProps) {
if (!hasCreatePortal) {
ReactDOM.render(this._getChildrenWithProps(), this._menuContainer);
} else {
this._updateMenuSize(prevProps);
}
this._updateMenuSize(prevProps);
const { onPlace } = this.props;
if (
this._placeInProgress &&
this.state.floatingPosition &&
typeof onPlace === 'function'
) {
onPlace(this._menuBody);
onPlace(this._menuBody.current);
this._placeInProgress = false;
}
}

componentDidMount() {
const { menuRef } = this.props;
if (!hasCreatePortal) {
this._menuContainer = document.createElement('div');
document.body.appendChild(this._menuContainer);
const style = {
display: 'block',
opacity: 0,
};
const childrenWithProps = React.cloneElement(this.props.children, {
style,
});
ReactDOM.render(childrenWithProps, this._menuContainer, () => {
this._menuBody = this._menuContainer.firstChild;
this._updateMenuSize();
ReactDOM.render(
this._getChildrenWithProps(),
this._menuContainer,
() => {
this._placeInProgress = true;
menuRef && menuRef(this._menuBody);
}
);
});
} else {
if (this.el && this.el.firstChild) {
this._menuBody = this.el.firstChild;
document.body.appendChild(this._menuBody);
this._placeInProgress = true;
menuRef && menuRef(this._menuBody);
}
this._updateMenuSize();
}
this._placeInProgress = true;
menuRef && menuRef(this._menuBody.current);
this._updateMenuSize();
}

componentWillUnmount() {
const { menuRef } = this.props;
menuRef && menuRef(null);
this._placeInProgress = false;
if (!hasCreatePortal) {
const menuContainer = this._menuContainer;
ReactDOM.unmountComponentAtNode(menuContainer);
if (menuContainer && menuContainer.parentNode) {
menuContainer.parentNode.removeChild(menuContainer);
}
this._menuContainer = null;
} else if (this._menuBody) {
// Moves the menu body back to the portal container so that React unmount code does not crash
this.el.appendChild(this._menuBody);
}
}

/**
Expand All @@ -351,6 +304,7 @@ class FloatingMenu extends React.Component {
top: '0px',
};
return React.cloneElement(children, {
ref: this._menuBody,
style: {
...styles,
...positioningStyle,
Expand All @@ -362,8 +316,12 @@ class FloatingMenu extends React.Component {
};

render() {
if (typeof document !== 'undefined' && hasCreatePortal) {
return ReactDOM.createPortal(this._getChildrenWithProps(), this.el);
if (typeof document !== 'undefined') {
const { target } = this.props;
return ReactDOM.createPortal(
this._getChildrenWithProps(),
!target ? document.body : target()
);
}
return null;
}
Expand Down

0 comments on commit 08d0a1b

Please sign in to comment.