闭社主体 forked from https://github.com/tootsuite/mastodon
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.2 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. 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/tootsuite/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. }