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';
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'];
12 export default class IntersectionObserverArticle
extends React
.Component
{
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
,
26 isHidden: false, // set to true in requestIdleCallback to trigger un-render
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
36 // Otherwise, diff based on props
37 const propsToDiff
= isUnrendered
? updateOnPropsForUnrendered : updateOnPropsForRendered
;
38 return !propsToDiff
.every(prop
=> is(nextProps
[prop
], this.props
[prop
]));
41 componentDidMount () {
42 const { intersectionObserverWrapper
, id
} = this.props
;
44 intersectionObserverWrapper
.observe(
47 this.handleIntersection
50 this.componentMounted
= true;
53 componentWillUnmount () {
54 const { intersectionObserverWrapper
, id
} = this.props
;
55 intersectionObserverWrapper
.unobserve(id
, this.node
);
57 this.componentMounted
= false;
60 handleIntersection
= (entry
) => {
63 scheduleIdleTask(this.calculateHeight
);
64 this.setState(this.updateStateAfterIntersection
);
67 updateStateAfterIntersection
= (prevState
) => {
68 if (prevState
.isIntersecting
!== false && !this.entry
.isIntersecting
) {
69 scheduleIdleTask(this.hideIfNotIntersecting
);
72 isIntersecting: this.entry
.isIntersecting
,
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
;
83 if (onHeightChange
&& saveHeightKey
) {
84 onHeightChange(saveHeightKey
, id
, this.height
);
88 hideIfNotIntersecting
= () => {
89 if (!this.componentMounted
) {
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
}));
100 handleRef
= (node
) => {
105 const { children
, id
, index
, listLength
, cachedHeight
} = this.props
;
106 const { isIntersecting
, isHidden
} = this.state
;
108 if (!isIntersecting
&& (isHidden
|| cachedHeight
)) {
112 aria
-posinset
={index
+ 1}
113 aria
-setsize
={listLength
}
114 style
={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
118 {children
&& React
.cloneElement(children
, { hidden: true })}
124 <article ref
={this.handleRef
} aria
-posinset
={index
+ 1} aria
-setsize
={listLength
} data
-id
={id
} tabIndex
='0'>
125 {children
&& React
.cloneElement(children
, { hidden: false })}