@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
JavaScript
;
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