UNPKG

@helpscout/hsds-react

Version:

React component library for Help Scout's Design System

314 lines (252 loc) 10.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports.isNodeVisible = isNodeVisible; exports.default = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose")); var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized")); var _inheritsLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/inheritsLoose")); var _react = _interopRequireDefault(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _reactDom = _interopRequireDefault(require("react-dom")); var _getValidProps = _interopRequireDefault(require("@helpscout/react-utils/dist/getValidProps")); var _EventListener = _interopRequireDefault(require("../EventListener")); var _classnames = _interopRequireDefault(require("classnames")); var _LoadingDots = _interopRequireDefault(require("../LoadingDots")); var _node = require("../../utilities/node"); var _other = require("../../utilities/other"); var _jsxRuntime = require("react/jsx-runtime"); function noop() {} var InfiniteScroller = /*#__PURE__*/function (_React$PureComponent) { (0, _inheritsLoose2.default)(InfiniteScroller, _React$PureComponent); function InfiniteScroller(props) { var _this; _this = _React$PureComponent.call(this, props) || this; _this._isMounted = void 0; _this.node = void 0; _this.getScrollNodeTop = function () { var nodeScope = _this.state.nodeScope; if (nodeScope === window) { return window.scrollY; } else { return nodeScope.scrollTop; } }; _this.setNodeRef = function (node) { return _this.node = node; }; _this.state = { isLoading: props.isLoading, nodeScope: window }; _this._isMounted = null; _this.node = null; _this.handleOnScroll = _this.handleOnScroll.bind((0, _assertThisInitialized2.default)(_this)); return _this; } var _proto = InfiniteScroller.prototype; _proto.componentDidMount = function componentDidMount() { this._isMounted = true; this.setParentNode(); }; _proto.componentWillUnmount = function componentWillUnmount() { this._isMounted = false; }; _proto.UNSAFE_componentWillReceiveProps = function UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.isLoading && !this.state.isLoading) { this.handleOnLoading(); } if (!nextProps.isLoading && this.state.isLoading) { this.handleOnLoaded(); } }; // This method is tested, but tested in separate parts. // Unable to validate isNodeVisible in this method. The isNodeVisible // has been thoroughly tested elsewhere. // The handleOnLoading method has been abstracted to be tested in // isolation. _proto.handleOnScroll = function handleOnScroll(event) { var _this$props = this.props, offset = _this$props.offset, onScroll = _this$props.onScroll; var _this$state = this.state, isLoading = _this$state.isLoading, nodeScope = _this$state.nodeScope; var isVisible = isNodeVisible({ node: this.node, scope: nodeScope, offset: offset, complete: true }); onScroll(event, { node: this.node, offset: offset, scrollNode: nodeScope, scrollTop: this.getScrollNodeTop(), isVisible: isVisible, isLoading: isLoading }); if (isLoading || !isVisible) return; this.handleOnLoading(); }; _proto.handleOnLoading = function handleOnLoading() { var _this2 = this; var onLoading = this.props.onLoading; if (this._isMounted) { this.setState({ isLoading: true }); } onLoading(function () { _this2.handleOnLoaded(); }); }; _proto.handleOnLoaded = function handleOnLoaded() { var onLoaded = this.props.onLoaded; // Prevents scrollable area for unexpectedly scrolling after // new items are injected. this.normalizeNodeScrollScroll(this.getNodeScrollTop()); // Once the scroll position as been re-adjusted, then load new items onLoaded(); if (this._isMounted && this.state.isLoading) { this.setState({ isLoading: false }); } }; _proto.getNodeScrollTop = function getNodeScrollTop() { var nodeScope = this.state.nodeScope; if (nodeScope !== window && nodeScope.scrollTop !== undefined) { return nodeScope.scrollTop; } else { return nodeScope.scrollY; } }; _proto.normalizeNodeScrollScroll = function normalizeNodeScrollScroll(scrollTop) { var nodeScope = this.state.nodeScope; if (typeof scrollTop !== 'number') return; if (nodeScope === window && nodeScope.scrollTo) { nodeScope.scrollTo(window.scrollX, scrollTop); } else if (nodeScope.scrollTop !== undefined) { nodeScope.scrollTop = scrollTop; } }; _proto.setParentNode = function setParentNode() { var _this$props2 = this.props, getScrollParent = _this$props2.getScrollParent, scrollParent = _this$props2.scrollParent; var nodeScope = getScrollParent({ node: this.node }); nodeScope = (0, _node.isNodeElement)(nodeScope) || nodeScope === window ? nodeScope : null; if (!nodeScope && scrollParent) { nodeScope = (0, _node.isNodeElement)(scrollParent) ? scrollParent : null; } if (!nodeScope) { var node = _reactDom.default.findDOMNode(this); // Tested for node.parentNode, but not for window. // This is a super fail-safe. This will always be parentNode, with the // exception of document or window. Cannot be tested in JSDOM/Enzyme, // since it prohibits mounting on document.body directly. nodeScope = node && node.parentNode ? node.parentNode : window; } this.setState({ nodeScope: nodeScope }); }; _proto.render = function render() { var _this$props3 = this.props, className = _this$props3.className, children = _this$props3.children, getScrollParent = _this$props3.getScrollParent, loading = _this$props3.loading, propsIsLoading = _this$props3.isLoading, onLoading = _this$props3.onLoading, onLoaded = _this$props3.onLoaded, scrollParent = _this$props3.scrollParent, rest = (0, _objectWithoutPropertiesLoose2.default)(_this$props3, ["className", "children", "getScrollParent", "loading", "isLoading", "onLoading", "onLoaded", "scrollParent"]); var _this$state2 = this.state, isLoading = _this$state2.isLoading, nodeScope = _this$state2.nodeScope; var handleOnScroll = this.handleOnScroll; var componentClassName = (0, _classnames.default)('c-InfiniteScroller', isLoading && 'is-loading', className); var loadingMarkup = loading || /*#__PURE__*/(0, _jsxRuntime.jsx)(_LoadingDots.default, { align: "center" }); var contentMarkup = isLoading ? loadingMarkup : children; return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", (0, _extends2.default)({}, (0, _getValidProps.default)(rest), { className: componentClassName, ref: this.setNodeRef, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_EventListener.default, { event: "scroll", handler: handleOnScroll, scope: nodeScope }), contentMarkup] })); }; return InfiniteScroller; }(_react.default.PureComponent); InfiniteScroller.defaultProps = { 'data-cy': 'InfiniteScroller', getScrollParent: noop, offset: 0, isLoading: false, loading: false, onLoading: noop, onLoaded: noop, onScroll: noop }; InfiniteScroller.propTypes = { /** Custom class names to be added to the component. */ className: _propTypes.default.string, /** Callback to retrieve the parentNode to listen for scroll events. */ getScrollParent: _propTypes.default.func, /** Sets the component into an `isLoading` state. */ isLoading: _propTypes.default.bool, /** Top buffer (`px`) before infinite scroll is triggered. */ offset: _propTypes.default.number, /** Callback when component completes `onLoading`. */ onLoaded: _propTypes.default.func, /** Callback when component becomes visible in the DOM, after scrolling. */ onLoading: _propTypes.default.func, /** DOM node to listen to scroll events on, instead of closest parentNode (default). */ scrollParent: _propTypes.default.any, onScroll: _propTypes.default.func, /** What to render while loading */ loading: _propTypes.default.any, /** Data attr for Cypress tests. */ 'data-cy': _propTypes.default.string }; /** * Checks if node is visible with the view (a node Element or window). This is typically used for scroll interactions. * Note: This function currently only measures vertical scroll-based * calculations. * * @param options object Config object * @option node Element DOM node to check visibility for * @option scope Element DOM node to check visibility within * @option offset number Top buffer amount for visiblity check * @option complete bool node must be in complete view, if true * @return bool True/False if node is in view */ function isNodeVisible(options) { if (!options || typeof options !== 'object') return false; var node = options.node, scope = options.scope, offset = options.offset, complete = options.complete; if (!(0, _node.isNodeElement)(node)) return false; var nodeOffset = offset !== undefined ? offset : 0; nodeOffset = typeof nodeOffset !== 'number' ? 0 : nodeOffset < 0 ? 0 : nodeOffset; var nodeScope = (0, _node.getNodeScope)(scope || window); var isWindow = nodeScope === window; var bufferOffset = 4; // To account for potential borders on the nodeScope var rect = node.getBoundingClientRect(); var offsetTop = (0, _other.isNodeEnv)() ? rect.top : node.offsetTop; var viewportHeight = isWindow ? window.innerHeight : nodeScope.getBoundingClientRect().height; var viewportTop = isWindow ? window.scrollY : nodeScope.scrollTop; var viewportBottom = isWindow ? window.innerHeight : viewportTop + viewportHeight + bufferOffset; var bottom = offsetTop + rect.height; var top = complete && nodeOffset === 0 ? bottom : bottom - nodeOffset; return parseInt(top, 10) <= parseInt(viewportBottom, 10) && parseInt(bottom, 10) >= parseInt(viewportTop, 10); } var _default = InfiniteScroller; exports.default = _default;