UNPKG

@wordpress/components

Version:
533 lines (440 loc) 17 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _element = require("@wordpress/element"); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _classnames = _interopRequireDefault(require("classnames")); var _dom = require("@wordpress/dom"); var _keycodes = require("@wordpress/keycodes"); var _deprecated = _interopRequireDefault(require("@wordpress/deprecated")); var _compose = require("@wordpress/compose"); var _icons = require("@wordpress/icons"); var _utils = require("./utils"); var _button = _interopRequireDefault(require("../button")); var _scrollLock = _interopRequireDefault(require("../scroll-lock")); var _slotFill = require("../slot-fill"); var _animate = require("../animate"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ /** * Name of slot in which popover should fill. * * @type {string} */ const SLOT_NAME = 'Popover'; function computeAnchorRect(anchorRefFallback, anchorRect, getAnchorRect, anchorRef = false, shouldAnchorIncludePadding) { if (anchorRect) { return anchorRect; } if (getAnchorRect) { if (!anchorRefFallback.current) { return; } return (0, _utils.offsetIframe)(getAnchorRect(anchorRefFallback.current), anchorRefFallback.current.ownerDocument); } if (anchorRef !== false) { if (!anchorRef || !window.Range || !window.Element || !window.DOMRect) { return; } // Duck-type to check if `anchorRef` is an instance of Range // `anchorRef instanceof window.Range` checks will break across document boundaries // such as in an iframe if (typeof (anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.cloneRange) === 'function') { return (0, _utils.offsetIframe)((0, _dom.getRectangleFromRange)(anchorRef), anchorRef.endContainer.ownerDocument); } // Duck-type to check if `anchorRef` is an instance of Element // `anchorRef instanceof window.Element` checks will break across document boundaries // such as in an iframe if (typeof (anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.getBoundingClientRect) === 'function') { const rect = (0, _utils.offsetIframe)(anchorRef.getBoundingClientRect(), anchorRef.ownerDocument); if (shouldAnchorIncludePadding) { return rect; } return withoutPadding(rect, anchorRef); } const { top, bottom } = anchorRef; const topRect = top.getBoundingClientRect(); const bottomRect = bottom.getBoundingClientRect(); const rect = (0, _utils.offsetIframe)(new window.DOMRect(topRect.left, topRect.top, topRect.width, bottomRect.bottom - topRect.top), top.ownerDocument); if (shouldAnchorIncludePadding) { return rect; } return withoutPadding(rect, anchorRef); } if (!anchorRefFallback.current) { return; } const { parentNode } = anchorRefFallback.current; const rect = parentNode.getBoundingClientRect(); if (shouldAnchorIncludePadding) { return rect; } return withoutPadding(rect, parentNode); } function getComputedStyle(node) { return node.ownerDocument.defaultView.getComputedStyle(node); } function withoutPadding(rect, element) { const { paddingTop, paddingBottom, paddingLeft, paddingRight } = getComputedStyle(element); const top = paddingTop ? parseInt(paddingTop, 10) : 0; const bottom = paddingBottom ? parseInt(paddingBottom, 10) : 0; const left = paddingLeft ? parseInt(paddingLeft, 10) : 0; const right = paddingRight ? parseInt(paddingRight, 10) : 0; return { x: rect.left + left, y: rect.top + top, width: rect.width - left - right, height: rect.height - top - bottom, left: rect.left + left, right: rect.right - right, top: rect.top + top, bottom: rect.bottom - bottom }; } /** * Sets or removes an element attribute. * * @param {Element} element The element to modify. * @param {string} name The attribute name to set or remove. * @param {?string} value The value to set. A falsy value will remove the * attribute. */ function setAttribute(element, name, value) { if (!value) { if (element.hasAttribute(name)) { element.removeAttribute(name); } } else if (element.getAttribute(name) !== value) { element.setAttribute(name, value); } } /** * Sets or removes an element style property. * * @param {Element} element The element to modify. * @param {string} property The property to set or remove. * @param {?string} value The value to set. A falsy value will remove the * property. */ function setStyle(element, property, value = '') { if (element.style[property] !== value) { element.style[property] = value; } } /** * Sets or removes an element class. * * @param {Element} element The element to modify. * @param {string} name The class to set or remove. * @param {boolean} toggle True to set the class, false to remove. */ function setClass(element, name, toggle) { if (toggle) { if (!element.classList.contains(name)) { element.classList.add(name); } } else if (element.classList.contains(name)) { element.classList.remove(name); } } function getAnchorDocument(anchor) { if (!anchor) { return; } if (anchor.endContainer) { return anchor.endContainer.ownerDocument; } if (anchor.top) { return anchor.top.ownerDocument; } return anchor.ownerDocument; } const Popover = ({ headerTitle, onClose, onKeyDown, children, className, noArrow = true, isAlternate, // Disable reason: We generate the `...contentProps` rest as remainder // of props which aren't explicitly handled by this component. /* eslint-disable no-unused-vars */ position = 'bottom right', range, focusOnMount = 'firstElement', anchorRef, shouldAnchorIncludePadding, anchorRect, getAnchorRect, expandOnMobile, animate = true, onClickOutside, onFocusOutside, __unstableStickyBoundaryElement, __unstableSlotName = SLOT_NAME, __unstableObserveElement, __unstableBoundaryParent, __unstableForcePosition, /* eslint-enable no-unused-vars */ ...contentProps }) => { const anchorRefFallback = (0, _element.useRef)(null); const contentRef = (0, _element.useRef)(null); const containerRef = (0, _element.useRef)(); const isMobileViewport = (0, _compose.useViewportMatch)('medium', '<'); const [animateOrigin, setAnimateOrigin] = (0, _element.useState)(); const slot = (0, _slotFill.useSlot)(__unstableSlotName); const isExpanded = expandOnMobile && isMobileViewport; const [containerResizeListener, contentSize] = (0, _compose.useResizeObserver)(); noArrow = isExpanded || noArrow; (0, _element.useLayoutEffect)(() => { if (isExpanded) { setClass(containerRef.current, 'is-without-arrow', noArrow); setClass(containerRef.current, 'is-alternate', isAlternate); setAttribute(containerRef.current, 'data-x-axis'); setAttribute(containerRef.current, 'data-y-axis'); setStyle(containerRef.current, 'top'); setStyle(containerRef.current, 'left'); setStyle(contentRef.current, 'maxHeight'); setStyle(contentRef.current, 'maxWidth'); return; } const refresh = () => { if (!containerRef.current || !contentRef.current) { return; } let anchor = computeAnchorRect(anchorRefFallback, anchorRect, getAnchorRect, anchorRef, shouldAnchorIncludePadding); if (!anchor) { return; } const { offsetParent, ownerDocument } = containerRef.current; let relativeOffsetTop = 0; // If there is a positioned ancestor element that is not the body, // subtract the position from the anchor rect. If the position of // the popover is fixed, the offset parent is null or the body // element, in which case the position is relative to the viewport. // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent if (offsetParent && offsetParent !== ownerDocument.body) { const offsetParentRect = offsetParent.getBoundingClientRect(); relativeOffsetTop = offsetParentRect.top; anchor = new window.DOMRect(anchor.left - offsetParentRect.left, anchor.top - offsetParentRect.top, anchor.width, anchor.height); } let boundaryElement; if (__unstableBoundaryParent) { var _containerRef$current; boundaryElement = (_containerRef$current = containerRef.current.closest('.popover-slot')) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.parentNode; } const usedContentSize = !contentSize.height ? contentRef.current.getBoundingClientRect() : contentSize; const { popoverTop, popoverLeft, xAxis, yAxis, contentHeight, contentWidth } = (0, _utils.computePopoverPosition)(anchor, usedContentSize, position, __unstableStickyBoundaryElement, containerRef.current, relativeOffsetTop, boundaryElement, __unstableForcePosition); if (typeof popoverTop === 'number' && typeof popoverLeft === 'number') { setStyle(containerRef.current, 'top', popoverTop + 'px'); setStyle(containerRef.current, 'left', popoverLeft + 'px'); } setClass(containerRef.current, 'is-without-arrow', noArrow || xAxis === 'center' && yAxis === 'middle'); setClass(containerRef.current, 'is-alternate', isAlternate); setAttribute(containerRef.current, 'data-x-axis', xAxis); setAttribute(containerRef.current, 'data-y-axis', yAxis); setStyle(contentRef.current, 'maxHeight', typeof contentHeight === 'number' ? contentHeight + 'px' : ''); setStyle(contentRef.current, 'maxWidth', typeof contentWidth === 'number' ? contentWidth + 'px' : ''); // Compute the animation position const yAxisMapping = { top: 'bottom', bottom: 'top' }; const xAxisMapping = { left: 'right', right: 'left' }; const animateYAxis = yAxisMapping[yAxis] || 'middle'; const animateXAxis = xAxisMapping[xAxis] || 'center'; setAnimateOrigin(animateXAxis + ' ' + animateYAxis); }; refresh(); const { ownerDocument } = containerRef.current; const { defaultView } = ownerDocument; /* * There are sometimes we need to reposition or resize the popover that * are not handled by the resize/scroll window events (i.e. CSS changes * in the layout that changes the position of the anchor). * * For these situations, we refresh the popover every 0.5s */ const intervalHandle = defaultView.setInterval(refresh, 500); let rafId; const refreshOnAnimationFrame = () => { defaultView.cancelAnimationFrame(rafId); rafId = defaultView.requestAnimationFrame(refresh); }; // Sometimes a click trigger a layout change that affects the popover // position. This is an opportunity to immediately refresh rather than // at the interval. defaultView.addEventListener('click', refreshOnAnimationFrame); defaultView.addEventListener('resize', refresh); defaultView.addEventListener('scroll', refresh, true); const anchorDocument = getAnchorDocument(anchorRef); // If the anchor is within an iframe, the popover position also needs // to refrest when the iframe content is scrolled or resized. if (anchorDocument && anchorDocument !== ownerDocument) { anchorDocument.defaultView.addEventListener('resize', refresh); anchorDocument.defaultView.addEventListener('scroll', refresh, true); } let observer; if (__unstableObserveElement) { observer = new defaultView.MutationObserver(refresh); observer.observe(__unstableObserveElement, { attributes: true }); } return () => { defaultView.clearInterval(intervalHandle); defaultView.removeEventListener('resize', refresh); defaultView.removeEventListener('scroll', refresh, true); defaultView.removeEventListener('click', refreshOnAnimationFrame); defaultView.cancelAnimationFrame(rafId); if (anchorDocument && anchorDocument !== ownerDocument) { anchorDocument.defaultView.removeEventListener('resize', refresh); anchorDocument.defaultView.removeEventListener('scroll', refresh, true); } if (observer) { observer.disconnect(); } }; }, [isExpanded, anchorRect, getAnchorRect, anchorRef, shouldAnchorIncludePadding, position, contentSize, __unstableStickyBoundaryElement, __unstableObserveElement, __unstableBoundaryParent]); const constrainedTabbingRef = (0, _compose.useConstrainedTabbing)(); const focusReturnRef = (0, _compose.useFocusReturn)(); const focusOnMountRef = (0, _compose.useFocusOnMount)(focusOnMount); const focusOutsideProps = (0, _compose.__experimentalUseFocusOutside)(handleOnFocusOutside); const mergedRefs = (0, _compose.useMergeRefs)([containerRef, focusOnMount ? constrainedTabbingRef : null, focusOnMount ? focusReturnRef : null, focusOnMount ? focusOnMountRef : null]); // Event handlers const maybeClose = event => { // Close on escape if (event.keyCode === _keycodes.ESCAPE && onClose) { event.stopPropagation(); onClose(); } // Preserve original content prop behavior if (onKeyDown) { onKeyDown(event); } }; /** * Shims an onFocusOutside callback to be compatible with a deprecated * onClickOutside prop function, if provided. * * @param {FocusEvent} event Focus event from onFocusOutside. */ function handleOnFocusOutside(event) { // Defer to given `onFocusOutside` if specified. Call `onClose` only if // both `onFocusOutside` and `onClickOutside` are unspecified. Doing so // assures backwards-compatibility for prior `onClickOutside` default. if (onFocusOutside) { onFocusOutside(event); return; } else if (!onClickOutside) { if (onClose) { onClose(); } return; } // Simulate MouseEvent using FocusEvent#relatedTarget as emulated click // target. MouseEvent constructor is unsupported in Internet Explorer. let clickEvent; try { clickEvent = new window.MouseEvent('click'); } catch (error) { clickEvent = document.createEvent('MouseEvent'); clickEvent.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); } Object.defineProperty(clickEvent, 'target', { get: () => event.relatedTarget }); (0, _deprecated.default)('Popover onClickOutside prop', { since: '5.3', alternative: 'onFocusOutside' }); onClickOutside(clickEvent); } /** @type {false | string} */ const animateClassName = Boolean(animate && animateOrigin) && (0, _animate.getAnimateClassName)({ type: 'appear', origin: animateOrigin }); // Disable reason: We care to capture the _bubbled_ events from inputs // within popover as inferring close intent. let content = // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions // eslint-disable-next-line jsx-a11y/no-static-element-interactions (0, _element.createElement)("div", (0, _extends2.default)({ className: (0, _classnames.default)('components-popover', className, animateClassName, { 'is-expanded': isExpanded, 'is-without-arrow': noArrow, 'is-alternate': isAlternate }) }, contentProps, { onKeyDown: maybeClose }, focusOutsideProps, { ref: mergedRefs, tabIndex: "-1" }), isExpanded && (0, _element.createElement)(_scrollLock.default, null), isExpanded && (0, _element.createElement)("div", { className: "components-popover__header" }, (0, _element.createElement)("span", { className: "components-popover__header-title" }, headerTitle), (0, _element.createElement)(_button.default, { className: "components-popover__close", icon: _icons.close, onClick: onClose })), (0, _element.createElement)("div", { ref: contentRef, className: "components-popover__content" }, (0, _element.createElement)("div", { style: { position: 'relative' } }, containerResizeListener, children))); if (slot.ref) { content = (0, _element.createElement)(_slotFill.Fill, { name: __unstableSlotName }, content); } if (anchorRef || anchorRect) { return content; } return (0, _element.createElement)("span", { ref: anchorRefFallback }, content); }; const PopoverContainer = Popover; PopoverContainer.Slot = ({ name = SLOT_NAME }) => (0, _element.createElement)(_slotFill.Slot, { bubblesVirtually: true, name: name, className: "popover-slot" }); var _default = PopoverContainer; exports.default = _default; //# sourceMappingURL=index.js.map