diff --git a/CHANGELOG.md b/CHANGELOG.md index 2111dfacce9..92a6515b312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - Added support for negated or clauses to `EuiSearchBar` ([#2140](https://github.com/elastic/eui/pull/2140)) +- Added `transition` utility services to help create timeouts that account for CSS transition durations and delays ([#2136](https://github.com/elastic/eui/pull/2136)) **Bug fixes** - Fixed `EuiComboBox`'s padding on the right ([#2135](https://github.com/elastic/eui/pull/2135)) +- Fixed `EuiAccordion` to correctly account for changing computed height of child elements ([#2136](https://github.com/elastic/eui/pull/2136)) **Breaking changes** diff --git a/src-docs/src/views/accordion/accordion_multiple.js b/src-docs/src/views/accordion/accordion_multiple.js index b43246cda47..23890a99b82 100644 --- a/src-docs/src/views/accordion/accordion_multiple.js +++ b/src-docs/src/views/accordion/accordion_multiple.js @@ -30,5 +30,31 @@ export default () => (

The content inside can be of any height.

+ + + + + +

+ This content area will grow to accomodate when the accordion below + opens +

+
+ + + +

The content inside can be of any height.

+

The content inside can be of any height.

+

The content inside can be of any height.

+

The content inside can be of any height.

+

The content inside can be of any height.

+

The content inside can be of any height.

+
+
+ +
); diff --git a/src/components/accordion/__snapshots__/accordion.test.tsx.snap b/src/components/accordion/__snapshots__/accordion.test.tsx.snap index 120e8bb7edb..612606f1323 100644 --- a/src/components/accordion/__snapshots__/accordion.test.tsx.snap +++ b/src/components/accordion/__snapshots__/accordion.test.tsx.snap @@ -85,6 +85,9 @@ exports[`EuiAccordion behavior closes when clicked twice 1`] = ` { + const isChildStyleMutation = records.find((record: MutationRecord) => { + return record.attributeName + ? MUTATION_ATTRIBUTE_FILTER.indexOf(record.attributeName) > -1 + : false; + }); + if (isChildStyleMutation) { + getDurationAndPerformOnFrame(records, this.setChildContentHeight); + } else { + this.setChildContentHeight(); + } + }; + componentDidMount() { this.setChildContentHeight(); } @@ -178,8 +194,12 @@ export class EuiAccordion extends Component< }} id={id}> + 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); +};