Skip to content

Commit

Permalink
Make EuiPopover copy the anchor's z-index to the popover content (#967)
Browse files Browse the repository at this point in the history
* Make EuiPopover copy the anchor's z-index to the popover content

* Updated getElementZIndex logic to handle z-index fighting edge case

* added popover-in-flyout example, changelog

* move euipopover component location in flyout example

* move flyout popover into flyout body
  • Loading branch information
chandlerprall authored Jul 3, 2018
1 parent 6f457eb commit 4d56c9c
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Fixed disabled states of icon buttons ([#963](https://github.com/elastic/eui/pull/963))
- Added word-break fallback for FF & IE in table cell ([#962](https://github.com/elastic/eui/pull/962))
- Fixed `EuiPopover` to show content over modals, flyouts, etc ([#967](https://github.com/elastic/eui/pull/967))

## [`1.0.1`](https://github.com/elastic/eui/tree/v1.0.1)

Expand Down
17 changes: 17 additions & 0 deletions src-docs/src/views/flyout/flyout_complicated.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiPopover,
EuiSpacer,
EuiTab,
EuiTabs,
Expand All @@ -27,6 +28,7 @@ export class FlyoutComplicated extends Component {
isFlyoutVisible: false,
isSwitchChecked: true,
selectedTabId: '1',
isPopoverOpen: false,
};

this.tabs = [{
Expand Down Expand Up @@ -55,6 +57,14 @@ export class FlyoutComplicated extends Component {
this.setState({ isFlyoutVisible: true });
}

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

togglePopover = () => {
this.setState(({ isPopoverOpen }) => ({ isPopoverOpen: !isPopoverOpen }));
}

onSelectedTabChanged = id => {
this.setState({
selectedTabId: id,
Expand Down Expand Up @@ -162,6 +172,13 @@ export class FlyoutComplicated extends Component {
</EuiTabs>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiPopover
closePopover={this.closePopover}
button={<EuiButton onClick={this.togglePopover}>Even popovers can be included</EuiButton>}
isOpen={this.state.isPopoverOpen}
>
<p>This is the popover content, notice how it can overflow the flyout!</p>
</EuiPopover>
{flyoutContent}
<EuiCodeBlock language="html">
{htmlCode}
Expand Down
8 changes: 7 additions & 1 deletion src/components/popover/popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { EuiPanel, SIZES } from '../panel';

import { EuiPortal } from '../portal';

import { findPopoverPosition } from '../../services/popover/popover_positioning';
import { findPopoverPosition, getElementZIndex } from '../../services/popover/popover_positioning';

const anchorPositionToPopoverPositionMap = {
'up': 'top',
Expand Down Expand Up @@ -189,9 +189,15 @@ export class EuiPopover extends Component {
}
});

// the popver's z-index must inherit from the button
// this keeps a button's popver under a flyover that would cover the button
// but a popover triggered inside a flyover will appear over that flyover
const zIndex = getElementZIndex(this.button, this.panel);

const popoverStyles = {
top,
left,
zIndex,
};

const arrowStyles = arrow;
Expand Down
64 changes: 64 additions & 0 deletions src/services/popover/popover_positioning.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,3 +508,67 @@ export function intersectBoundingBoxes(firstBox, secondBox) {

return intersection;
}


/**
* Returns the top-most defined z-index in the element's ancestor hierarchy
* relative to the `target` element; if no z-index is defined, returns "0"
* @param element {HTMLElement|React.Component}
* @param cousin {HTMLElement|React.Component}
* @returns {string}
*/
export function getElementZIndex(element, cousin) {
element = findDOMNode(element);
cousin = findDOMNode(cousin);

/**
* finding the z-index of `element` is not the full story
* its the CSS stacking context that is important
* take this DOM for example:
* body
* section[z-index: 1000]
* p[z-index: 500]
* button
* div
*
* what z-index does the `div` need to display next to `button`?
* the `div` and `section` are where the stacking context splits
* so `div` needs to copy `section`'s z-index in order to
* appear next to / over `button`
*
* calculate this by starting at `button` and finding its offsetParents
* then walk the parents from top -> down until the stacking context
* split is found, or if there is no split then a specific z-index is unimportant
*/

// build the array of the element + its offset parents
const nodesToInspect = [];
while (true) {
nodesToInspect.push(element);

element = element.offsetParent;

// stop if there is no parent
if (element == null) break;

// stop if the parent contains the related element
// as this is the z-index ancestor
if (element.contains(cousin)) break;
}

// reverse the nodes to walk from top -> element
nodesToInspect.reverse();

return nodesToInspect.reduce(
(foundZIndex, node) => {
if (foundZIndex != null) return foundZIndex;

// get this node's z-index css value
const zIndex = window.document.defaultView.getComputedStyle(node).getPropertyValue('z-index');

// if the z-index is not a number (e.g. "auto") return null, else the value
return isNaN(zIndex) ? null : zIndex;
},
null
) || '0';
}

0 comments on commit 4d56c9c

Please sign in to comment.