]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/components/status_list.js
Improve accessibility (part 4) (#4408)
[mastodon.git] / app / javascript / mastodon / components / status_list.js
1 import React from 'react';
2 import ImmutablePropTypes from 'react-immutable-proptypes';
3 import { ScrollContainer } from 'react-router-scroll';
4 import PropTypes from 'prop-types';
5 import StatusContainer from '../containers/status_container';
6 import LoadMore from './load_more';
7 import ImmutablePureComponent from 'react-immutable-pure-component';
8 import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
9 import { throttle } from 'lodash';
10
11 export default class StatusList extends ImmutablePureComponent {
12
13 static propTypes = {
14 scrollKey: PropTypes.string.isRequired,
15 statusIds: ImmutablePropTypes.list.isRequired,
16 onScrollToBottom: PropTypes.func,
17 onScrollToTop: PropTypes.func,
18 onScroll: PropTypes.func,
19 trackScroll: PropTypes.bool,
20 shouldUpdateScroll: PropTypes.func,
21 isLoading: PropTypes.bool,
22 hasMore: PropTypes.bool,
23 prepend: PropTypes.node,
24 emptyMessage: PropTypes.node,
25 };
26
27 static defaultProps = {
28 trackScroll: true,
29 };
30
31 intersectionObserverWrapper = new IntersectionObserverWrapper();
32
33 handleScroll = throttle(() => {
34 if (this.node) {
35 const { scrollTop, scrollHeight, clientHeight } = this.node;
36 const offset = scrollHeight - scrollTop - clientHeight;
37 this._oldScrollPosition = scrollHeight - scrollTop;
38
39 if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
40 this.props.onScrollToBottom();
41 } else if (scrollTop < 100 && this.props.onScrollToTop) {
42 this.props.onScrollToTop();
43 } else if (this.props.onScroll) {
44 this.props.onScroll();
45 }
46 }
47 }, 150, {
48 trailing: true,
49 });
50
51 componentDidMount () {
52 this.attachScrollListener();
53 this.attachIntersectionObserver();
54
55 // Handle initial scroll posiiton
56 this.handleScroll();
57 }
58
59 componentDidUpdate (prevProps) {
60 // Reset the scroll position when a new toot comes in in order not to
61 // jerk the scrollbar around if you're already scrolled down the page.
62 if (prevProps.statusIds.size < this.props.statusIds.size && this._oldScrollPosition && this.node.scrollTop > 0) {
63 if (prevProps.statusIds.first() !== this.props.statusIds.first()) {
64 let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
65 if (this.node.scrollTop !== newScrollTop) {
66 this.node.scrollTop = newScrollTop;
67 }
68 } else {
69 this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
70 }
71 }
72 }
73
74 componentWillUnmount () {
75 this.detachScrollListener();
76 this.detachIntersectionObserver();
77 }
78
79 attachIntersectionObserver () {
80 this.intersectionObserverWrapper.connect({
81 root: this.node,
82 rootMargin: '300% 0px',
83 });
84 }
85
86 detachIntersectionObserver () {
87 this.intersectionObserverWrapper.disconnect();
88 }
89
90 attachScrollListener () {
91 this.node.addEventListener('scroll', this.handleScroll);
92 }
93
94 detachScrollListener () {
95 this.node.removeEventListener('scroll', this.handleScroll);
96 }
97
98 setRef = (c) => {
99 this.node = c;
100 }
101
102 handleLoadMore = (e) => {
103 e.preventDefault();
104 this.props.onScrollToBottom();
105 }
106
107 render () {
108 const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
109
110 const loadMore = <LoadMore visible={!isLoading && statusIds.size > 0 && hasMore} onClick={this.handleLoadMore} />;
111 let scrollableArea = null;
112
113 if (isLoading || statusIds.size > 0 || !emptyMessage) {
114 scrollableArea = (
115 <div className='scrollable' ref={this.setRef}>
116 <div role='feed' className='status-list'>
117 {prepend}
118
119 {statusIds.map((statusId, index) => {
120 return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
121 })}
122
123 {loadMore}
124 </div>
125 </div>
126 );
127 } else {
128 scrollableArea = (
129 <div className='empty-column-indicator' ref={this.setRef}>
130 {emptyMessage}
131 </div>
132 );
133 }
134
135 if (trackScroll) {
136 return (
137 <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
138 {scrollableArea}
139 </ScrollContainer>
140 );
141 } else {
142 return scrollableArea;
143 }
144 }
145
146 }
This page took 0.171652 seconds and 4 git commands to generate.