Skip to content

Commit

Permalink
Merge pull request #13 from vtex-apps/feature/lazy-rendering
Browse files Browse the repository at this point in the history
Lazy content rendering/Swipable update
  • Loading branch information
lbebber authored Jan 6, 2020
2 parents 2167972 + 5929168 commit 65c5156
Show file tree
Hide file tree
Showing 10 changed files with 561 additions and 336 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Allows dragging from outside of the drawer.

### Changed
- Preserves momentum after swipe release.
- Prevents rendering of content if the menu has not been opened.

### Fixed
- Issue where client rendering on the Portal component would be inconsistent with SSR.
- "Accordeon effect" when opening the drawer, when the scrollbars are visible in the user's OS.

## [0.5.0] - 2019-12-27
### Changed
Expand Down
235 changes: 77 additions & 158 deletions react/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,117 +1,64 @@
import React, { useRef, useState, useEffect, ReactElement } from 'react'
import React, { ReactElement, Suspense, useReducer } from 'react'
import { defineMessages } from 'react-intl'

import { IconClose, IconMenu } from 'vtex.store-icons'
import { useCssHandles } from 'vtex.css-handles'

import Overlay from './Overlay'
import Portal from './Portal'
import Swipable from './Swipable'

// https://stackoverflow.com/a/3464890/5313009
const getScrollPosition = () => {
const documentElement =
window && window.document && window.document.documentElement
if (!documentElement) {
return 0
}
return (
(window.pageYOffset || documentElement.scrollTop) -
(documentElement.clientTop || 0)
)
}

