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.

130 lines
4.1 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. // Diff these props in the "unrendered" state
  6. const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
  7. export default class IntersectionObserverArticle extends React.Component {
  8. static propTypes = {
  9. intersectionObserverWrapper: PropTypes.object.isRequired,
  10. id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  11. index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  12. listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  13. saveHeightKey: PropTypes.string,
  14. cachedHeight: PropTypes.number,
  15. onHeightChange: PropTypes.func,
  16. children: PropTypes.node,
  17. };
  18. state = {
  19. isHidden: false, // set to true in requestIdleCallback to trigger un-render
  20. }
  21. shouldComponentUpdate (nextProps, nextState) {
  22. const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
  23. const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
  24. if (!!isUnrendered !== !!willBeUnrendered) {
  25. // If we're going from rendered to unrendered (or vice versa) then update
  26. return true;
  27. }
  28. // If we are and remain hidden, diff based on props
  29. if (isUnrendered) {
  30. return !updateOnPropsForUnrendered.every(prop => nextProps[prop] === this.props[prop]);
  31. }
  32. // Else, assume the children have changed
  33. return true;
  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. this.entry = entry;
  51. scheduleIdleTask(this.calculateHeight);
  52. this.setState(this.updateStateAfterIntersection);
  53. }
  54. updateStateAfterIntersection = (prevState) => {
  55. if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
  56. scheduleIdleTask(this.hideIfNotIntersecting);
  57. }
  58. return {
  59. isIntersecting: this.entry.isIntersecting,
  60. isHidden: false,
  61. };
  62. }
  63. calculateHeight = () => {
  64. const { onHeightChange, saveHeightKey, id } = this.props;
  65. // save the height of the fully-rendered element (this is expensive
  66. // on Chrome, where we need to fall back to getBoundingClientRect)
  67. this.height = getRectFromEntry(this.entry).height;
  68. if (onHeightChange && saveHeightKey) {
  69. onHeightChange(saveHeightKey, id, this.height);
  70. }
  71. }
  72. hideIfNotIntersecting = () => {
  73. if (!this.componentMounted) {
  74. return;
  75. }
  76. // When the browser gets a chance, test if we're still not intersecting,
  77. // and if so, set our isHidden to true to trigger an unrender. The point of
  78. // this is to save DOM nodes and avoid using up too much memory.
  79. // See: https://github.com/mastodon/mastodon/issues/2900
  80. this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
  81. }
  82. handleRef = (node) => {
  83. this.node = node;
  84. }
  85. render () {
  86. const { children, id, index, listLength, cachedHeight } = this.props;
  87. const { isIntersecting, isHidden } = this.state;
  88. if (!isIntersecting && (isHidden || cachedHeight)) {
  89. return (
  90. <article
  91. ref={this.handleRef}
  92. aria-posinset={index + 1}
  93. aria-setsize={listLength}
  94. style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
  95. data-id={id}
  96. tabIndex='0'
  97. >
  98. {children && React.cloneElement(children, { hidden: true })}
  99. </article>
  100. );
  101. }
  102. return (
  103. <article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
  104. {children && React.cloneElement(children, { hidden: false })}
  105. </article>
  106. );
  107. }
  108. }