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.

128 lines
4.2 KiB

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