Skip to content

Commit

Permalink
initial build of sheet component
Browse files Browse the repository at this point in the history
  • Loading branch information
danrosenthal committed Mar 26, 2019
1 parent 46b7484 commit e976448
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 0 deletions.
Empty file added src/components/Sheet/README.md
Empty file.
71 changes: 71 additions & 0 deletions src/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: 100vh;
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%);
}
114 changes: 114 additions & 0 deletions src/components/Sheet/Sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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 styles from './Sheet.scss';

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

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 bottomClassNames = {
enter: classNames(styles.Bottom, styles.enterBottom),
enterActive: classNames(styles.Bottom, styles.enterBottomActive),
exit: classNames(styles.Bottom, styles.exitBottom),
exitActive: classNames(styles.Bottom, styles.exitBottomActive),
};

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

const finalTransitionProps = {
classNames: mobile ? bottomClassNames : rightClassNames,
...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/Sheet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Sheet from './Sheet';

export {Props, isMobile} from './Sheet';
export default Sheet;
31 changes: 31 additions & 0 deletions src/components/Sheet/tests/Sheet.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import {noop} from '@shopify/javascript-utilities/other';
import {mountWithAppProvider} from 'test-utilities';
import Sheet from '../Sheet';

window.matchMedia =
window.matchMedia ||
function() {
return {
matches: false,
addListener() {},
removeListener() {},
};
};

describe('<Sheet />', () => {
const mockProps = {
open: true,
onClose: noop,
};

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

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

expect(drawer.find(children)).not.toBeNull();
});
});
6 changes: 6 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ export {
Props as SettingToggleProps,
} from './SettingToggle';

export {
default as Sheet,
Props as SheetProps,
isMobile as sheetIsMobile,
} from './Sheet';

export {default as Spinner, Props as SpinnerProps} from './Spinner';

export {default as Stack, Props as StackProps} from './Stack';
Expand Down

0 comments on commit e976448

Please sign in to comment.