UNPKG

@shopify/polaris

Version:

Shopify’s product component library

218 lines (217 loc) • 10.2 kB
import React, { createRef } from 'react'; import { MobileCancelMajorMonotone } from '@shopify/polaris-icons'; import { durationSlow } from '@shopify/polaris-tokens'; import { CSSTransition } from '@material-ui/react-transition-group'; import { classNames } from '../../utilities/css'; import { Icon } from '../Icon'; import { EventListener } from '../EventListener'; import { withAppProvider, } from '../../utilities/with-app-provider'; import { Backdrop } from '../Backdrop'; import { TrapFocus } from '../TrapFocus'; import { dataPolarisTopBar, layer } from '../shared'; import { setRootProperty } from '../../utilities/set-root-property'; import { FrameContext, } from '../../utilities/frame'; import { ToastManager, Loading, ContextualSaveBar, CSSAnimation, AnimationType, } from './components'; import styles from './Frame.scss'; export const GLOBAL_RIBBON_CUSTOM_PROPERTY = '--global-ribbon-height'; export const APP_FRAME_MAIN = 'AppFrameMain'; export const APP_FRAME_MAIN_ANCHOR_TARGET = 'AppFrameMainContent'; const APP_FRAME_NAV = 'AppFrameNav'; const APP_FRAME_TOP_BAR = 'AppFrameTopBar'; const APP_FRAME_LOADING_BAR = 'AppFrameLoadingBar'; class Frame extends React.PureComponent { constructor() { super(...arguments); this.state = { skipFocused: false, globalRibbonHeight: 0, loadingStack: 0, toastMessages: [], showContextualSaveBar: false, }; this.globalRibbonContainer = null; this.navigationNode = createRef(); this.skipToMainContentTargetNode = this.props.skipToContentTarget || React.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 = Object.assign({}, 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 = () => { this.skipToMainContentTargetNode.current && this.skipToMainContentTargetNode.current.focus(); }; this.handleNavigationDismiss = () => { const { onNavigationDismiss } = this.props; if (onNavigationDismiss != null) { onNavigationDismiss(); } }; this.setGlobalRibbonContainer = (node) => { this.globalRibbonContainer = node; }; this.handleNavKeydown = (event) => { const { key } = event; if (key === 'Escape') { this.handleNavigationDismiss(); } }; this.findNavigationNode = () => { return this.navigationNode.current; }; } 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, polaris: { intl, 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 navigationMarkup = navigation ? (<TrapFocus trapping={mobileNavShowing}> <CSSTransition findDOMNode={this.findNavigationNode} appear={isNavigationCollapsed} exit={isNavigationCollapsed} in={showMobileNavigation} timeout={durationSlow} classNames={navTransitionClasses}> <div ref={this.navigationNode} className={navClassName} onKeyDown={this.handleNavKeydown} id={APP_FRAME_NAV} key="NavContent" hidden={mobileNavHidden}> {navigation} <button type="button" className={styles.NavigationDismiss} onClick={this.handleNavigationDismiss} aria-hidden={mobileNavHidden || (!isNavigationCollapsed && !showMobileNavigation)} aria-label={intl.translate('Polaris.Frame.Navigation.closeMobileNavigationLabel')} tabIndex={tabIndex}> <Icon source={MobileCancelMajorMonotone} color="white"/> </button> </div> </CSSTransition> </TrapFocus>) : null; const loadingMarkup = loadingStack > 0 ? (<div className={styles.LoadingBar} id={APP_FRAME_LOADING_BAR}> <Loading /> </div>) : null; const contextualSaveBarMarkup = (<CSSAnimation in={showContextualSaveBar} className={styles.ContextualSaveBar} type={AnimationType.Fade}> <ContextualSaveBar {...this.contextualSaveBar}/> </CSSAnimation>); const topBarMarkup = topBar ? (<div className={styles.TopBar} {...layer.props} {...dataPolarisTopBar.props} id={APP_FRAME_TOP_BAR}> {topBar} </div>) : null; const globalRibbonMarkup = globalRibbon ? (<div className={styles.GlobalRibbonContainer} ref={this.setGlobalRibbonContainer}> {globalRibbon} </div>) : null; const skipClassName = classNames(styles.Skip, skipFocused && styles.focused); const skipTarget = skipToContentTarget ? (skipToContentTarget.current && skipToContentTarget.current.id) || '' : APP_FRAME_MAIN_ANCHOR_TARGET; const skipMarkup = (<div className={skipClassName}> <a href={`#${skipTarget}`} onFocus={this.handleFocus} onBlur={this.handleBlur} onClick={this.handleClick} className={styles.SkipAnchor}> {intl.translate('Polaris.Frame.skipToContent')} </a> </div>); const navigationAttributes = navigation ? { 'data-has-navigation': true, } : {}; const frameClassName = classNames(styles.Frame, navigation && styles.hasNav, topBar && styles.hasTopBar); const navigationOverlayMarkup = showMobileNavigation && isNavigationCollapsed ? (<Backdrop belowNavigation onClick={this.handleNavigationDismiss} onTouchStart={this.handleNavigationDismiss}/>) : null; const skipToMainContentTarget = skipToContentTarget ? null : ( // eslint-disable-next-line jsx-a11y/anchor-is-valid <a id={APP_FRAME_MAIN_ANCHOR_TARGET} ref={this.skipToMainContentTargetNode} tabIndex={-1}/>); const context = { showToast: this.showToast, hideToast: this.hideToast, startLoading: this.startLoading, stopLoading: this.stopLoading, setContextualSaveBar: this.setContextualSaveBar, removeContextualSaveBar: this.removeContextualSaveBar, }; return (<FrameContext.Provider value={context}> <div className={frameClassName} {...layer.props} {...navigationAttributes}> {skipMarkup} {topBarMarkup} {navigationMarkup} {contextualSaveBarMarkup} {loadingMarkup} {navigationOverlayMarkup} <main className={styles.Main} id={APP_FRAME_MAIN} data-has-global-ribbon={Boolean(globalRibbon)}> {skipToMainContentTarget} <div className={styles.Content}>{children}</div> </main> <ToastManager toastMessages={toastMessages}/> {globalRibbonMarkup} <EventListener event="resize" handler={this.handleResize}/> </div> </FrameContext.Provider>); } } 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']), }; // Use named export once withAppProvider is refactored away // eslint-disable-next-line import/no-default-export export default withAppProvider()(Frame);