+ observerOptions={{
+ childList: true,
+ subtree: true,
+ attributeFilter: MUTATION_ATTRIBUTE_FILTER,
+ }}
+ onMutation={this.onMutation}>
{mutationRef => (
{
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 01d39744cc5..ba6838e0948 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -3,7 +3,12 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import tabbable from 'tabbable';
-import { cascadingMenuKeyCodes } from '../../services';
+import {
+ cascadingMenuKeyCodes,
+ getTransitionTimings,
+ getWaitDuration,
+ performOnFrame,
+} from '../../services';
import { EuiFocusTrap } from '../focus_trap';
@@ -84,8 +89,6 @@ const DEFAULT_POPOVER_STYLES = {
left: 50,
};
-const GROUP_NUMERIC = /^([\d.]+)/;
-
function getElementFromInitialFocus(initialFocus) {
const initialFocusType = typeof initialFocus;
if (initialFocusType === 'string')
@@ -94,22 +97,6 @@ function getElementFromInitialFocus(initialFocus) {
return initialFocus;
}
-function getTransitionTimings(element) {
- const computedStyle = window.getComputedStyle(element);
-
- const computedDuration = computedStyle.getPropertyValue(
- 'transition-duration'
- );
- let durationMatch = computedDuration.match(GROUP_NUMERIC);
- durationMatch = durationMatch ? parseFloat(durationMatch[1]) * 1000 : 0;
-
- const computedDelay = computedStyle.getPropertyValue('transition-delay');
- let delayMatch = computedDelay.match(GROUP_NUMERIC);
- delayMatch = delayMatch ? parseFloat(delayMatch[1]) * 1000 : 0;
-
- return { durationMatch, delayMatch };
-}
-
export class EuiPopover extends Component {
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.prevProps.isOpen && !nextProps.isOpen) {
@@ -278,33 +265,10 @@ export class EuiPopover extends Component {
}
onMutation = records => {
- const waitDuration = records.reduce((waitDuration, record) => {
- // only check for CSS transition values for ELEMENT nodes
- if (record.target.nodeType === document.ELEMENT_NODE) {
- const { durationMatch, delayMatch } = getTransitionTimings(
- record.target
- );
- waitDuration = Math.max(waitDuration, durationMatch + delayMatch);
- }
-
- return waitDuration;
- }, 0);
+ const waitDuration = getWaitDuration(records);
this.positionPopoverFixed();
- if (waitDuration > 0) {
- const startTime = Date.now();
- const endTime = startTime + waitDuration;
-
- const onFrame = () => {
- this.positionPopoverFixed();
-
- if (endTime > Date.now()) {
- requestAnimationFrame(onFrame);
- }
- };
-
- requestAnimationFrame(onFrame);
- }
+ performOnFrame(waitDuration, this.positionPopoverFixed);
};
positionPopover = allowEnforcePosition => {
diff --git a/src/services/index.ts b/src/services/index.ts
index 41a42e877ca..d019a56af20 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -59,4 +59,11 @@ export {
export { calculatePopoverPosition, findPopoverPosition } from './popover';
+export {
+ getDurationAndPerformOnFrame,
+ getTransitionTimings,
+ getWaitDuration,
+ performOnFrame,
+} from './transition';
+
export { EuiWindowEvent } from './window_event';
diff --git a/src/services/transition/index.ts b/src/services/transition/index.ts
new file mode 100644
index 00000000000..aa31b053527
--- /dev/null
+++ b/src/services/transition/index.ts
@@ -0,0 +1,6 @@
+export {
+ getDurationAndPerformOnFrame,
+ getTransitionTimings,
+ getWaitDuration,
+ performOnFrame,
+} from './transition';
diff --git a/src/services/transition/transition.ts b/src/services/transition/transition.ts
new file mode 100644
index 00000000000..fb7b69d098d
--- /dev/null
+++ b/src/services/transition/transition.ts
@@ -0,0 +1,71 @@
+const GROUP_NUMERIC = /^([\d.]+)(s|ms)/;
+
+function getMilliseconds(value: string, unit: string) {
+ // Given the regex match and capture groups, we can assume `unit` to be either 's' or 'ms'
+ const multiplier = unit === 's' ? 1000 : 1;
+ return parseFloat(value) * multiplier;
+}
+// Find CSS `transition-duration` and `transition-delay` intervals
+// and return the value of each computed property in 'ms'
+export const getTransitionTimings = (element: Element) => {
+ const computedStyle = window.getComputedStyle(element);
+
+ const computedDuration = computedStyle.getPropertyValue(
+ 'transition-duration'
+ );
+ const durationMatchArray = computedDuration.match(GROUP_NUMERIC);
+ const durationMatch = durationMatchArray
+ ? getMilliseconds(durationMatchArray[1], durationMatchArray[2])
+ : 0;
+
+ const computedDelay = computedStyle.getPropertyValue('transition-delay');
+ const delayMatchArray = computedDelay.match(GROUP_NUMERIC);
+ const delayMatch = delayMatchArray
+ ? getMilliseconds(delayMatchArray[1], delayMatchArray[2])
+ : 0;
+
+ return { durationMatch, delayMatch };
+};
+
+function isElementNode(element: Node): element is Element {
+ return element.nodeType === document.ELEMENT_NODE;
+}
+// Uses `getTransitionTimings` to find the total transition time for
+// all elements targeted by a MutationObserver callback
+export const getWaitDuration = (records: MutationRecord[]) => {
+ return records.reduce((waitDuration, record) => {
+ // only check for CSS transition values for ELEMENT nodes
+ if (isElementNode(record.target)) {
+ const { durationMatch, delayMatch } = getTransitionTimings(record.target);
+ waitDuration = Math.max(waitDuration, durationMatch + delayMatch);
+ }
+
+ return waitDuration;
+ }, 0);
+};
+
+// Uses `requestAnimationFrame` to perform a given callback after a specified waiting period
+export const performOnFrame = (waitDuration: number, toPerform: () => void) => {
+ if (waitDuration > 0) {
+ const startTime = Date.now();
+ const endTime = startTime + waitDuration;
+
+ const onFrame = () => {
+ toPerform();
+
+ if (endTime > Date.now()) {
+ requestAnimationFrame(onFrame);
+ }
+ };
+
+ requestAnimationFrame(onFrame);
+ }
+};
+
+// Convenience method for combining the result of 'getWaitDuration' directly with 'performOnFrame'
+export const getDurationAndPerformOnFrame = (
+ records: MutationRecord[],
+ toPerform: () => void
+) => {
+ performOnFrame(getWaitDuration(records), toPerform);
+};