DELETE INTERSECTION OBSERVER ARTICLE
This commit is contained in:
parent
da17214a0b
commit
ea34a7f303
9 changed files with 1 additions and 295 deletions
|
@ -1,17 +0,0 @@
|
||||||
export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET';
|
|
||||||
export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR';
|
|
||||||
|
|
||||||
export function setHeight(key, id, height) {
|
|
||||||
return {
|
|
||||||
type: HEIGHT_CACHE_SET,
|
|
||||||
key,
|
|
||||||
id,
|
|
||||||
height,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearHeight() {
|
|
||||||
return {
|
|
||||||
type: HEIGHT_CACHE_CLEAR,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
import { is } from 'immutable';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
|
|
||||||
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
|
||||||
|
|
||||||
// Diff these props in the "rendered" state
|
|
||||||
const updateOnPropsForRendered = ['id', 'index', 'listLength'];
|
|
||||||
// Diff these props in the "unrendered" state
|
|
||||||
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
|
|
||||||
|
|
||||||
export default class IntersectionObserverArticle extends React.Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
intersectionObserverWrapper: PropTypes.object.isRequired,
|
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
saveHeightKey: PropTypes.string,
|
|
||||||
cachedHeight: PropTypes.number,
|
|
||||||
onHeightChange: PropTypes.func,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
|
||||||
isIntersecting: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
|
|
||||||
const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
|
|
||||||
if (!!isUnrendered !== !!willBeUnrendered) {
|
|
||||||
// If we're going from rendered to unrendered (or vice versa) then update
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Otherwise, diff based on props
|
|
||||||
const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
|
|
||||||
return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { intersectionObserverWrapper, id } = this.props;
|
|
||||||
|
|
||||||
intersectionObserverWrapper.observe(
|
|
||||||
id,
|
|
||||||
this.node,
|
|
||||||
this.handleIntersection,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.componentMounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
const { intersectionObserverWrapper, id } = this.props;
|
|
||||||
intersectionObserverWrapper.unobserve(id, this.node);
|
|
||||||
|
|
||||||
this.componentMounted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleIntersection = (entry) => {
|
|
||||||
this.entry = entry;
|
|
||||||
|
|
||||||
scheduleIdleTask(this.calculateHeight);
|
|
||||||
this.setState(this.updateStateAfterIntersection);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStateAfterIntersection = (prevState) => {
|
|
||||||
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
|
|
||||||
scheduleIdleTask(this.hideIfNotIntersecting);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isIntersecting: this.entry.isIntersecting,
|
|
||||||
isHidden: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateHeight = () => {
|
|
||||||
const { onHeightChange, saveHeightKey, id } = this.props;
|
|
||||||
// save the height of the fully-rendered element (this is expensive
|
|
||||||
// on Chrome, where we need to fall back to getBoundingClientRect)
|
|
||||||
this.height = getRectFromEntry(this.entry).height;
|
|
||||||
|
|
||||||
if (onHeightChange && saveHeightKey) {
|
|
||||||
onHeightChange(saveHeightKey, id, this.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hideIfNotIntersecting = () => {
|
|
||||||
if (!this.componentMounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the browser gets a chance, test if we're still not intersecting,
|
|
||||||
// and if so, set our isHidden to true to trigger an unrender. The point of
|
|
||||||
// this is to save DOM nodes and avoid using up too much memory.
|
|
||||||
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRef = (node) => {
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { children, id, index, listLength, cachedHeight } = this.props;
|
|
||||||
const { isIntersecting, isHidden } = this.state;
|
|
||||||
|
|
||||||
if (!isIntersecting && (isHidden || cachedHeight)) {
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
ref={this.handleRef}
|
|
||||||
aria-posinset={index + 1}
|
|
||||||
aria-setsize={listLength}
|
|
||||||
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
|
|
||||||
data-id={id}
|
|
||||||
tabIndex='0'
|
|
||||||
>
|
|
||||||
{children && React.cloneElement(children, { hidden: true })}
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
|
|
||||||
{children && React.cloneElement(children, { hidden: false })}
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { setHeight } from '../actions/height_cache';
|
|
||||||
import IntersectionObserverArticle from '../components/intersection_observer_article';
|
|
||||||
|
|
||||||
const makeMapStateToProps = (state, props) => ({
|
|
||||||
cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
|
|
||||||
onHeightChange(key, id, height) {
|
|
||||||
dispatch(setHeight(key, id, height));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle);
|
|
|
@ -35,7 +35,6 @@ import { fetchFollowRequests } from '../../actions/accounts';
|
||||||
import { fetchReports, fetchUsers, fetchConfig } from '../../actions/admin';
|
import { fetchReports, fetchUsers, fetchConfig } from '../../actions/admin';
|
||||||
import { uploadCompose, resetCompose } from '../../actions/compose';
|
import { uploadCompose, resetCompose } from '../../actions/compose';
|
||||||
import { fetchFilters } from '../../actions/filters';
|
import { fetchFilters } from '../../actions/filters';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
|
||||||
import { openModal } from '../../actions/modals';
|
import { openModal } from '../../actions/modals';
|
||||||
import { expandNotifications } from '../../actions/notifications';
|
import { expandNotifications } from '../../actions/notifications';
|
||||||
import { fetchScheduledStatuses } from '../../actions/scheduled_statuses';
|
import { fetchScheduledStatuses } from '../../actions/scheduled_statuses';
|
||||||
|
@ -187,7 +186,6 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
onLayoutChange: PropTypes.func.isRequired,
|
|
||||||
soapbox: ImmutablePropTypes.record.isRequired,
|
soapbox: ImmutablePropTypes.record.isRequired,
|
||||||
features: PropTypes.object.isRequired,
|
features: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -205,9 +203,6 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResize = debounce(() => {
|
handleResize = debounce(() => {
|
||||||
// The cached heights are no longer accurate, invalidate
|
|
||||||
this.props.onLayoutChange();
|
|
||||||
|
|
||||||
this.setState({ mobile: isMobile(window.innerWidth) });
|
this.setState({ mobile: isMobile(window.innerWidth) });
|
||||||
}, 500, {
|
}, 500, {
|
||||||
trailing: true,
|
trailing: true,
|
||||||
|
@ -408,11 +403,6 @@ class UI extends React.PureComponent {
|
||||||
mobile: isMobile(window.innerWidth),
|
mobile: isMobile(window.innerWidth),
|
||||||
};
|
};
|
||||||
|
|
||||||
handleLayoutChange = () => {
|
|
||||||
// The cached heights are no longer accurate, invalidate
|
|
||||||
this.props.dispatch(clearHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDragEnter = (e) => {
|
handleDragEnter = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -756,7 +746,7 @@ class UI extends React.PureComponent {
|
||||||
<div className='z-10 flex flex-col'>
|
<div className='z-10 flex flex-col'>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange} soapbox={soapbox} features={features}>
|
<SwitchingColumnsArea location={location} soapbox={soapbox} features={features}>
|
||||||
{children}
|
{children}
|
||||||
</SwitchingColumnsArea>
|
</SwitchingColumnsArea>
|
||||||
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
|
|
||||||
// Get the bounding client rect from an IntersectionObserver entry.
|
|
||||||
// This is to work around a bug in Chrome: https://crbug.com/737228
|
|
||||||
|
|
||||||
let hasBoundingRectBug;
|
|
||||||
|
|
||||||
function getRectFromEntry(entry) {
|
|
||||||
if (typeof hasBoundingRectBug !== 'boolean') {
|
|
||||||
const boundingRect = entry.target.getBoundingClientRect();
|
|
||||||
const observerRect = entry.boundingClientRect;
|
|
||||||
hasBoundingRectBug = boundingRect.height !== observerRect.height ||
|
|
||||||
boundingRect.top !== observerRect.top ||
|
|
||||||
boundingRect.width !== observerRect.width ||
|
|
||||||
boundingRect.bottom !== observerRect.bottom ||
|
|
||||||
boundingRect.left !== observerRect.left ||
|
|
||||||
boundingRect.right !== observerRect.right;
|
|
||||||
}
|
|
||||||
return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getRectFromEntry;
|
|
|
@ -1,57 +0,0 @@
|
||||||
// Wrapper for IntersectionObserver in order to make working with it
|
|
||||||
// a bit easier. We also follow this performance advice:
|
|
||||||
// "If you need to observe multiple elements, it is both possible and
|
|
||||||
// advised to observe multiple elements using the same IntersectionObserver
|
|
||||||
// instance by calling observe() multiple times."
|
|
||||||
// https://developers.google.com/web/updates/2016/04/intersectionobserver
|
|
||||||
|
|
||||||
class IntersectionObserverWrapper {
|
|
||||||
|
|
||||||
callbacks = {};
|
|
||||||
observerBacklog = [];
|
|
||||||
observer = null;
|
|
||||||
|
|
||||||
connect(options) {
|
|
||||||
const onIntersection = (entries) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
const id = entry.target.getAttribute('data-id');
|
|
||||||
if (this.callbacks[id]) {
|
|
||||||
this.callbacks[id](entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.observer = new IntersectionObserver(onIntersection, options);
|
|
||||||
this.observerBacklog.forEach(([ id, node, callback ]) => {
|
|
||||||
this.observe(id, node, callback);
|
|
||||||
});
|
|
||||||
this.observerBacklog = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
observe(id, node, callback) {
|
|
||||||
if (!this.observer) {
|
|
||||||
this.observerBacklog.push([ id, node, callback ]);
|
|
||||||
} else {
|
|
||||||
this.callbacks[id] = callback;
|
|
||||||
this.observer.observe(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unobserve(id, node) {
|
|
||||||
if (this.observer) {
|
|
||||||
delete this.callbacks[id];
|
|
||||||
this.observer.unobserve(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
if (this.observer) {
|
|
||||||
this.callbacks = {};
|
|
||||||
this.observer.disconnect();
|
|
||||||
this.observer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IntersectionObserverWrapper;
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
import reducer from '../height_cache';
|
|
||||||
import { HEIGHT_CACHE_CLEAR } from '../height_cache';
|
|
||||||
|
|
||||||
describe('height_cache reducer', () => {
|
|
||||||
it('should return the initial state', () => {
|
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableMap());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle HEIGHT_CACHE_CLEAR', () => {
|
|
||||||
expect(reducer(undefined, { type: HEIGHT_CACHE_CLEAR })).toEqual(ImmutableMap());
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from '../actions/height_cache';
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
const setHeight = (state, key, id, height) => {
|
|
||||||
return state.update(key, ImmutableMap(), map => map.set(id, height));
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearHeights = () => {
|
|
||||||
return ImmutableMap();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function statuses(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case HEIGHT_CACHE_SET:
|
|
||||||
return setHeight(state, action.key, action.id, action.height);
|
|
||||||
case HEIGHT_CACHE_CLEAR:
|
|
||||||
return clearHeights();
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -28,7 +28,6 @@ import group_editor from './group_editor';
|
||||||
import group_lists from './group_lists';
|
import group_lists from './group_lists';
|
||||||
import group_relationships from './group_relationships';
|
import group_relationships from './group_relationships';
|
||||||
import groups from './groups';
|
import groups from './groups';
|
||||||
import height_cache from './height_cache';
|
|
||||||
import identity_proofs from './identity_proofs';
|
import identity_proofs from './identity_proofs';
|
||||||
import instance from './instance';
|
import instance from './instance';
|
||||||
import listAdder from './list_adder';
|
import listAdder from './list_adder';
|
||||||
|
@ -83,7 +82,6 @@ const reducers = {
|
||||||
compose,
|
compose,
|
||||||
search,
|
search,
|
||||||
notifications,
|
notifications,
|
||||||
height_cache,
|
|
||||||
custom_emojis,
|
custom_emojis,
|
||||||
identity_proofs,
|
identity_proofs,
|
||||||
lists,
|
lists,
|
||||||
|
|
Loading…
Reference in a new issue