You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

125 lines
4.0 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
  4. import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
  5. import { is } from 'immutable';
  6. // Diff these props in the "rendered" state
  7. const updateOnPropsForRendered = ['id', 'index', 'listLength'];
  8. // Diff these props in the "unrendered" state
  9. const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
  10. export default class IntersectionObserverArticle extends React.Component {
  11. static propTypes = {
  12. intersectionObserverWrapper: PropTypes.object.isRequired,
  13. id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  14. index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  15. listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  16. saveHeightKey: PropTypes.string,
  17. cachedHeight: PropTypes.number,
  18. onHeightChange: PropTypes.func,
  19. children: PropTypes.node,
  20. };
  21. state = {
  22. isHidden: false, // set to true in requestIdleCallback to trigger un-render
  23. }
  24. shouldComponentUpdate (nextProps, nextState) {
  25. const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
  26. const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
  27. if (!!isUnrendered !== !!willBeUnrendered) {
  28. // If we're going from rendered to unrendered (or vice versa) then update
  29. return true;
  30. }
  31. // Otherwise, diff based on props
  32. const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
  33. return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
  34. }
  35. componentDidMount () {
  36. const { intersectionObserverWrapper, id } = this.props;
  37. intersectionObserverWrapper.observe(
  38. id,
  39. this.node,
  40. this.handleIntersection
  41. );
  42. this.componentMounted = true;
  43. }
  44. componentWillUnmount () {
  45. const { intersectionObserverWrapper, id } = this.props;
  46. intersectionObserverWrapper.unobserve(id, this.node);
  47. this.componentMounted = false;
  48. }
  49. handleIntersection = (entry) => {
  50. const { onHeightChange, saveHeightKey, id } = this.props;
  51. if (this.node && this.node.children.length !== 0) {
  52. // save the height of the fully-rendered element
  53. this.height = getRectFromEntry(entry).height;
  54. if (onHeightChange && saveHeightKey) {
  55. onHeightChange(saveHeightKey, id, this.height);
  56. }
  57. }
  58. this.setState((prevState) => {
  59. if (prevState.isIntersecting && !entry.isIntersecting) {
  60. scheduleIdleTask(this.hideIfNotIntersecting);
  61. }
  62. return {
  63. isIntersecting: entry.isIntersecting,
  64. isHidden: false,
  65. };
  66. });
  67. }
  68. hideIfNotIntersecting = () => {
  69. if (!this.componentMounted) {
  70. return;
  71. }
  72. // When the browser gets a chance, test if we're still not intersecting,
  73. // and if so, set our isHidden to true to trigger an unrender. The point of
  74. // this is to save DOM nodes and avoid using up too much memory.
  75. // See: https://github.com/tootsuite/mastodon/issues/2900
  76. this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
  77. }
  78. handleRef = (node) => {
  79. this.node = node;
  80. }
  81. render () {
  82. const { children, id, index, listLength, cachedHeight } = this.props;
  83. const { isIntersecting, isHidden } = this.state;
  84. if (!isIntersecting && (isHidden || cachedHeight)) {
  85. return (
  86. <article
  87. ref={this.handleRef}
  88. aria-posinset={index}
  89. aria-setsize={listLength}
  90. style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
  91. data-id={id}
  92. tabIndex='0'
  93. >
  94. {children && React.cloneElement(children, { hidden: true })}
  95. </article>
  96. );
  97. }
  98. return (
  99. <article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'>
  100. {children && React.cloneElement(children, { hidden: false })}
  101. </article>
  102. );
  103. }
  104. }