Skip to content

Commit

Permalink
Add EuiWrappingPopover to allow non-React elements to be used as popo…
Browse files Browse the repository at this point in the history
…ver anchors
  • Loading branch information
chandlerprall committed Jul 11, 2018
1 parent 2e74893 commit 01088fe
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 3 deletions.
23 changes: 23 additions & 0 deletions src-docs/src/views/popover/popover_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ import PopoverWithTitlePadding from './popover_with_title_padding';
const popoverWithTitlePaddingSource = require('!!raw-loader!./popover_with_title_padding');
const popoverWithTitlePaddingHtml = renderToHtml(PopoverWithTitlePadding);

import PopoverHTMLElementAnchor from './popover_htmlelement_anchor';
const popoverHTMLElementAnchorSource = require('!!raw-loader!./popover_htmlelement_anchor');
const popoverHTMLElementAnchorHtml = renderToHtml(PopoverHTMLElementAnchor);


export const PopoverExample = {
title: 'Popover',
sections: [{
Expand Down Expand Up @@ -158,5 +163,23 @@ export const PopoverExample = {
</div>
),
demo: <PopoverWithTitlePadding />,
}, {
title: 'Popover using an HTMLElement as the anchor',
source: [{
type: GuideSectionTypes.JS,
code: popoverHTMLElementAnchorSource,
}, {
type: GuideSectionTypes.HTML,
code: popoverHTMLElementAnchorHtml,
}],
text: (
<div>
<p>
<EuiCode>EuiWrappingPopover</EuiCode> is an extra popover component that allows
any existing DOM element to be passed as the <EuiCode>button</EuiCode> prop.
</p>
</div>
),
demo: <PopoverHTMLElementAnchor />,
}],
};
81 changes: 81 additions & 0 deletions src-docs/src/views/popover/popover_htmlelement_anchor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable react/no-multi-comp */
import React, {
Component,
} from 'react';

import { findDOMNode, render, unmountComponentAtNode } from 'react-dom';

import {
EuiWrappingPopover,
} from '../../../../src/components';

class PopoverApp extends Component {
constructor(props) {
super(props);

this.state = {
isPopoverOpen: false,
};
}

componentDidMount() {
this.props.anchor.addEventListener('click', this.onButtonClick);
}

onButtonClick = () => {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen,
});
}

closePopover = () => {
this.setState({
isPopoverOpen: false,
});
}

render() {
return (
<EuiWrappingPopover
id="popover"
button={this.props.anchor}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
>
<div>Normal JSX content populates the popover.</div>
</EuiWrappingPopover>
);
}
}

export default class extends Component {
componentDidMount() {
const thisNode = findDOMNode(this);
const thisAnchor = thisNode.querySelector('button');

// `container` can be created here or use an existing DOM element
// the popover DOM is positioned independently of where the container exists
this.container = document.createElement('div');
document.body.appendChild(this.container);

render(
<PopoverApp anchor={thisAnchor}/>,
this.container
);
}

componentWillUnmount() {
unmountComponentAtNode(this.container);
}

render() {
return (
<div dangerouslySetInnerHTML={{ __html: `
<button class="euiButton euiButton--primary">
<span class="euiButton__content">This is an HTML button</span>
</button>
` }}
/>
);
}
}
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export {
export {
EuiPopover,
EuiPopoverTitle,
EuiWrappingPopover,
} from './popover';

export {
Expand Down
1 change: 1 addition & 0 deletions src/components/popover/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { EuiPopover } from './popover';
export { EuiPopoverTitle } from './popover_title';
export { EuiWrappingPopover } from './wrapping_popover';
7 changes: 5 additions & 2 deletions src/components/popover/popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export class EuiPopover extends Component {
{...rest}
>
<div className="euiPopover__anchor" ref={this.buttonRef}>
{button}
{button instanceof HTMLElement ? null : button}
</div>
{panel}
</div>
Expand All @@ -409,7 +409,10 @@ EuiPopover.propTypes = {
ownFocus: PropTypes.bool,
withTitle: PropTypes.bool,
closePopover: PropTypes.func.isRequired,
button: PropTypes.node.isRequired,
button: PropTypes.oneOfType([
PropTypes.node,
PropTypes.instanceOf(HTMLElement),
]).isRequired,
children: PropTypes.node,
anchorPosition: PropTypes.oneOf(ANCHOR_POSITIONS),
panelClassName: PropTypes.string,
Expand Down
57 changes: 57 additions & 0 deletions src/components/popover/wrapping_popover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import { EuiPopover } from './popover';
import { EuiPortal } from '../portal';

/**
* Injects the EuiPopover next to the button via EuiPortal
* then the button element is moved into the popover dom.
* On unmount, the button is moved back to its original location.
*/
export class EuiWrappingPopover extends Component {
constructor(...args) {
super(...args);

this.portal = null;
this.contentParent = this.props.button.parentNode;
}

componentDidMount() {
const thisDomNode = findDOMNode(this);
const placeholderAnchor = thisDomNode.querySelector('.euiWrappingPopover__anchor');

placeholderAnchor.insertAdjacentElement(
'beforebegin',
this.props.button
);
}

componentWillUnmount() {
this.portal.insertAdjacentElement(
'beforebegin',
this.props.button
);
}

setPortalRef = node => {
this.portal = node;
};

render() {
const {
button, // eslint-disable-line no-unused-vars
...rest
} = this.props;
return (
<EuiPortal
portalRef={this.setPortalRef}
insert={{ sibling: this.props.button, position: 'after' }}
>
<EuiPopover
{...rest}
button={<div className="euiWrappingPopover__anchor"/>}
/>
</EuiPortal>
);
}
}
14 changes: 13 additions & 1 deletion src/components/portal/portal.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,20 @@ export class EuiPortal extends Component {
}
}

componentDidMount() {
this.updatePortalRef();
}

componentWillUnmount() {
this.portalNode.parentNode.removeChild(this.portalNode);
this.portalNode = null;
this.updatePortalRef();
}

updatePortalRef() {
if (this.props.portalRef) {
this.props.portalRef(this.portalNode);
}
}

render() {
Expand All @@ -57,6 +68,7 @@ EuiPortal.propTypes = {
PropTypes.node,
PropTypes.instanceOf(HTMLElement)
]).isRequired,
position: PropTypes.oneOf(INSERT_POSITIONS)
position: PropTypes.oneOf(INSERT_POSITIONS),
portalRef: PropTypes.func,
})
};

0 comments on commit 01088fe

Please sign in to comment.