UNPKG

@rakhimgaliyev/react-bottom-sheet

Version:

Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it.

715 lines (616 loc) 22.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var React = require('react'); var React__default = _interopDefault(React); var reactJss = require('react-jss'); var cx = _interopDefault(require('classnames')); var ReactDOM = _interopDefault(require('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 = React.useRef(null); React.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__default.createElement(React__default.Fragment, null); }; var useWindowSize = function useWindowSize() { var getSize = function getSize() { return { width: window.innerWidth, height: window.innerHeight }; }; var _useState = React.useState(getSize), windowSize = _useState[0], setWindowSize = _useState[1]; React.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__*/reactJss.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 = React.useRef(null); var contentRef = React.useRef(null); var innerRef = React.useRef(null); var windowSize = useWindowSize(); var _useState = React.useState(uniqueId()), bottomSheetId = _useState[0]; var _useState2 = React.useState(BottomSheetStatus.DIALOG_INIT), dialogViewState = _useState2[0], setDialogViewState = _useState2[1]; var _useState3 = React.useState(false), isMovingContent = _useState3[0], setIsMovingContent = _useState3[1]; var _useState4 = React.useState(false), isTouchMoveHandled = _useState4[0], setIsTouchMoveHandled = _useState4[1]; var _useState5 = React.useState(touchInitState), touchState = _useState5[0], setTouchState = _useState5[1]; var _useState6 = React.useState({ curr: 0, prev: 0 }), touchY = _useState6[0], setTouchY = _useState6[1]; var _useState7 = React.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 = React.useState({ startX: 0, startY: 0, isCalculated: false, preventScroll: false }), horizontalScrollElTouch = _useState8[0], setHorizontalScrollElTouch = _useState8[1]; var isShown = React.useMemo(function () { return dialogViewState === BottomSheetStatus.DIALOG_IS_OPENING || dialogViewState === BottomSheetStatus.DIALOG_IS_OPEN; }, [dialogViewState]); var bottomSheetOffsetY = React.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 = React.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 = React.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 = React.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)); }; React.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 = React.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]); React.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]); React.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]); React.useEffect(function () { return function () { if (contentRef && contentRef.current) { contentRef.current.removeEventListener('scroll', handleOnScroll); } }; }, []); React.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 = React.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__default.createElement(React__default.Fragment, null); } return React__default.createElement(Portal, { id: "BottomSheetComponent-" + bottomSheetId }, React__default.createElement("div", { className: cx(classes.mask, isShown && classes.maskIsOpen), ref: maskRef }), React__default.createElement("div", { className: cx(classes.root, isShown && classes.rootIsOpen), onTransitionEnd: handleTransitionEnd }, React__default.createElement("div", { className: classes.contentWrap, style: _extends({}, !isShown && { transform: 'translateY(100%)' }, bottomSheetOffsetY !== 0 && { transition: 'none 0s ease 0s', transform: "translateY(" + -bottomSheetOffsetY + "px" }) }, React__default.createElement("div", { className: classes.inner, ref: innerRef }, React__default.createElement("div", { className: classes.content, style: { maxHeight: windowSize.height * 0.9 } }, React__default.createElement("div", { className: classes.header }, header), React__default.createElement("div", { className: classes.scrollDiv, ref: contentRef }, React__default.createElement("div", { className: classes.shadowBox, style: _extends({ boxShadow: bottomShadow }, contentRef.current && { height: contentRef.current.offsetHeight }) }), children), React__default.createElement("div", { className: classes.footer }, footer)))))); }; exports.BottomSheetDialog = BottomSheetDialog; //# sourceMappingURL=react-bottom-sheet.cjs.development.js.map