|
9 | 9 | */ |
10 | 10 |
|
11 | 11 | import VirtualizedList from '../VirtualizedList'; |
| 12 | +import CellRenderer from '../VirtualizedListCellRenderer'; |
12 | 13 | import {format} from 'node:util'; |
13 | 14 | import * as React from 'react'; |
14 | 15 | import {createElement, createRef} from 'react'; |
@@ -1140,6 +1141,107 @@ describe('VirtualizedList', () => { |
1140 | 1141 | {first: 13, last: 19, isSpacer: true}, |
1141 | 1142 | ]); |
1142 | 1143 | }); |
| 1144 | + |
| 1145 | + it('uses the sticky wrapper layout for sticky header cells', async () => { |
| 1146 | + // Sticky headers at 0, 5 and 10. Every cell is 50dip tall. |
| 1147 | + const items = generateItemsStickyEveryN(15, 5); |
| 1148 | + |
| 1149 | + let component; |
| 1150 | + await act(() => { |
| 1151 | + component = create( |
| 1152 | + <VirtualizedList initialNumToRender={15} {...baseItemProps(items)} />, |
| 1153 | + ); |
| 1154 | + }); |
| 1155 | + |
| 1156 | + await act(() => { |
| 1157 | + simulateLayout(component, { |
| 1158 | + viewport: {width: 10, height: 100}, |
| 1159 | + content: {width: 10, height: 750}, |
| 1160 | + }); |
| 1161 | + for (let i = 0; i < items.length; i++) { |
| 1162 | + // Sticky header cells measure themselves relative to their wrapper. |
| 1163 | + const y = items[i].sticky === true ? 0 : i * 50; |
| 1164 | + simulateCellLayout(component, items, i, { |
| 1165 | + width: 10, |
| 1166 | + height: 50, |
| 1167 | + x: 0, |
| 1168 | + y, |
| 1169 | + }); |
| 1170 | + } |
| 1171 | + }); |
| 1172 | + |
| 1173 | + // The wrapper reports after the cell's own layout. |
| 1174 | + await act(() => { |
| 1175 | + simulateStickyHeaderWrapperLayout(component, items, 5, { |
| 1176 | + width: 10, |
| 1177 | + height: 50, |
| 1178 | + x: 0, |
| 1179 | + y: 250, |
| 1180 | + }); |
| 1181 | + }); |
| 1182 | + expect(getCellOffset(component, 5)).toBe(250); |
| 1183 | + |
| 1184 | + // The wrapper reports before the cell's own layout. |
| 1185 | + await act(() => { |
| 1186 | + simulateStickyHeaderWrapperLayout(component, items, 10, { |
| 1187 | + width: 10, |
| 1188 | + height: 50, |
| 1189 | + x: 0, |
| 1190 | + y: 500, |
| 1191 | + }); |
| 1192 | + simulateCellLayout(component, items, 10, { |
| 1193 | + width: 10, |
| 1194 | + height: 50, |
| 1195 | + x: 0, |
| 1196 | + y: 0, |
| 1197 | + }); |
| 1198 | + }); |
| 1199 | + expect(getCellOffset(component, 10)).toBe(500); |
| 1200 | + |
| 1201 | + // Cells that are not sticky keep using their own layout. |
| 1202 | + expect(getCellOffset(component, 7)).toBe(350); |
| 1203 | + }); |
| 1204 | + |
| 1205 | + it('uses the cell layout again once a cell is no longer sticky', async () => { |
| 1206 | + const items = generateItemsStickyEveryN(10, 5); |
| 1207 | + const props = {initialNumToRender: 10, ...baseItemProps(items)}; |
| 1208 | + |
| 1209 | + let component; |
| 1210 | + await act(() => { |
| 1211 | + component = create(<VirtualizedList {...props} />); |
| 1212 | + }); |
| 1213 | + |
| 1214 | + await act(() => { |
| 1215 | + simulateLayout(component, { |
| 1216 | + viewport: {width: 10, height: 100}, |
| 1217 | + content: {width: 10, height: 500}, |
| 1218 | + }); |
| 1219 | + simulateStickyHeaderWrapperLayout(component, items, 5, { |
| 1220 | + width: 10, |
| 1221 | + height: 50, |
| 1222 | + x: 0, |
| 1223 | + y: 250, |
| 1224 | + }); |
| 1225 | + }); |
| 1226 | + expect(getCellOffset(component, 5)).toBe(250); |
| 1227 | + |
| 1228 | + await act(() => { |
| 1229 | + component.update(<VirtualizedList {...props} stickyHeaderIndices={[]} />); |
| 1230 | + }); |
| 1231 | + expect(findCellRenderer(component, items, 5).props.onLayout).toBe( |
| 1232 | + undefined, |
| 1233 | + ); |
| 1234 | + |
| 1235 | + await act(() => { |
| 1236 | + simulateCellLayout(component, items, 5, { |
| 1237 | + width: 10, |
| 1238 | + height: 50, |
| 1239 | + x: 0, |
| 1240 | + y: 260, |
| 1241 | + }); |
| 1242 | + }); |
| 1243 | + expect(getCellOffset(component, 5)).toBe(260); |
| 1244 | + }); |
1143 | 1245 | }); |
1144 | 1246 |
|
1145 | 1247 | it('unmounts sticky headers moved below viewport', async () => { |
@@ -3141,6 +3243,36 @@ function simulateCellLayout(component, items, itemIndex, dimensions) { |
3141 | 3243 | ); |
3142 | 3244 | } |
3143 | 3245 |
|
| 3246 | +function findCellRenderer(component, items, itemIndex) { |
| 3247 | + const cellKey = VirtualizedList._keyExtractor( |
| 3248 | + items[itemIndex], |
| 3249 | + itemIndex, |
| 3250 | + component.getInstance().props, |
| 3251 | + ); |
| 3252 | + return component.root.find( |
| 3253 | + node => node.type === CellRenderer && node.props.cellKey === cellKey, |
| 3254 | + ); |
| 3255 | +} |
| 3256 | + |
| 3257 | +// Mimics `ScrollViewStickyHeader`, which calls the cell element's `onLayout` |
| 3258 | +// with the layout of its wrapper. |
| 3259 | +function simulateStickyHeaderWrapperLayout( |
| 3260 | + component, |
| 3261 | + items, |
| 3262 | + itemIndex, |
| 3263 | + dimensions, |
| 3264 | +) { |
| 3265 | + findCellRenderer(component, items, itemIndex).props.onLayout({ |
| 3266 | + nativeEvent: {layout: dimensions, zoomScale: 1}, |
| 3267 | + }); |
| 3268 | +} |
| 3269 | + |
| 3270 | +function getCellOffset(component, itemIndex) { |
| 3271 | + const instance = component.getInstance(); |
| 3272 | + return instance.__getListMetrics().getCellMetrics(itemIndex, instance.props) |
| 3273 | + .offset; |
| 3274 | +} |
| 3275 | + |
3144 | 3276 | function simulateScroll(component, position) { |
3145 | 3277 | component.getInstance()._onScroll({ |
3146 | 3278 | nativeEvent: { |
|
0 commit comments