Skip to content

Commit

Permalink
initial move of Sheet as subcomponent of Frame
Browse files Browse the repository at this point in the history
  • Loading branch information
danrosenthal committed Mar 26, 2019
1 parent dcc6c55 commit 153337e
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 1 deletion.
27 changes: 26 additions & 1 deletion src/components/Frame/Frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import {MobileCancelMajorMonotone} from '@shopify/polaris-icons';
import {classNames} from '@shopify/react-utilities/styles';
import {CSSTransition} from 'react-transition-group';
import {noop} from 'babel-types';
import {navigationBarCollapsed} from '../../utilities/breakpoints';
import Button from '../Button';
import Icon from '../Icon';
Expand All @@ -17,8 +18,9 @@ import {
FrameContext,
frameContextTypes,
ToastProps,
SheetProps,
} from './types';
import {ToastManager, Loading, ContextualSaveBar} from './components';
import {ToastManager, Loading, ContextualSaveBar, Sheet} from './components';

import styles from './Frame.scss';

Expand All @@ -44,6 +46,7 @@ export interface State {
loadingStack: number;
toastMessages: (ToastProps & {id: string})[];
showContextualSaveBar: boolean;
sheet: SheetProps;
}

export const GLOBAL_RIBBON_CUSTOM_PROPERTY = '--global-ribbon-height';
Expand All @@ -64,6 +67,7 @@ export class Frame extends React.PureComponent<CombinedProps, State> {
toastMessages: [],
mobileView: isMobileView(),
showContextualSaveBar: false,
sheet: initializeSheet(),
};

