UNPKG

@shopify/polaris

Version:

Shopify’s admin product component library

340 lines (309 loc) • 10.9 kB
import React, { PureComponent, createRef } from 'react'; import { MobileCancelMajor } from '@shopify/polaris-icons'; import { durationSlow } from '@shopify/polaris-tokens'; import { CSSTransition } from 'react-transition-group'; import { classNames } from '../../utilities/css.js'; import { layer, dataPolarisTopBar } from '../shared.js'; import { setRootProperty } from '../../utilities/set-root-property.js'; import styles from './Frame.scss.js'; import { Loading } from './components/Loading/Loading.js'; import { CSSAnimation } from './components/CSSAnimation/CSSAnimation.js'; import { ContextualSaveBar } from './components/ContextualSaveBar/ContextualSaveBar.js'; import { FrameContext } from '../../utilities/frame/context.js'; import { ToastManager } from './components/ToastManager/ToastManager.js'; import { useI18n } from '../../utilities/i18n/hooks.js'; import { useMediaQuery } from '../../utilities/media-query/hooks.js'; import { TrapFocus } from '../TrapFocus/TrapFocus.js'; import { Icon } from '../Icon/Icon.js'; import { Backdrop } from '../Backdrop/Backdrop.js'; import { EventListener } from '../EventListener/EventListener.js'; const GLOBAL_RIBBON_CUSTOM_PROPERTY = '--global-ribbon-height'; const APP_FRAME_MAIN = 'AppFrameMain'; const APP_FRAME_NAV = 'AppFrameNav'; const APP_FRAME_TOP_BAR = 'AppFrameTopBar'; const APP_FRAME_LOADING_BAR = 'AppFrameLoadingBar'; class FrameInner extends PureComponent { constructor(...args) { super(...args); this.state = { skipFocused: false, globalRibbonHeight: 0, loadingStack: 0, toastMessages: [], showContextualSaveBar: false }; this.contextualSaveBar = null; this.globalRibbonContainer = null; this.navigationNode = /*#__PURE__*/createRef(); this.setGlobalRibbonHeight = () => { const { globalRibbonContainer } = this; if (globalRibbonContainer) { this.setState({ globalRibbonHeight: globalRibbonContainer.offsetHeight }, this.setGlobalRibbonRootProperty); } }; this.setGlobalRibbonRootProperty = () => { const { globalRibbonHeight } = this.state; setRootProperty(GLOBAL_RIBBON_CUSTOM_PROPERTY, `${globalRibbonHeight}px`, null); }; this.showToast = toast => { this.setState(({ toastMessages }) => { const hasToastById = toastMessages.find(({ id }) => id === toast.id) != null; return { toastMessages: hasToastById ? toastMessages : [...toastMessages, toast] }; }); }; this.hideToast = ({ id }) => { this.setState(({ toastMessages }) => { return { toastMessages: toastMessages.filter(({ id: toastId }) => id !== toastId) }; }); }; this.setContextualSaveBar = props => { const { showContextualSaveBar } = this.state; this.contextualSaveBar = { ...props }; if (showContextualSaveBar === true) { this.forceUpdate(); } else { this.setState({ showContextualSaveBar: true }); } }; this.removeContextualSaveBar = () => { this.contextualSaveBar = null; this.setState({ showContextualSaveBar: false }); }; this.startLoading = () => { this.setState(({ loadingStack }) => ({ loadingStack: loadingStack + 1 })); }; this.stopLoading = () => { this.setState(({ loadingStack }) => ({ loadingStack: Math.max(0, loadingStack - 1) })); }; this.handleResize = () => { if (this.props.globalRibbon) { this.setGlobalRibbonHeight(); } }; this.handleFocus = () => { this.setState({ skipFocused: true }); }; this.handleBlur = () => { this.setState({ skipFocused: false }); }; this.handleClick = event => { const { skipToContentTarget } = this.props; if (skipToContentTarget && skipToContentTarget.current) { skipToContentTarget.current.focus(); event === null || event === void 0 ? void 0 : event.preventDefault(); } }; this.handleNavigationDismiss = () => { const { onNavigationDismiss } = this.props; if (onNavigationDismiss != null) { onNavigationDismiss(); } }; this.setGlobalRibbonContainer = node => { this.globalRibbonContainer = node; }; this.handleNavKeydown = event => { const { key } = event; const { mediaQuery: { isNavigationCollapsed }, showMobileNavigation } = this.props; const mobileNavShowing = isNavigationCollapsed && showMobileNavigation; if (mobileNavShowing && key === 'Escape') { this.handleNavigationDismiss(); } }; } componentDidMount() { this.handleResize(); if (this.props.globalRibbon) { return; } this.setGlobalRibbonRootProperty(); } componentDidUpdate(prevProps) { if (this.props.globalRibbon !== prevProps.globalRibbon) { this.setGlobalRibbonHeight(); } } render() { const { skipFocused, loadingStack, toastMessages, showContextualSaveBar } = this.state; const { children, navigation, topBar, globalRibbon, showMobileNavigation = false, skipToContentTarget, i18n, mediaQuery: { isNavigationCollapsed } } = this.props; const navClassName = classNames(styles.Navigation, showMobileNavigation && styles['Navigation-visible']); const mobileNavHidden = isNavigationCollapsed && !showMobileNavigation; const mobileNavShowing = isNavigationCollapsed && showMobileNavigation; const tabIndex = mobileNavShowing ? 0 : -1; const mobileNavAttributes = { ...(mobileNavShowing && { 'aria-modal': true, role: 'dialog' }) }; const navigationMarkup = navigation ? /*#__PURE__*/React.createElement(TrapFocus, { trapping: mobileNavShowing }, /*#__PURE__*/React.createElement(CSSTransition, { nodeRef: this.navigationNode, appear: isNavigationCollapsed, exit: isNavigationCollapsed, in: showMobileNavigation, timeout: durationSlow, classNames: navTransitionClasses }, /*#__PURE__*/React.createElement("div", Object.assign({}, mobileNavAttributes, { "aria-label": i18n.translate('Polaris.Frame.navigationLabel'), ref: this.navigationNode, className: navClassName, onKeyDown: this.handleNavKeydown, id: APP_FRAME_NAV, key: "NavContent", hidden: mobileNavHidden }), navigation, /*#__PURE__*/React.createElement("button", { type: "button", className: styles.NavigationDismiss, onClick: this.handleNavigationDismiss, "aria-hidden": mobileNavHidden || !isNavigationCollapsed && !showMobileNavigation, "aria-label": i18n.translate('Polaris.Frame.Navigation.closeMobileNavigationLabel'), tabIndex: tabIndex }, /*#__PURE__*/React.createElement(Icon, { source: MobileCancelMajor }))))) : null; const loadingMarkup = loadingStack > 0 ? /*#__PURE__*/React.createElement("div", { className: styles.LoadingBar, id: APP_FRAME_LOADING_BAR }, /*#__PURE__*/React.createElement(Loading, null)) : null; const contextualSaveBarMarkup = /*#__PURE__*/React.createElement(CSSAnimation, { in: showContextualSaveBar, className: styles.ContextualSaveBar, type: "fade" }, /*#__PURE__*/React.createElement(ContextualSaveBar, this.contextualSaveBar)); const topBarMarkup = topBar ? /*#__PURE__*/React.createElement("div", Object.assign({ className: styles.TopBar }, layer.props, dataPolarisTopBar.props, { id: APP_FRAME_TOP_BAR }), topBar) : null; const globalRibbonMarkup = globalRibbon ? /*#__PURE__*/React.createElement("div", { className: styles.GlobalRibbonContainer, ref: this.setGlobalRibbonContainer }, globalRibbon) : null; const skipClassName = classNames(styles.Skip, skipFocused && styles.focused); const skipTarget = skipToContentTarget !== null && skipToContentTarget !== void 0 && skipToContentTarget.current ? skipToContentTarget.current.id : APP_FRAME_MAIN; const skipMarkup = /*#__PURE__*/React.createElement("div", { className: skipClassName }, /*#__PURE__*/React.createElement("a", { href: `#${skipTarget}`, onFocus: this.handleFocus, onBlur: this.handleBlur, onClick: this.handleClick }, i18n.translate('Polaris.Frame.skipToContent'))); const navigationAttributes = navigation ? { 'data-has-navigation': true } : {}; const frameClassName = classNames(styles.Frame, navigation && styles.hasNav, topBar && styles.hasTopBar); const navigationOverlayMarkup = showMobileNavigation && isNavigationCollapsed ? /*#__PURE__*/React.createElement(Backdrop, { belowNavigation: true, onClick: this.handleNavigationDismiss, onTouchStart: this.handleNavigationDismiss }) : null; const context = { showToast: this.showToast, hideToast: this.hideToast, startLoading: this.startLoading, stopLoading: this.stopLoading, setContextualSaveBar: this.setContextualSaveBar, removeContextualSaveBar: this.removeContextualSaveBar }; return /*#__PURE__*/React.createElement(FrameContext.Provider, { value: context }, /*#__PURE__*/React.createElement("div", Object.assign({ className: frameClassName }, layer.props, navigationAttributes), skipMarkup, topBarMarkup, navigationMarkup, contextualSaveBarMarkup, loadingMarkup, navigationOverlayMarkup, /*#__PURE__*/React.createElement("main", { className: styles.Main, id: APP_FRAME_MAIN, "data-has-global-ribbon": Boolean(globalRibbon) }, /*#__PURE__*/React.createElement("div", { className: styles.Content }, children)), /*#__PURE__*/React.createElement(ToastManager, { toastMessages: toastMessages }), globalRibbonMarkup, /*#__PURE__*/React.createElement(EventListener, { event: "resize", handler: this.handleResize }))); } } const navTransitionClasses = { enter: classNames(styles['Navigation-enter']), enterActive: classNames(styles['Navigation-enterActive']), enterDone: classNames(styles['Navigation-enterActive']), exit: classNames(styles['Navigation-exit']), exitActive: classNames(styles['Navigation-exitActive']) }; function Frame(props) { const i18n = useI18n(); const mediaQuery = useMediaQuery(); return /*#__PURE__*/React.createElement(FrameInner, Object.assign({}, props, { i18n: i18n, mediaQuery: mediaQuery })); } export { Frame };