UNPKG

@hackplan/polaris

Version:

Shopify’s product component library

199 lines (198 loc) 8.14 kB
import React from 'react'; import isEqual from 'lodash/isEqual'; import { TransitionGroup } from 'react-transition-group'; import { write } from '@shopify/javascript-utilities/fastdom'; import { focusFirstFocusableNode } from '@shopify/javascript-utilities/focus'; import { createUniqueIDFactory } from '@shopify/javascript-utilities/other'; import { Modal as AppBridgeModal } from '@shopify/app-bridge/actions'; import WithinContentContext from '../WithinContentContext'; import { wrapWithComponent } from '../../utilities/components'; import { transformActions } from '../../utilities/app-bridge-transformers'; import pick from '../../utilities/pick'; import { withAppProvider } from '../AppProvider'; import Backdrop from '../Backdrop'; import Scrollable from '../Scrollable'; import Spinner from '../Spinner'; import Portal from '../Portal'; import { CloseButton, Dialog, Footer, Header, Section, } from './components'; import styles from './Modal.scss'; const IFRAME_LOADING_HEIGHT = 200; const getUniqueID = createUniqueIDFactory('modal-header'); const APP_BRIDGE_PROPS = [ 'title', 'size', 'message', 'src', 'primaryAction', 'secondaryActions', ]; export class Modal extends React.Component { constructor() { super(...arguments); this.state = { iframeHeight: IFRAME_LOADING_HEIGHT, }; this.headerId = getUniqueID(); this.handleEntered = () => { const { onTransitionEnd } = this.props; if (onTransitionEnd) { onTransitionEnd(); } }; this.handleExited = () => { this.setState({ iframeHeight: IFRAME_LOADING_HEIGHT, }); if (this.focusReturnPointNode) { write(() => focusFirstFocusableNode(this.focusReturnPointNode, false)); } }; this.handleIFrameLoad = (evt) => { const iframe = evt.target; if (iframe && iframe.contentWindow) { this.setState({ iframeHeight: iframe.contentWindow.document.body.scrollHeight, }); } const { onIFrameLoad } = this.props; if (onIFrameLoad != null) { onIFrameLoad(evt); } }; } componentDidMount() { if (this.props.polaris.appBridge == null) { return; } // eslint-disable-next-line no-console console.warn("Deprecation: Using `Modal` in an embedded app is deprecated and will be removed in v5.0. Use `Modal` from `@shopify/app-bridge-react` instead. For example, `import {Modal} from '@shopify/app-bridge-react';`"); const transformProps = this.transformProps(); if (transformProps) { this.appBridgeModal = AppBridgeModal.create(this.props.polaris.appBridge, transformProps); } if (this.appBridgeModal) { this.appBridgeModal.subscribe(AppBridgeModal.Action.CLOSE, this.props.onClose); } const { open } = this.props; if (open) { this.focusReturnPointNode = document.activeElement; this.appBridgeModal && this.appBridgeModal.dispatch(AppBridgeModal.Action.OPEN); } } componentDidUpdate(prevProps) { if (this.props.polaris.appBridge == null || this.appBridgeModal == null) { return; } const { open } = this.props; const wasOpen = prevProps.open; const transformedProps = this.transformProps(); const prevAppBridgeProps = pick(prevProps, APP_BRIDGE_PROPS); const currentAppBridgeProps = pick(this.props, APP_BRIDGE_PROPS); if (!isEqual(prevAppBridgeProps, currentAppBridgeProps) && transformedProps) { if (isIframeModal(transformedProps)) { this.appBridgeModal.set(transformedProps); } else { this.appBridgeModal.set(transformedProps); } } if (wasOpen !== open) { if (open) { this.appBridgeModal.dispatch(AppBridgeModal.Action.OPEN); } else { this.appBridgeModal.dispatch(AppBridgeModal.Action.CLOSE); } } if (!wasOpen && open) { this.focusReturnPointNode = document.activeElement; } else if (wasOpen && !open && this.focusReturnPointNode != null && document.contains(this.focusReturnPointNode)) { this.focusReturnPointNode.focus(); this.focusReturnPointNode = null; } } componentWillUnmount() { if (this.props.polaris.appBridge == null || this.appBridgeModal == null) { return; } this.appBridgeModal.unsubscribe(); } render() { if (this.props.polaris.appBridge != null) { return null; } const { children, title, src, iFrameName, open, instant, sectioned, loading, large, limitHeight, onClose, footer, primaryAction, secondaryActions, polaris: { intl }, onScrolledToBottom, } = this.props; const { iframeHeight } = this.state; const iframeTitle = intl.translate('Polaris.Modal.iFrameTitle'); let dialog; let backdrop; if (open) { const footerMarkup = !footer && !primaryAction && !secondaryActions ? null : (<Footer primaryAction={primaryAction} secondaryActions={secondaryActions}> {footer} </Footer>); const content = sectioned ? wrapWithComponent(children, Section, {}) : children; const body = loading ? (<div className={styles.Spinner}> <Spinner /> </div>) : (content); const bodyMarkup = src ? (<iframe name={iFrameName} title={iframeTitle} src={src} className={styles.IFrame} onLoad={this.handleIFrameLoad} style={{ height: `${iframeHeight}px` }}/>) : (<Scrollable shadow className={styles.Body} onScrolledToBottom={onScrolledToBottom}> {body} </Scrollable>); const headerMarkup = title ? (<Header id={this.headerId} onClose={onClose} testID="ModalHeader"> {title} </Header>) : (<CloseButton onClick={onClose} title={false} testID="ModalCloseButton"/>); dialog = (<Dialog instant={instant} labelledBy={this.headerId} onClose={onClose} onEntered={this.handleEntered} onExited={this.handleExited} large={large} limitHeight={limitHeight}> {headerMarkup} <div className={styles.BodyWrapper}>{bodyMarkup}</div> {footerMarkup} </Dialog>); backdrop = <Backdrop />; } const animated = !instant; return (<WithinContentContext.Provider value> <Portal idPrefix="modal"> <TransitionGroup appear={animated} enter={animated} exit={animated}> {dialog} </TransitionGroup> {backdrop} </Portal> </WithinContentContext.Provider>); } transformProps() { const { title, size, message, src, primaryAction, secondaryActions, polaris, } = this.props; const { appBridge } = polaris; if (!appBridge) return; const safeTitle = typeof title === 'string' ? title : undefined; const safeSize = size != null ? AppBridgeModal.Size[size] : undefined; const srcPayload = {}; if (src != null) { if (src.match('^https?://')) { srcPayload.url = src; } else { srcPayload.path = src; } } return Object.assign({ title: safeTitle, message, size: safeSize }, srcPayload, { footer: { buttons: transformActions(appBridge, { primaryAction, secondaryActions, }), } }); } } Modal.Dialog = Dialog; Modal.Section = Section; function isIframeModal(options) { return (typeof options.url === 'string' || typeof options.path === 'string'); } export default withAppProvider()(Modal);