const useLockScroll = () => {
const [isLocked, setLocked] = useState(false)
type ScrollPosition = number | null
const [lockedScrollPosition, setLockedScrollPosition] = useState<
ScrollPosition
>(null)

useEffect(() => {
/** Locks scroll of the root HTML element if the
* drawer menu is open
*/
const shouldLockScroll = isLocked

const documentElement =
window && window.document && window.document.documentElement
if (documentElement) {
documentElement.style.overflow = shouldLockScroll ? 'hidden' : 'auto'

/** iOS doesn't lock the scroll of the body by just setting overflow to hidden.
* It requires setting the position of the HTML element to fixed, which also
* resets the scroll position.
* This code is intended to record the scroll position and set it as
* the element's position, and revert it once the menu is closed.
*/
const scrollPosition =
lockedScrollPosition == null
? getScrollPosition()
: lockedScrollPosition

if (lockedScrollPosition == null && shouldLockScroll) {
setLockedScrollPosition(scrollPosition)
}

if (lockedScrollPosition != null && !shouldLockScroll) {
window && window.scrollTo(0, scrollPosition)
setLockedScrollPosition(null)
}
import useLockScroll from './modules/useLockScroll'

documentElement.style.position = shouldLockScroll ? 'fixed' : 'static'
const Swipable = React.lazy(() => import('./Swipable'))

documentElement.style.top = shouldLockScroll
? `-${scrollPosition}px`
: 'auto'

documentElement.style.bottom = shouldLockScroll ? '0' : 'auto'
documentElement.style.left = shouldLockScroll ? '0' : 'auto'
documentElement.style.right = shouldLockScroll ? '0' : 'auto'
}
interface MenuState {
isOpen: boolean
hasBeenOpened: boolean
}

return () => {
documentElement.style.overflow = 'auto'
documentElement.style.position = 'static'
interface MenuAction {
type: 'open' | 'close'
}

documentElement.style.top = 'auto'
documentElement.style.bottom = 'auto'
documentElement.style.left = 'auto'
documentElement.style.right = 'auto'
}
}, [isLocked]) // eslint-disable-line react-hooks/exhaustive-deps
// ☝️ no need to trigger this on lockedScrollPosition changes
const initialMenuState: MenuState = {
isOpen: false,
hasBeenOpened: false,
}

return setLocked
function menuReducer(state: MenuState, action: MenuAction) {
switch (action.type) {
case 'open':
return {
...state,
isOpen: true,
hasBeenOpened: true,
}
case 'close':
return {
...state,
isOpen: false,
}
default:
return state
}
}

const useMenuState = () => {
const [isMenuOpen, setIsOpen] = useState(false)
const [isMenuTransitioning, setIsTransitioning] = useState(false)
const [state, dispatch] = useReducer(menuReducer, initialMenuState)
const setLockScroll = useLockScroll()

let transitioningTimeout: number | null

const setMenuOpen = (value: boolean) => {
setIsOpen(value)
setIsTransitioning(true)
dispatch({ type: value ? 'open' : 'close' })
setLockScroll(value)

if (transitioningTimeout != null) {
clearTimeout(transitioningTimeout)
transitioningTimeout = null
}
transitioningTimeout =
window &&
window.setTimeout(() => {
setIsTransitioning(false)
}, 300)
}

const openMenu = () => setMenuOpen(true)
const closeMenu = () => setMenuOpen(false)

return { isMenuOpen, isMenuTransitioning, setMenuOpen, openMenu, closeMenu }
return {
state,
openMenu,
closeMenu,
}
}

const CSS_HANDLES = [
Expand All @@ -125,48 +72,23 @@ const CSS_HANDLES = [
const Drawer: StorefrontComponent<
DrawerSchema & { customIcon: ReactElement }
> = ({
// actionIconId,
// dismissIconId,
// position,
// height,
width,
customIcon,
maxWidth = 450,
isFullWidth,
slideDirection = 'horizontal',
children,
}) => {
const {
isMenuOpen,
isMenuTransitioning,
openMenu,
closeMenu,
} = useMenuState()
const { state: menuState, openMenu, closeMenu } = useMenuState()
const { isOpen: isMenuOpen, hasBeenOpened: hasMenuBeenOpened } = menuState
const handles = useCssHandles(CSS_HANDLES)
const menuRef = useRef(null)

const slideFromTopToBottom = `translate3d(0, ${
isMenuOpen ? '0' : '-100%'
}, 0)`
const slideFromLeftToRight = `translate3d(${
isMenuOpen ? '0' : '-100%'
}, 0, 0)`
const slideFromRightToLeft = `translate3d(${isMenuOpen ? '0' : '100%'}, 0, 0)`

const resolveSlideDirection = () => {
switch (slideDirection) {
case 'horizontal':
return slideFromLeftToRight
case 'vertical':
return slideFromTopToBottom
case 'leftToRight':
return slideFromLeftToRight
case 'rightToLeft':
return slideFromRightToLeft
default:
return slideFromLeftToRight
}
}

const direction =
slideDirection === 'horizontal' || slideDirection === 'leftToRight'
? 'left'
: 'right'

const swipeHandler = direction === 'left' ? 'onSwipeLeft' : 'onSwipeRight'

return (
<>
Expand All @@ -179,47 +101,44 @@ const Drawer: StorefrontComponent<
</div>
<Portal>
<Overlay visible={isMenuOpen} onClick={closeMenu} />

<Swipable
enabled={isMenuOpen}
element={menuRef && menuRef.current}
onSwipeLeft={
slideDirection === 'horizontal' || slideDirection === 'leftToRight'
? closeMenu
: null
}
onSwipeRight={slideDirection === 'rightToLeft' ? closeMenu : null}
rubberBanding
>
<div
ref={menuRef}
className={`${handles.drawer} fixed top-0 ${
slideDirection === 'rightToLeft' ? 'right-0' : 'left-0'
} bottom-0 bg-base z-999 flex flex-column`}
<Suspense fallback={<React.Fragment />}>
<Swipable
{...{
[swipeHandler]: closeMenu,
}}
enabled={isMenuOpen}
position={isMenuOpen ? 'center' : direction}
allowOutsideDrag
className={`${handles.drawer} ${
direction === 'right' ? 'right-0' : 'left-0'
} fixed top-0 bottom-0 bg-base z-999 flex flex-column`}
style={{
WebkitOverflowScrolling: 'touch',
overflowY: 'scroll',
width: width || (isFullWidth ? '100%' : '85%'),
maxWidth,
pointerEvents: isMenuOpen ? 'auto' : 'none',
transform: resolveSlideDirection(),
transition: isMenuTransitioning ? 'transform 300ms' : 'none',
minWidth: 280,
pointerEvents: isMenuOpen ? 'auto' : 'none',
}}
>
<div className={`flex ${handles.closeIconContainer}`}>
<button
className={`pa4 pointer bg-transparent transparent bn pointer ${handles.closeIconButton}`}
onClick={closeMenu}
>
<IconClose size={30} type="line" />
</button>
</div>
<div className={`${handles.childrenContainer} flex flex-grow-1`}>
{children}
<div
style={{
WebkitOverflowScrolling: 'touch',
overflowY: 'scroll',
}}
>
<div className={`flex ${handles.closeIconContainer}`}>
<button
className={`${handles.closeIconButton} pa4 pointer bg-transparent transparent bn pointer`}
onClick={closeMenu}
>
<IconClose size={30} type="line" />
</button>
</div>
<div className={`${handles.childrenContainer} flex flex-grow-1`}>
{hasMenuBeenOpened && children}
</div>
</div>
</div>
</Swipable>
</Swipable>
</Suspense>
</Portal>
</>
)
Expand Down
4 changes: 3 additions & 1 deletion react/Portal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { FunctionComponent } from 'react'
import ReactDOM from 'react-dom'
import { useSSR } from 'vtex.render-runtime'

const Portal: FunctionComponent = ({ children }) => {
const body = window && window.document && window.document.body
const isSSR = useSSR()

if (!body) {
if (!body || isSSR) {
return null
}

Expand Down
Loading

0 comments on commit 65c5156

Please sign in to comment.