@helpscout/hsds-react
Version:
React component library for Help Scout's Design System
314 lines (252 loc) • 10.6 kB
JavaScript
"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;