Skip to content

Commit 256397b

Browse files
committed
feat: implement sticky headers in Tree component
1 parent 0a65dfb commit 256397b

File tree

1 file changed

+144
-14
lines changed

1 file changed

+144
-14
lines changed

src/Tree.js

Lines changed: 144 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import {FlattenedNode} from './shapes/nodeShapes';
66
import TreeState, {State} from './state/TreeState';
77

88
export default class Tree extends React.Component {
9+
constructor(props) {
10+
super(props);
11+
this.state = {
12+
topStickyHeader: null,
13+
};
14+
this._listRef = React.createRef();
15+
}
16+
917
_cache = new CellMeasurerCache({
1018
fixedWidth: true,
1119
minHeight: 20,
@@ -35,8 +43,66 @@ export default class Tree extends React.Component {
3543
: nodes[index];
3644
};
3745

46+
isGroupHeader = node => {
47+
return node.children && node.children.length > 0 && node.deepness === 0;
48+
};
49+
50+
componentDidMount() {
51+
if (this._listRef.current) {
52+
const list = this._listRef.current;
53+
const grid = list && list.Grid;
54+
if (grid) {
55+
this.handleScroll({
56+
scrollTop: grid.state.scrollTop,
57+
});
58+
}
59+
}
60+
}
61+
62+
getAllHeaders = () => {
63+
const rowCount = this.getRowCount();
64+
const headers = [];
65+
let cumulativeHeight = 0;
66+
67+
for (let i = 0; i < rowCount; i++) {
68+
const node = this.getNode(i);
69+
70+
if (this.isGroupHeader(node)) {
71+
headers.push({
72+
node,
73+
index: i,
74+
top: cumulativeHeight,
75+
});
76+
}
77+
78+
cumulativeHeight += this._cache.rowHeight({index: i});
79+
}
80+
81+
return headers;
82+
};
83+
84+
handleScroll = ({scrollTop}) => {
85+
if (!this._listRef.current) return;
86+
87+
const allHeaders = this.getAllHeaders();
88+
89+
const topStickyHeader = allHeaders.filter(h => h.top <= scrollTop).pop() || null;
90+
91+
const currentStickyId =
92+
this.state.topStickyHeader && this.state.topStickyHeader.node && this.state.topStickyHeader.node.id;
93+
const newStickyId = topStickyHeader && topStickyHeader.node && topStickyHeader.node.id;
94+
95+
if (currentStickyId !== newStickyId) {
96+
this.setState({
97+
topStickyHeader,
98+
});
99+
}
100+
};
101+
38102
rowRenderer = ({node, key, measure, style, NodeRenderer, index}) => {
39103
const {nodeMarginLeft} = this.props;
104+
const isHeader = this.isGroupHeader(node);
105+
const className = isHeader ? 'tree-group-header' : '';
40106

41107
return (
42108
<NodeRenderer
@@ -47,10 +113,41 @@ export default class Tree extends React.Component {
47113
userSelect: 'none',
48114
cursor: 'pointer',
49115
}}
116+
className={className}
50117
node={node}
51118
onChange={this.props.onChange}
52119
measure={measure}
53120
index={index}
121+
isGroupHeader={isHeader}
122+
/>
123+
);
124+
};
125+
126+
renderStickyHeader = () => {
127+
const {topStickyHeader} = this.state;
128+
if (!topStickyHeader) return null;
129+
130+
const {NodeRenderer, nodeMarginLeft} = this.props;
131+
const index = topStickyHeader.index;
132+
const currentNode = this.getNode(index);
133+
134+
return (
135+
<NodeRenderer
136+
key={`sticky-header-${currentNode.id}`}
137+
style={{
138+
marginLeft: currentNode.deepness * nodeMarginLeft,
139+
userSelect: 'none',
140+
cursor: 'pointer',
141+
width: '100%',
142+
background: '#fff',
143+
zIndex: 10,
144+
}}
145+
className="tree-group-header tree-sticky"
146+
node={currentNode}
147+
onChange={this.props.onChange}
148+
index={index}
149+
isGroupHeader={true}
150+
isSticky={true}
54151
/>
55152
);
56153
};
@@ -66,25 +163,58 @@ export default class Tree extends React.Component {
66163
);
67164
};
68165

166+
componentDidUpdate(prevProps) {
167+
if (prevProps.nodes !== this.props.nodes) {
168+
this._cache.clearAll();
169+
if (this._listRef.current) {
170+
this._listRef.current.recomputeRowHeights();
171+
}
172+
173+
this.forceUpdate();
174+
}
175+
}
176+
69177
render() {
70178
const {nodes, width, scrollToIndex, scrollToAlignment} = this.props;
179+
const {topStickyHeader} = this.state;
180+
const stickyHeaderHeight = topStickyHeader ? this._cache.rowHeight({index: topStickyHeader.index}) : 0;
71181

72182
return (
73-
<AutoSizer disableWidth={Boolean(width)}>
74-
{({height, width: autoWidth}) => (
75-
<List
76-
deferredMeasurementCache={this._cache}
77-
ref={r => (this._list = r)}
78-
height={height}
79-
rowCount={this.getRowCount()}
80-
rowHeight={this._cache.rowHeight}
81-
rowRenderer={this.measureRowRenderer(nodes)}
82-
width={width || autoWidth}
83-
scrollToIndex={scrollToIndex}
84-
scrollToAlignment={scrollToAlignment}
85-
/>
183+
<div className="tree-container" style={{position: 'relative', height: '100%'}}>
184+
{topStickyHeader && (
185+
<div
186+
className="tree-sticky-header-container"
187+
style={{
188+
position: 'absolute',
189+
top: 0,
190+
left: 0,
191+
right: 0,
192+
zIndex: 100,
193+
height: `${stickyHeaderHeight}px`,
194+
}}
195+
>
196+
{this.renderStickyHeader()}
197+
</div>
86198
)}
87-
</AutoSizer>
199+
200+
<AutoSizer disableWidth={Boolean(width)}>
201+
{({height, width: autoWidth}) => (
202+
<List
203+
deferredMeasurementCache={this._cache}
204+
ref={this._listRef}
205+
height={height}
206+
rowCount={this.getRowCount()}
207+
rowHeight={this._cache.rowHeight}
208+
rowRenderer={this.measureRowRenderer(nodes)}
209+
width={width || autoWidth}
210+
scrollToIndex={scrollToIndex}
211+
scrollToAlignment={scrollToAlignment}
212+
onScroll={this.handleScroll}
213+
overscanRowCount={20}
214+
/>
215+
)}
216+
</AutoSizer>
217+
</div>
88218
);
89219
}
90220
}

0 commit comments

Comments
 (0)