@shopify/polaris
Version:
Shopify’s product component library
218 lines (217 loc) • 10.2 kB
JavaScript
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);