Skip to content

Commit 62505b8

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

File tree

1 file changed

+165
-14
lines changed

1 file changed

+165
-14
lines changed

src/Tree.js

Lines changed: 165 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, // The header that should be sticky
13+
};
14+
this._listRef = React.createRef();
15+
}
16+
917
_cache = new CellMeasurerCache({
1018
fixedWidth: true,
1119
minHeight: 20,
@@ -35,8 +43,80 @@ export default class Tree extends React.Component {
3543
: nodes[index];
3644
};
3745

46+
// Determine if a node is a group header
47+
isGroupHeader = node => {
48+
// Group headers are typically parent nodes with children
49+
// and deepness of 0 (root level)
50+
return node.children && node.children.length > 0 && node.deepness === 0;
51+
};
52+
53+
componentDidMount() {
54+
// Initial check for headers after mounting
55+
if (this._listRef.current) {
56+
const list = this._listRef.current;
57+
const grid = list && list.Grid;
58+
if (grid) {
59+
this.handleScroll({
60+
scrollTop: grid.state.scrollTop,
61+
scrollHeight: grid.state.scrollHeight,
62+
clientHeight: grid.props.height,
63+
});
64+
}
65+
}
66+
}
67+
68+
// Get all headers in the current data - simplified to reduce loops
69+
getAllHeaders = () => {
70+
const rowCount = this.getRowCount();
71+
const headers = [];
72+
let cumulativeHeight = 0;
73+
74+
for (let i = 0; i < rowCount; i++) {
75+
const node = this.getNode(i);
76+
77+
if (this.isGroupHeader(node)) {
78+
headers.push({
79+
node,
80+
index: i,
81+
top: cumulativeHeight,
82+
});
83+
}
84+
85+
cumulativeHeight += this._cache.rowHeight({index: i});
86+
}
87+
88+
return headers;
89+
};
90+
91+
// Handle scroll events to update sticky headers - simplified logic
92+
handleScroll = ({scrollTop, scrollHeight, clientHeight}) => {
93+
if (!this._listRef.current) return;
94+
95+
// Get all headers in the tree
96+
const allHeaders = this.getAllHeaders();
97+
98+
// Find the header that should be sticky (last one whose top is <= scrollTop)
99+
const topStickyHeader = allHeaders
100+
.filter(h => h.top <= scrollTop)
101+
.pop() || null;
102+
103+
// Only update state if something has changed
104+
const currentStickyId = this.state.topStickyHeader && this.state.topStickyHeader.node && this.state.topStickyHeader.node.id;
105+
const newStickyId = topStickyHeader && topStickyHeader.node && topStickyHeader.node.id;
106+
107+
if (currentStickyId !== newStickyId) {
108+
this.setState({
109+
topStickyHeader,
110+
});
111+
}
112+
};
113+
38114
rowRenderer = ({node, key, measure, style, NodeRenderer, index}) => {
39115
const {nodeMarginLeft} = this.props;
116+
const isHeader = this.isGroupHeader(node);
117+
118+
// Add a class to identify group headers
119+
const className = isHeader ? 'tree-group-header' : '';
40120

41121
return (
42122
<NodeRenderer
@@ -47,10 +127,43 @@ export default class Tree extends React.Component {
47127
userSelect: 'none',
48128
cursor: 'pointer',
49129
}}
130+
className={className}
50131
node={node}
51132
onChange={this.props.onChange}
52133
measure={measure}
53134
index={index}
135+
isGroupHeader={isHeader}
136+
/>
137+
);
138+
};
139+
140+
// Render the sticky header - simplified render method
141+
renderStickyHeader = () => {
142+
const {topStickyHeader} = this.state;
143+
if (!topStickyHeader) return null;
144+
145+
const {NodeRenderer, nodeMarginLeft} = this.props;
146+
// Always use the current node from the tree to ensure we have the latest state
147+
const index = topStickyHeader.index;
148+
const currentNode = this.getNode(index);
149+
150+
return (
151+
<NodeRenderer
152+
key={`sticky-header-${currentNode.id}`}
153+
style={{
154+
marginLeft: currentNode.deepness * nodeMarginLeft,
155+
userSelect: 'none',
156+
cursor: 'pointer',
157+
width: '100%',
158+
background: '#fff',
159+
zIndex: 10,
160+
}}
161+
className="tree-group-header tree-sticky"
162+
node={currentNode}
163+
onChange={this.props.onChange}
164+
index={index}
165+
isGroupHeader={true}
166+
isSticky={true}
54167
/>
55168
);
56169
};
@@ -66,25 +179,63 @@ export default class Tree extends React.Component {
66179
);
67180
};
68181

182+
componentDidUpdate(prevProps) {
183+
// If nodes change, reset the cache
184+
if (prevProps.nodes !== this.props.nodes) {
185+
this._cache.clearAll();
186+
if (this._listRef.current) {
187+
this._listRef.current.recomputeRowHeights();
188+
}
189+
190+
// Force rerender of sticky header when nodes change
191+
this.forceUpdate();
192+
}
193+
}
194+
69195
render() {
70196
const {nodes, width, scrollToIndex, scrollToAlignment} = this.props;
197+
const {topStickyHeader} = this.state;
198+
199+
// Calculate the height of the sticky header to properly offset the list
200+
const stickyHeaderHeight = topStickyHeader ? this._cache.rowHeight({index: topStickyHeader.index}) : 0;
71201

72202
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-
/>
203+
<div className="tree-container" style={{position: 'relative', height: '100%'}}>
204+
{/* Sticky header container - simplified markup */}
205+
{topStickyHeader && (
206+
<div
207+
className="tree-sticky-header-container"
208+
style={{
209+
position: 'absolute',
210+
top: 0,
211+
left: 0,
212+
right: 0,
213+
zIndex: 100,
214+
height: `${stickyHeaderHeight}px`,
215+
}}
216+
>
217+
{this.renderStickyHeader()}
218+
</div>
86219
)}
87-
</AutoSizer>
220+
221+
<AutoSizer disableWidth={Boolean(width)}>
222+
{({height, width: autoWidth}) => (
223+
<List
224+
deferredMeasurementCache={this._cache}
225+
ref={this._listRef}
226+
height={height}
227+
rowCount={this.getRowCount()}
228+
rowHeight={this._cache.rowHeight}
229+
rowRenderer={this.measureRowRenderer(nodes)}
230+
width={width || autoWidth}
231+
scrollToIndex={scrollToIndex}
232+
scrollToAlignment={scrollToAlignment}
233+
onScroll={this.handleScroll}
234+
overscanRowCount={20}
235+
/>
236+
)}
237+
</AutoSizer>
238+
</div>
88239
);
89240
}
90241
}

0 commit comments

Comments
 (0)