diff --git a/src/components/Sheet/README.md b/src/components/Sheet/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/components/Sheet/Sheet.scss b/src/components/Sheet/Sheet.scss new file mode 100644 index 00000000000..b8a848080a6 --- /dev/null +++ b/src/components/Sheet/Sheet.scss @@ -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%); +} diff --git a/src/components/Sheet/Sheet.tsx b/src/components/Sheet/Sheet.tsx new file mode 100644 index 00000000000..5f1f3bd41a2 --- /dev/null +++ b/src/components/Sheet/Sheet.tsx @@ -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 { + 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 ( +
+ +
+ {children} +
+
+
+ ); + } + + 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 ( + + + + + + + + ); + } +} + +export function isMobile(): boolean { + return navigationBarCollapsed().matches; +} diff --git a/src/components/Sheet/index.ts b/src/components/Sheet/index.ts new file mode 100644 index 00000000000..d2f90771d02 --- /dev/null +++ b/src/components/Sheet/index.ts @@ -0,0 +1,4 @@ +import Sheet from './Sheet'; + +export {Props, isMobile} from './Sheet'; +export default Sheet; diff --git a/src/components/Sheet/tests/Sheet.test.tsx b/src/components/Sheet/tests/Sheet.test.tsx new file mode 100644 index 00000000000..304417031d1 --- /dev/null +++ b/src/components/Sheet/tests/Sheet.test.tsx @@ -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('', () => { + const mockProps = { + open: true, + onClose: noop, + }; + + it('renders its children', () => { + const children =
Content
; + + const drawer = mountWithAppProvider( + {children}, + ); + + expect(drawer.find(children)).not.toBeNull(); + }); +}); diff --git a/src/components/index.ts b/src/components/index.ts index f2ee8e288d7..b8fc2a43655 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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';