UNPKG

@shopify/polaris

Version:

Shopify’s product component library

175 lines (174 loc) 7.73 kB
import React from 'react'; import { getRectForNode, Rect } from '@shopify/javascript-utilities/geometry'; import { closest } from '@shopify/javascript-utilities/dom'; import { classNames } from '../../utilities/css'; import { EventListener } from '../EventListener'; import { Scrollable } from '../Scrollable'; import { layer } from '../shared'; import { calculateVerticalPosition, calculateHorizontalPosition, rectIsOutsideOfRect, } from './utilities/math'; import styles from './PositionedOverlay.scss'; const OBSERVER_CONFIG = { childList: true, subtree: true }; export class PositionedOverlay extends React.PureComponent { constructor(props) { super(props); this.state = { measuring: true, activatorRect: getRectForNode(this.props.activator), left: 0, top: 0, height: 0, width: null, positioning: 'below', zIndex: null, outsideScrollableContainer: false, lockPosition: false, }; this.overlay = null; this.scrollableContainer = null; this.overlayDetails = () => { const { measuring, left, positioning, height, activatorRect } = this.state; return { measuring, left, desiredHeight: height, positioning, activatorRect, }; }; this.setOverlay = (node) => { this.overlay = node; }; this.handleMeasurement = () => { const { lockPosition, top } = this.state; this.observer.disconnect(); this.setState(({ left, top }) => ({ left, top, height: 0, positioning: 'below', measuring: true, }), () => { if (this.overlay == null || this.scrollableContainer == null) { return; } const { activator, preferredPosition = 'below', preferredAlignment = 'center', onScrollOut, fullWidth, fixed, } = this.props; const textFieldActivator = activator.querySelector('input'); const activatorRect = textFieldActivator != null ? getRectForNode(textFieldActivator) : getRectForNode(activator); const currentOverlayRect = getRectForNode(this.overlay); const scrollableElement = isDocument(this.scrollableContainer) ? document.body : this.scrollableContainer; const scrollableContainerRect = getRectForNode(scrollableElement); const overlayRect = fullWidth ? Object.assign({}, currentOverlayRect, { width: activatorRect.width }) : currentOverlayRect; // If `body` is 100% height, it still acts as though it were not constrained to that size. This adjusts for that. if (scrollableElement === document.body) { scrollableContainerRect.height = document.body.scrollHeight; } const overlayMargins = this.overlay.firstElementChild ? getMarginsForNode(this.overlay.firstElementChild) : { activator: 0, container: 0, horizontal: 0 }; const containerRect = windowRect(); const zIndexForLayer = getZIndexForLayerFromNode(activator); const zIndex = zIndexForLayer == null ? zIndexForLayer : zIndexForLayer + 1; const verticalPosition = calculateVerticalPosition(activatorRect, overlayRect, overlayMargins, scrollableContainerRect, containerRect, preferredPosition, fixed); const horizontalPosition = calculateHorizontalPosition(activatorRect, overlayRect, containerRect, overlayMargins, preferredAlignment); this.setState({ measuring: false, activatorRect: getRectForNode(activator), left: horizontalPosition, top: lockPosition ? top : verticalPosition.top, lockPosition: Boolean(fixed), height: verticalPosition.height || 0, width: fullWidth ? overlayRect.width : null, positioning: verticalPosition.positioning, outsideScrollableContainer: onScrollOut != null && rectIsOutsideOfRect(activatorRect, intersectionWithViewport(scrollableContainerRect)), zIndex, }, () => { if (!this.overlay) return; this.observer.observe(this.overlay, OBSERVER_CONFIG); }); }); }; this.observer = new MutationObserver(this.handleMeasurement); } componentDidMount() { this.scrollableContainer = Scrollable.forNode(this.props.activator); if (this.scrollableContainer && !this.props.fixed) { this.scrollableContainer.addEventListener('scroll', this.handleMeasurement); } this.handleMeasurement(); } componentWillUnmount() { if (this.scrollableContainer && !this.props.fixed) { this.scrollableContainer.removeEventListener('scroll', this.handleMeasurement); } } componentDidUpdate() { const { outsideScrollableContainer, top } = this.state; const { onScrollOut, active } = this.props; if (active && onScrollOut != null && top !== 0 && outsideScrollableContainer) { onScrollOut(); } } render() { const { left, top, zIndex, width } = this.state; const { render, fixed, classNames: propClassNames } = this.props; const style = { top: top == null || isNaN(top) ? undefined : top, left: left == null || isNaN(left) ? undefined : left, width: width == null || isNaN(width) ? undefined : width, zIndex: zIndex == null || isNaN(zIndex) ? undefined : zIndex, }; const className = classNames(styles.PositionedOverlay, fixed && styles.fixed, propClassNames); return (<div className={className} style={style} ref={this.setOverlay}> <EventListener event="resize" handler={this.handleMeasurement}/> {render(this.overlayDetails())} </div>); } } export function intersectionWithViewport(rect, viewport = windowRect()) { const top = Math.max(rect.top, 0); const left = Math.max(rect.left, 0); const bottom = Math.min(rect.top + rect.height, viewport.height); const right = Math.min(rect.left + rect.width, viewport.width); return new Rect({ top, left, height: bottom - top, width: right - left, }); } function getMarginsForNode(node) { const nodeStyles = window.getComputedStyle(node); return { activator: parseFloat(nodeStyles.marginTop || ''), container: parseFloat(nodeStyles.marginBottom || ''), horizontal: parseFloat(nodeStyles.marginLeft || ''), }; } function getZIndexForLayerFromNode(node) { const layerNode = closest(node, layer.selector) || document.body; const zIndex = layerNode === document.body ? 'auto' : parseInt(window.getComputedStyle(layerNode).zIndex || '0', 10); return zIndex === 'auto' || isNaN(zIndex) ? null : zIndex; } function windowRect() { return new Rect({ top: window.scrollY, left: window.scrollX, height: window.innerHeight, width: window.innerWidth, }); } function isDocument(node) { return node === document; }