]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/components/intersection_observer_article.js
Fix IntersectionObserverArticle not hiding some out-of-view items (#9982)
[mastodon.git] / app / javascript / mastodon / components / intersection_observer_article.js
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
7 // Diff these props in the "rendered" state
8 const updateOnPropsForRendered = ['id', 'index', 'listLength'];
9 // Diff these props in the "unrendered" state
10 const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
11
12 export default class IntersectionObserverArticle extends React.Component {
13
14 static propTypes = {
15 intersectionObserverWrapper: PropTypes.object.isRequired,
16 id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
17 index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
18 listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
19 saveHeightKey: PropTypes.string,
20 cachedHeight: PropTypes.number,
21 onHeightChange: PropTypes.func,
22 children: PropTypes.node,
23 };
24
25 state = {
26 isHidden: false, // set to true in requestIdleCallback to trigger un-render
27 }
28
29 shouldComponentUpdate (nextProps, nextState) {
30 const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
31 const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
32 if (!!isUnrendered !== !!willBeUnrendered) {
33 // If we're going from rendered to unrendered (or vice versa) then update
34 return true;
35 }
36 // Otherwise, diff based on props
37 const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
38 return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
39 }
40
41 componentDidMount () {
42 const { intersectionObserverWrapper, id } = this.props;
43
44 intersectionObserverWrapper.observe(
45 id,
46 this.node,
47 this.handleIntersection
48 );
49
50 this.componentMounted = true;
51 }
52
53 componentWillUnmount () {
54 const { intersectionObserverWrapper, id } = this.props;
55 intersectionObserverWrapper.unobserve(id, this.node);
56
57 this.componentMounted = false;
58 }
59
60 handleIntersection = (entry) => {
61 this.entry = entry;
62
63 scheduleIdleTask(this.calculateHeight);
64 this.setState(this.updateStateAfterIntersection);
65 }
66
67 updateStateAfterIntersection = (prevState) => {
68 if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
69 scheduleIdleTask(this.hideIfNotIntersecting);
70 }
71 return {
72 isIntersecting: this.entry.isIntersecting,
73 isHidden: false,
74 };
75 }
76
77 calculateHeight = () => {
78 const { onHeightChange, saveHeightKey, id } = this.props;
79 // save the height of the fully-rendered element (this is expensive
80 // on Chrome, where we need to fall back to getBoundingClientRect)
81 this.height = getRectFromEntry(this.entry).height;
82
83 if (onHeightChange && saveHeightKey) {
84 onHeightChange(saveHeightKey, id, this.height);
85 }
86 }
87
88 hideIfNotIntersecting = () => {
89 if (!this.componentMounted) {
90 return;
91 }
92
93 // When the browser gets a chance, test if we're still not intersecting,
94 // and if so, set our isHidden to true to trigger an unrender. The point of
95 // this is to save DOM nodes and avoid using up too much memory.
96 // See: https://github.com/tootsuite/mastodon/issues/2900
97 this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
98 }
99
100 handleRef = (node) => {
101 this.node = node;
102 }
103
104 render () {
105 const { children, id, index, listLength, cachedHeight } = this.props;
106 const { isIntersecting, isHidden } = this.state;
107
108 if (!isIntersecting && (isHidden || cachedHeight)) {
109 return (
110 <article
111 ref={this.handleRef}
112 aria-posinset={index + 1}
113 aria-setsize={listLength}
114 style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
115 data-id={id}
116 tabIndex='0'
117 >
118 {children && React.cloneElement(children, { hidden: true })}
119 </article>
120 );
121 }
122
123 return (
124 <article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
125 {children && React.cloneElement(children, { hidden: false })}
126 </article>
127 );
128 }
129
130 }
This page took 0.100511 seconds and 4 git commands to generate.