Skip to content

Commit

Permalink
refactor EuiMutationObserver to support multiple children
Browse files Browse the repository at this point in the history
  • Loading branch information
chandlerprall committed Jul 9, 2018
1 parent 4f90621 commit 9a3d1ab
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 30 deletions.
20 changes: 15 additions & 5 deletions src/components/popover/popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const DEFAULT_POPOVER_STYLES = {
left: 50,
};

const GROUP_MUMERIC = /^([\d.]+)/;

export class EuiPopover extends Component {
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.prevProps.isOpen && !nextProps.isOpen) {
Expand Down Expand Up @@ -181,11 +183,17 @@ export class EuiPopover extends Component {
onMutation = (records) => {
const waitDuration = records.reduce(
(waitDuration, record) => {
const computedDuration = window.getComputedStyle(record.target).getPropertyValue('transition-duration');
const durationMatch = computedDuration.match(/^([\d.]+)/);
if (durationMatch != null) {
waitDuration = Math.max(waitDuration, parseFloat(durationMatch[1]) * 1000);
}
const computedStyle = window.getComputedStyle(record.target);

const computedDuration = computedStyle.getPropertyValue('transition-duration');
let durationMatch = computedDuration.match(GROUP_MUMERIC);
durationMatch = durationMatch ? parseFloat(durationMatch[1]) * 1000 : 0;

const computedDelay = computedStyle.getPropertyValue('transition-delay');
let delayMatch = computedDelay.match(GROUP_MUMERIC);
delayMatch = delayMatch ? parseFloat(delayMatch[1]) * 1000 : 0;

waitDuration = Math.max(waitDuration, durationMatch + delayMatch);
return waitDuration;
},
0
Expand All @@ -209,6 +217,8 @@ export class EuiPopover extends Component {
}

positionPopover = () => {
if (this.button == null || this.panel == null) return;

const { top, left, position, arrow } = findPopoverPosition({
position: getPopoverPositionFromAnchorPosition(this.props.anchorPosition),
align: getPopoverAlignFromAnchorPosition(this.props.anchorPosition),
Expand Down
99 changes: 74 additions & 25 deletions src/utils/mutation_observer/mutation_observer.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,98 @@
import { Component, cloneElement } from 'react';
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import PropTypes from 'prop-types';

/**
* EuiMutationObserver watches its children with the MutationObserver API
* There are a couple constraints which inform how this component works
*
* 1. React refs cannot be added to functional components
* 2. findDOMNode will only return the first element from an array of children
* or from a fragment.
*
* Because of #1, we can't blindly attach refs to children and expect them to work in all cases
* Because of #2, we can't observe all children for mutations, only the first
*
* When only one child is passed its found by findDOMNode and the mutation observer is attached
* When children is an array the render function maps over them wrapping each child
* with another EuiMutationObserver, e.g.:
*
* <Observer>
* <div>First</div>
* <div>Second</div>
* </Observer>
*
* becomes
*
* <Observer>
* <Observer><div>First</div></Observer>
* <Observer><div>Second</div></Observer>
* </Observer>
*
* each descendant-Observer has only one child and can independently watch for mutations,
* triggering the parent's onMutation callback when an event is observed
*/
class EuiMutationObserver extends Component {
constructor(...args) {
super(...args);
this.childrenRef = null;
this.childNode = null;
this.observer = null;
}

onMutation = (...args) => {
this.props.onMutation(...args);
componentDidMount() {
this.updateChildNode();
}

updateRef = ref => {
if (this.props.children.ref) {
this.props.children.ref(ref);
}
updateChildNode() {
if (Array.isArray(this.props.children) === false) {
const currentNode = findDOMNode(this);
if (this.childNode !== currentNode) {
// if there's an existing observer disconnect it
if (this.observer != null) {
this.observer.disconnect();
this.observer = null;
}

if (this.observer != null) {
this.observer.disconnect();
this.observer = null;
this.childNode = currentNode;
if (this.childNode != null) {
this.observer = new MutationObserver(this.onMutation);
this.observer.observe(this.childNode, this.props.observerOptions);
}
}
}
}

if (ref != null) {
const node = findDOMNode(ref);
this.observer = new MutationObserver(this.onMutation);
this.observer.observe(node, this.props.observerOptions);
}
componentDidUpdate() {
// in case the child element was changed
this.updateChildNode();
}

onMutation = (...args) => {
this.props.onMutation(...args);
}

render() {
const children = cloneElement(
this.props.children,
{
...this.props.children.props,
ref: this.updateRef,
}
);
return children;
const { children, ...rest } = this.props;
if (Array.isArray(children)) {
return React.Children.map(
children,
child => (
<EuiMutationObserver {...rest}>
{child}
</EuiMutationObserver>
)
);
} else {
return children;
}
}
}

EuiMutationObserver.propTypes = {
children: PropTypes.element.isRequired,
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node)
]).isRequired,
observerOptions: PropTypes.shape({ // matches a [MutationObserverInit](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit)
attributeFilter: PropTypes.arrayOf(PropTypes.string),
attributeOldValue: PropTypes.bool,
Expand Down

0 comments on commit 9a3d1ab

Please sign in to comment.