private contextualSaveBar: ContextualSaveBarProps | null;
Expand Down Expand Up @@ -104,6 +108,7 @@ export class Frame extends React.PureComponent<CombinedProps, State> {
toastMessages,
showContextualSaveBar,
mobileView,
sheet,
} = this.state;
const {
children,
Expand Down Expand Up @@ -240,6 +245,8 @@ export class Frame extends React.PureComponent<CombinedProps, State> {
/>
) : null;

const sheetMarkup = <Sheet {...sheet} />;

return (
<div
className={frameClassName}
Expand All @@ -263,11 +270,21 @@ export class Frame extends React.PureComponent<CombinedProps, State> {
</main>
<ToastManager toastMessages={toastMessages} />
{globalRibbonMarkup}
{sheetMarkup}
<EventListener event="resize" handler={this.handleResize} />
</div>
);
}

// showSheet
// takes sheet props
// if current sheet is open, should hide sheet first
// calls setState on the sheet property with recieved sheet props

// hideSheet
// takes no props
// calls setState on the sheet property with initializeSheet()

private setGlobalRibbonHeight = () => {
const {globalRibbonContainer} = this;
if (globalRibbonContainer) {
Expand Down Expand Up @@ -404,4 +421,12 @@ function isMobileView() {
return navigationBarCollapsed().matches;
}

function initializeSheet(): SheetProps {
return {
open: false,
children: null,
onClose: noop,
};
}

export default withAppProvider<Props>()(Frame);
Empty file.
71 changes: 71 additions & 0 deletions src/components/Frame/components/Sheet/Sheet.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
$sheet-desktop-width: rem(380px);

.Sheet {
position: fixed;
bottom: 0;
width: 100vw;
height: 100%;
background: color('white');
box-shadow: shadow('layer');

&.desktop {
right: 0;
width: $sheet-desktop-width;
}

&:focus {
outline: 0;
}
}

.Container {
position: fixed;
z-index: z-index('modal', $fixed-element-stacking-order);
top: 0;
right: 0;
bottom: 0;
left: 0;

&.desktop {
left: auto;
width: $sheet-desktop-width;
}
}

// Bottom
.Bottom {
will-change: transform;
transition: transform duration('slow') easing('base');
transform-origin: bottom;
}
.enterBottom {
transform: translateY(100%);
}
.enterBottomActive {
transform: translateY(0%);
}
.exitBottom {
transform: translateY(0%);
}
.exitBottomActive {
transform: translateY(100%);
}

// Right
.Right {
will-change: transform;
transition: transform duration('slow') easing('base');
transform-origin: right;
}
.enterRight {
transform: translateX(100%);
}
.enterRightActive {
transform: translateX(0%);
}
.exitRight {
transform: translateX(0%);
}
.exitRightActive {
transform: translateX(100%);
}
110 changes: 110 additions & 0 deletions src/components/Frame/components/Sheet/Sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as React from 'react';

import {CSSTransition} from 'react-transition-group';
import debounce from 'lodash/debounce';
import {classNames} from '@shopify/react-utilities/styles';

import {navigationBarCollapsed} from '../../../../utilities/breakpoints';
import {Key} from '../../../../types';
import {layer, overlay, Duration} from '../../../shared';

import TrapFocus from '../../../TrapFocus';
import Portal from '../../../Portal';
import KeypressListener from '../../../KeypressListener';
import EventListener from '../../../EventListener';

import {SheetProps as Props} from '../../types';

import styles from './Sheet.scss';

export const BOTTOM_CLASS_NAMES = {
enter: classNames(styles.Bottom, styles.enterBottom),
enterActive: classNames(styles.Bottom, styles.enterBottomActive),
exit: classNames(styles.Bottom, styles.exitBottom),
exitActive: classNames(styles.Bottom, styles.exitBottomActive),
};

export const RIGHT_CLASS_NAMES = {
enter: classNames(styles.Right, styles.enterRight),
enterActive: classNames(styles.Right, styles.enterRightActive),
exit: classNames(styles.Right, styles.exitRight),
exitActive: classNames(styles.Right, styles.exitRightActive),
};

export interface State {
mobile: boolean;
}

export default class Sheet extends React.PureComponent<Props, State> {
state: State = {
mobile: false,
};

private handleResize = debounce(
() => {
const {mobile} = this.state;
if (mobile !== isMobile()) {
this.setState({mobile: !mobile});
}
},
40,
{leading: true, trailing: true, maxWait: 40},
);

componentDidMount() {
const {mobile} = this.state;
if (mobile !== isMobile()) {
this.setState({mobile: !mobile});
}
}

render() {
const {children, open, onClose} = this.props;
const {mobile} = this.state;

const sheetClassName = classNames(styles.Sheet, !mobile && styles.desktop);

const containerClassName = classNames(
styles.Container,
!mobile && styles.desktop,
);

function Container() {
return (
<div className={containerClassName} {...layer.props} {...overlay.props}>
<TrapFocus trapping={open}>
<div role="dialog" tabIndex={-1} className={sheetClassName}>
{children}
</div>
</TrapFocus>
</div>
);
}

const sharedTransitionProps = {
timeout: Duration.Slow,
in: open,
mountOnEnter: true,
unmountOnExit: true,
};

const finalTransitionProps = {
classNames: mobile ? BOTTOM_CLASS_NAMES : RIGHT_CLASS_NAMES,
...sharedTransitionProps,
};

return (
<Portal idPrefix="sheet">
<CSSTransition {...finalTransitionProps}>
<Container />
</CSSTransition>
<KeypressListener keyCode={Key.Escape} handler={onClose} />
<EventListener event="resize" handler={this.handleResize} />
</Portal>
);
}
}

export function isMobile(): boolean {
return navigationBarCollapsed().matches;
}
4 changes: 4 additions & 0 deletions src/components/Frame/components/Sheet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Sheet from './Sheet';

export {isMobile} from './Sheet';
export default Sheet;
70 changes: 70 additions & 0 deletions src/components/Frame/components/Sheet/tests/Sheet.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as React from 'react';

import {CSSTransition} from 'react-transition-group';
import {noop} from '@shopify/javascript-utilities/other';
import {matchMedia} from '@shopify/jest-dom-mocks';
import {mountWithAppProvider} from 'test-utilities';

import Sheet, {isMobile, BOTTOM_CLASS_NAMES, RIGHT_CLASS_NAMES} from '../Sheet';

describe('<Sheet />', () => {
beforeEach(() => {
matchMedia.mock();
});

afterEach(() => {
matchMedia.restore();
});

const mockProps = {
open: true,
onClose: noop,
};

it('renders its children', () => {
const children = <div>Content</div>;

const sheet = mountWithAppProvider(
<Sheet {...mockProps}>{children}</Sheet>,
);

expect(sheet.find(children)).not.toBeNull();
});

it('renders a css transition component with bottom class names at mobile sizes', () => {
matchMedia.setMedia(() => ({matches: true}));

const sheet = mountWithAppProvider(
<Sheet {...mockProps}>
<div>Content</div>
</Sheet>,
);

expect(sheet.find(CSSTransition).props().classNames).toEqual(
BOTTOM_CLASS_NAMES,
);
});

it('renders a css transition component with right class names at desktop sizes', () => {
const sheet = mountWithAppProvider(
<Sheet {...mockProps}>
<div>Content</div>
</Sheet>,
);

expect(sheet.find(CSSTransition).props().classNames).toEqual(
RIGHT_CLASS_NAMES,
);
});

describe('isMobile', () => {
it('returns false by default', () => {
expect(isMobile()).toBe(false);
});

it('returns true at mobile sizes', () => {
matchMedia.setMedia(() => ({matches: true}));
expect(isMobile()).toBe(true);
});
});
});
1 change: 1 addition & 0 deletions src/components/Frame/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export {
} from './ToastManager';
export {default as Loading, Props as LoadingProps} from './Loading';
export {default as ContextualSaveBar} from './ContextualSaveBar';
export {default as Sheet, isMobile as sheetIsMobile} from './Sheet';
6 changes: 6 additions & 0 deletions src/components/Frame/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,9 @@ export interface ToastProps {
/** Adds an action next to the message (stand-alone app use only) */
action?: Action;
}

export interface SheetProps {
open: boolean;
children: React.ReactNode;
onClose(): void;
}

0 comments on commit 153337e

Please sign in to comment.