UNPKG

@rakhimgaliyev/react-bottom-sheet

Version:

[![npm version](https://img.shields.io/npm/v/@rakhimgaliyev/react-bottom-sheet.svg?style=flat-square)](https://www.npmjs.com/package/@rakhimgaliyev/react-bottom-sheet) [![gzip size][gzip-badge]][unpkg-dist]

708 lines (612 loc) 21.6 kB
import React, { useRef, useEffect, useState, useMemo, useCallback } from 'react'; import { createUseStyles } from 'react-jss'; import cx from 'classnames'; import ReactDOM from 'react-dom'; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /** * Creates DOM element to be used as React root. * @returns {HTMLElement} */ var createRootElement = function createRootElement(id) { var rootContainer = document.createElement('div'); rootContainer.setAttribute('id', id); rootContainer.style.cssText = 'position: fixed; z-index: 300;'; return rootContainer; }; /** * Appends element as last child of body. * @param {HTMLElement} rootElem */ var addRootElement = function addRootElement(rootElem) { document.body.insertBefore(rootElem, // @ts-ignore document.body.lastElementChild.nextElementSibling); }; /** * Hook to create a React Portal. * Automatically handles creating and tearing-down the root elements (no SRR * makes this trivial), so there is no need to ensure the parent target already * exists. * @example * const target = usePortal(id, [id]); * return createPortal(children, target); * @param {String} id The id of the target container, e.g 'modal' or 'spotlight' * @returns {HTMLElement} The DOM node to use as the Portal target. */ var usePortal = function usePortal(id) { var rootElemRef = useRef(null); useEffect(function () { // Look for existing target dom element to append to var existingParent = document.querySelector("#" + id); // Parent is either a new root or the existing dom element var parentElem = existingParent || createRootElement(id); // If there is no existing DOM element, add a new one. if (!existingParent) { addRootElement(parentElem); } // Add the detached element to the parent // @ts-ignore parentElem.appendChild(rootElemRef.current); return function () { // @ts-ignore rootElemRef.current.remove(); if (!parentElem.childElementCount) { parentElem.remove(); } }; }, [id]); /** * It's important we evaluate this lazily: * - We need first render to contain the DOM element, so it shouldn't happen * in useEffect. We would normally put this in the constructor(). * - We can't do 'const rootElemRef = useRef(document.createElement('div))', * since this will run every single render (that's a lot). * - We want the ref to consistently point to the same DOM element and only * ever run once. * @link https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily */ var getRootElem = function getRootElem() { if (!rootElemRef.current) { // @ts-ignore rootElemRef.current = document.createElement('div'); // @ts-ignore rootElemRef.current.style.cssText = 'top: 0px; bottom: 0px; left: 0px; right: 0px; position: fixed; overflow: hidden;'; } return rootElemRef.current; }; return getRootElem(); }; /** * @example * <Portal id="modal"> * <p>Thinking with portals</p> * </Portal> */ var Portal = function Portal(_ref) { var id = _ref.id, children = _ref.children; var target = usePortal(id); if (target) { return ReactDOM.createPortal(children, target); } return React.createElement(React.Fragment, null); }; var useWindowSize = function useWindowSize() { var getSize = function getSize() { return { width: window.innerWidth, height: window.innerHeight }; }; var _useState = useState(getSize), windowSize = _useState[0], setWindowSize = _useState[1]; useEffect(function () { var handleResize = function handleResize() { setWindowSize(getSize()); }; window.addEventListener('resize', handleResize); return function () { return window.removeEventListener('resize', handleResize); }; }, []); return windowSize; }; var lastId = 0; var uniqueId = function uniqueId() { lastId++; return lastId; }; var DIALOG_BORDER_RADIUS_PX = 30; var animationDuration = 0.25; var useStyles = /*#__PURE__*/createUseStyles(function () { return { '@global': { 'html[data-hide-scroll], html[data-hide-scroll] body': { position: 'relative !important' } }, root: { top: 'auto', bottom: 0, left: 0, width: '100%', willChange: 'transform', position: 'fixed', zIndex: 2, '-webkit-transform': 'matrix(1, 0, 0, 1, 0, 0)', '@media (min-width: 641px)': { left: '50%', maxWidth: 500, transform: 'translateX(-50%)', transition: 'translateX(-50%)' } }, rootIsOpen: { pointerEvents: 'auto' }, mask: { position: 'fixed', top: -375, left: 0, bottom: 0, right: 0, opacity: 0, background: 'rgba(0, 0, 0, 0.7);', transition: "opacity " + animationDuration + "s cubic-bezier(0.7, 0.3, 0.1, 1)", pointerEvents: 'auto', '-webkit-transform': 'translate3d(0, 0, 0)' }, maskIsOpen: { opacity: 1 }, contentWrap: { width: '100%', bottom: 0, background: '#fff', overflow: 'hidden', '-webkit-transform': 'translate3d(0,0,0)', zIndex: 2, overscrollBehavior: 'none', scrollbarWidth: 'none', '-ms-overflow-style': 'none', '&::-webkit-scrollbar': { display: 'none' }, position: 'fixed', borderRadius: DIALOG_BORDER_RADIUS_PX + "px " + DIALOG_BORDER_RADIUS_PX + "px 0px 0px", display: 'flex', justifyContent: 'center', transition: "transform " + animationDuration + "s cubic-bezier(0.7, 0.3, 0.1, 1)" }, inner: { width: '100%', overflowY: 'hidden', overflowX: 'hidden' }, content: { display: 'flex', flexDirection: 'column', width: '100%' }, scrollDiv: { display: 'flex', flexDirection: 'column', width: '100%', overflowY: 'auto', overflowX: 'hidden' }, header: { position: 'relative', left: 0, top: 0, width: '100%' }, footer: { position: 'relative', left: 0, bottom: 0, width: '100%' }, shadowBox: { pointerEvents: 'none', position: 'absolute', zIndex: 2, width: '100%' } }; }); var touchInitState = { startY: 0, touchStartY: 0, isTop: true, noScroll: false, startScrollTop: 0 }; var CLOSE_DIALOG_PERCENT = 0.25; var BottomSheetStatus; (function (BottomSheetStatus) { // eslint-disable-next-line no-unused-vars BottomSheetStatus[BottomSheetStatus["DIALOG_INIT"] = 1] = "DIALOG_INIT"; // eslint-disable-next-line no-unused-vars BottomSheetStatus[BottomSheetStatus["DIALOG_STARTED_TO_OPEN"] = 2] = "DIALOG_STARTED_TO_OPEN"; // eslint-disable-next-line no-unused-vars BottomSheetStatus[BottomSheetStatus["DIALOG_IS_OPENING"] = 3] = "DIALOG_IS_OPENING"; // eslint-disable-next-line no-unused-vars BottomSheetStatus[BottomSheetStatus["DIALOG_IS_OPEN"] = 4] = "DIALOG_IS_OPEN"; // eslint-disable-next-line no-unused-vars BottomSheetStatus[BottomSheetStatus["DIALOG_STARTED_TO_CLOSE"] = 5] = "DIALOG_STARTED_TO_CLOSE"; // eslint-disable-next-line no-unused-vars BottomSheetStatus[BottomSheetStatus["DIALOG_IS_CLOSING"] = 6] = "DIALOG_IS_CLOSING"; // eslint-disable-next-line no-unused-vars BottomSheetStatus[BottomSheetStatus["DIALOG_IS_CLOSED"] = 7] = "DIALOG_IS_CLOSED"; })(BottomSheetStatus || (BottomSheetStatus = {})); var BottomSheetDialog = function BottomSheetDialog(_ref) { var open = _ref.open, setOpen = _ref.setOpen, children = _ref.children, header = _ref.header, footer = _ref.footer, horizontalScrollElRef = _ref.horizontalScrollElRef; var classes = useStyles(); var maskRef = useRef(null); var contentRef = useRef(null); var innerRef = useRef(null); var windowSize = useWindowSize(); var _useState = useState(uniqueId()), bottomSheetId = _useState[0]; var _useState2 = useState(BottomSheetStatus.DIALOG_INIT), dialogViewState = _useState2[0], setDialogViewState = _useState2[1]; var _useState3 = useState(false), isMovingContent = _useState3[0], setIsMovingContent = _useState3[1]; var _useState4 = useState(false), isTouchMoveHandled = _useState4[0], setIsTouchMoveHandled = _useState4[1]; var _useState5 = useState(touchInitState), touchState = _useState5[0], setTouchState = _useState5[1]; var _useState6 = useState({ curr: 0, prev: 0 }), touchY = _useState6[0], setTouchY = _useState6[1]; var _useState7 = useState(0), scrollPercent = _useState7[0], setScrollPercent = _useState7[1]; var clearStates = function clearStates() { setIsMovingContent(false); setIsTouchMoveHandled(false); setTouchState(touchInitState); setTouchY({ curr: 0, prev: 0 }); }; var _useState8 = useState({ startX: 0, startY: 0, isCalculated: false, preventScroll: false }), horizontalScrollElTouch = _useState8[0], setHorizontalScrollElTouch = _useState8[1]; var isShown = useMemo(function () { return dialogViewState === BottomSheetStatus.DIALOG_IS_OPENING || dialogViewState === BottomSheetStatus.DIALOG_IS_OPEN; }, [dialogViewState]); var bottomSheetOffsetY = useMemo(function () { if (dialogViewState === BottomSheetStatus.DIALOG_INIT || dialogViewState === BottomSheetStatus.DIALOG_STARTED_TO_CLOSE || dialogViewState === BottomSheetStatus.DIALOG_IS_CLOSING) { return 0; } var isStartedFromTop = touchState.startScrollTop === 0; if (!isStartedFromTop) { return 0; } var touchOffsetY = touchState.startY - touchY.curr; if (touchOffsetY < 0 && (touchState.noScroll || touchState.isTop)) { return touchOffsetY; } return 0; }, [dialogViewState, touchState.startScrollTop, touchState.startY, touchState.noScroll, touchState.isTop, touchY.curr]); var handleStartClosing = function handleStartClosing() { setDialogViewState(BottomSheetStatus.DIALOG_STARTED_TO_CLOSE); }; var handleTouchStart = useCallback(function (event) { if (horizontalScrollElRef && horizontalScrollElRef.current) { if (horizontalScrollElRef.current.contains(event.target)) { setHorizontalScrollElTouch({ startX: event.touches[0].clientX, startY: event.touches[0].clientY, isCalculated: false, preventScroll: false }); event.stopPropagation(); return; } } if (!contentRef.current) { if (event.cancelable) { event.preventDefault(); return; } } event.stopPropagation(); // @ts-ignore var maxScrollHeight = contentRef.current.scrollHeight - contentRef.current.clientHeight; setIsTouchMoveHandled(false); setTouchState(_extends({}, touchState, { startY: event.touches[0].clientY, touchStartY: event.touches[0].clientY, noScroll: maxScrollHeight === 0, // @ts-ignore isTop: contentRef.current.scrollTop === 0, // @ts-ignore startScrollTop: contentRef.current.scrollTop })); }, [horizontalScrollElRef, touchState]); var handleTouchMove = useCallback(function (event) { if (horizontalScrollElRef && horizontalScrollElRef.current) { if (horizontalScrollElRef.current.contains(event.target)) { if (!horizontalScrollElTouch.isCalculated) { var clientX = event.touches[0].clientX; if (Math.abs(horizontalScrollElTouch.startX - clientX) < 5) { setHorizontalScrollElTouch({ startX: 0, startY: 0, isCalculated: true, preventScroll: true }); event.preventDefault(); } else { var isLeft = horizontalScrollElRef.current.scrollLeft === 0; var isRight = horizontalScrollElRef.current.scrollLeft === horizontalScrollElRef.current.scrollWidth - horizontalScrollElRef.current.clientWidth; if (isLeft && horizontalScrollElTouch.startX - clientX < 0 || isRight && horizontalScrollElTouch.startX - clientX > 0) { setHorizontalScrollElTouch({ startX: 0, startY: 0, isCalculated: true, preventScroll: true }); } else { setHorizontalScrollElTouch({ startX: 0, startY: 0, isCalculated: true, preventScroll: false }); } } } else if (horizontalScrollElTouch.preventScroll) { event.preventDefault(); } else { event.stopPropagation(); } return; } } if (!contentRef.current) { if (event.cancelable) { event.preventDefault(); return; } } var clientY = event.touches[0].clientY; var touchOffsetY = touchState.startY - clientY; // @ts-ignore var isTop = contentRef.current.scrollTop === 0; var isBottom = // @ts-ignore contentRef.current.scrollTop === contentRef.current.scrollHeight - contentRef.current.clientHeight; var isMoving = isMovingContent; if (!isTouchMoveHandled) { if (isTop && touchOffsetY < 0) { setIsMovingContent(true); isMoving = true; } setIsTouchMoveHandled(true); } setTouchY({ curr: clientY, prev: touchY.curr }); if (touchState.noScroll || isMoving || touchOffsetY < 0 && isTop || isBottom && touchOffsetY > 0) { if (event.cancelable) { event.preventDefault(); } } }, [horizontalScrollElRef, horizontalScrollElTouch.isCalculated, horizontalScrollElTouch.preventScroll, horizontalScrollElTouch.startX, isMovingContent, isTouchMoveHandled, touchState.noScroll, touchState.startY, touchY.curr]); var handleTouchEnd = useCallback(function (event) { if (horizontalScrollElRef && horizontalScrollElRef.current) { if (horizontalScrollElRef.current.contains(event.target)) { event.stopPropagation(); return; } } if (!contentRef.current) { if (event.cancelable) { event.preventDefault(); return; } } setIsMovingContent(false); setTouchState(_extends({}, touchState, { // @ts-ignore isTop: contentRef.current.scrollTop === 0 })); setTouchY({ curr: 0, prev: 0 }); if (touchState.touchStartY !== 0) { var touchOffset = touchState.touchStartY - event.changedTouches[0].clientY; if (touchState.isTop && touchOffset < 0) { // @ts-ignore var clientHeight = contentRef.current.clientHeight; if (clientHeight > 0 && -touchOffset > clientHeight * CLOSE_DIALOG_PERCENT) { handleStartClosing(); } } } }, [horizontalScrollElRef, touchState]); var handleOnScroll = function handleOnScroll(event) { var target = event.target; setScrollPercent(target.scrollTop / (target.scrollHeight - target.clientHeight)); }; useEffect(function () { if (open && dialogViewState === BottomSheetStatus.DIALOG_INIT) { setDialogViewState(BottomSheetStatus.DIALOG_STARTED_TO_OPEN); } if (dialogViewState === BottomSheetStatus.DIALOG_STARTED_TO_OPEN) { setDialogViewState(BottomSheetStatus.DIALOG_IS_OPENING); } if (dialogViewState === BottomSheetStatus.DIALOG_STARTED_TO_CLOSE) { setDialogViewState(BottomSheetStatus.DIALOG_IS_CLOSING); } if (dialogViewState === BottomSheetStatus.DIALOG_IS_CLOSED) { clearStates(); setDialogViewState(BottomSheetStatus.DIALOG_INIT); if (open) { setOpen(false); } } if (!open && dialogViewState === BottomSheetStatus.DIALOG_IS_OPEN) { handleStartClosing(); } }, [open, dialogViewState, setOpen]); var handleTransitionEnd = useCallback(function () { if (dialogViewState === BottomSheetStatus.DIALOG_IS_OPENING) { setDialogViewState(BottomSheetStatus.DIALOG_IS_OPEN); } else if (dialogViewState === BottomSheetStatus.DIALOG_IS_CLOSING) { setDialogViewState(BottomSheetStatus.DIALOG_IS_CLOSED); } }, [dialogViewState]); useEffect(function () { if (dialogViewState === BottomSheetStatus.DIALOG_IS_CLOSED) { if (innerRef && innerRef.current) { // @ts-ignore innerRef.current.removeEventListener('touchstart', handleTouchStart); // @ts-ignore innerRef.current.removeEventListener('touchmove', handleTouchMove); // @ts-ignore innerRef.current.removeEventListener('touchend', handleTouchEnd); } } if (innerRef && innerRef.current) { // @ts-ignore innerRef.current.addEventListener('touchstart', handleTouchStart, { passive: false }); // @ts-ignore innerRef.current.addEventListener('touchmove', handleTouchMove, { passive: false }); // @ts-ignore innerRef.current.addEventListener('touchend', handleTouchEnd, { passive: false }); } return function () { if (innerRef && innerRef.current) { // @ts-ignore innerRef.current.removeEventListener('touchstart', handleTouchStart); // @ts-ignore innerRef.current.removeEventListener('touchmove', handleTouchMove); // @ts-ignore innerRef.current.removeEventListener('touchend', handleTouchEnd); } }; }, [dialogViewState, handleTouchStart, handleTouchMove, handleTouchEnd]); useEffect(function () { if (dialogViewState === BottomSheetStatus.DIALOG_STARTED_TO_OPEN) { if (contentRef && contentRef.current && children) { if (contentRef.current.scrollHeight !== contentRef.current.clientHeight) { setScrollPercent(0); contentRef.current.addEventListener('scroll', handleOnScroll, { passive: true }); } else { setScrollPercent(1); } } } else if (dialogViewState === BottomSheetStatus.DIALOG_IS_CLOSED) { if (contentRef && contentRef.current) { contentRef.current.removeEventListener('scroll', handleOnScroll); } } }, [children, dialogViewState]); useEffect(function () { return function () { if (contentRef && contentRef.current) { contentRef.current.removeEventListener('scroll', handleOnScroll); } }; }, []); useEffect(function () { var handleTouchStart = function handleTouchStart(e) { if (dialogViewState === BottomSheetStatus.DIALOG_IS_OPEN || dialogViewState === BottomSheetStatus.DIALOG_IS_OPENING) { if (e.cancelable) { e.preventDefault(); } handleStartClosing(); } }; if (maskRef && maskRef.current) { maskRef.current.addEventListener('touchstart', handleTouchStart, { passive: false }); } return function () { if (maskRef && maskRef.current) { maskRef.current.removeEventListener('touchstart', handleTouchStart); } }; }, [dialogViewState]); var bottomShadow = useMemo(function () { if (scrollPercent > 0.99) { return 'rgba(0, 0, 0, 0.05) 0px 8px 8px -4px inset, rgba(0, 0, 0, 0) 0px 0px 0px 0px inset, rgba(0, 0, 0, 0) 0px 0px 0px 0px inset, rgba(0, 0, 0, 0) 0px 0px 0px 0px inset'; } if (scrollPercent > 0.01) { return 'rgba(0, 0, 0, 0.05) 0px 8px 8px -4px inset, rgba(0, 0, 0, 0.05) 0px -8px 8px -4px inset, rgba(0, 0, 0, 0) 0px 0px 0px 0px inset, rgba(0, 0, 0, 0) 0px 0px 0px 0px inset'; } return 'rgba(0, 0, 0, 0) 0px 0px 0px 0px inset, rgba(0, 0, 0, 0.05) 0px -8px 8px -4px inset, rgba(0, 0, 0, 0) 0px 0px 0px 0px inset, rgba(0, 0, 0, 0) 0px 0px 0px 0px inset'; }, [scrollPercent]); if (dialogViewState === BottomSheetStatus.DIALOG_INIT) { return React.createElement(React.Fragment, null); } return React.createElement(Portal, { id: "BottomSheetComponent-" + bottomSheetId }, React.createElement("div", { className: cx(classes.mask, isShown && classes.maskIsOpen), ref: maskRef }), React.createElement("div", { className: cx(classes.root, isShown && classes.rootIsOpen), onTransitionEnd: handleTransitionEnd }, React.createElement("div", { className: classes.contentWrap, style: _extends({}, !isShown && { transform: 'translateY(100%)' }, bottomSheetOffsetY !== 0 && { transition: 'none 0s ease 0s', transform: "translateY(" + -bottomSheetOffsetY + "px" }) }, React.createElement("div", { className: classes.inner, ref: innerRef }, React.createElement("div", { className: classes.content, style: { maxHeight: windowSize.height * 0.9 } }, React.createElement("div", { className: classes.header }, header), React.createElement("div", { className: classes.scrollDiv, ref: contentRef }, React.createElement("div", { className: classes.shadowBox, style: _extends({ boxShadow: bottomShadow }, contentRef.current && { height: contentRef.current.offsetHeight }) }), children), React.createElement("div", { className: classes.footer }, footer)))))); }; export { BottomSheetDialog }; //# sourceMappingURL=react-bottom-sheet.esm.js.map