Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
import {VirtualizedListCellContextProvider} from './VirtualizedListContext.js';
import invariant from 'invariant';
import * as React from 'react';
import {isValidElement} from 'react';
import {Children, Fragment, isValidElement} from 'react';
import {StyleSheet, View} from 'react-native';

export type Props<ItemT> = {
Expand Down Expand Up @@ -204,13 +204,19 @@ export default class CellRenderer<ItemT> extends React.PureComponent<
// $FlowFixMe[not-a-component]
<ItemSeparatorComponent {...this.state.separatorProps} />
);
const cellStyle = inversionStyle
const baseCellStyle = inversionStyle
? horizontal
? [styles.rowReverse, inversionStyle]
: [styles.columnReverse, inversionStyle]
: horizontal
? [styles.row, inversionStyle]
: inversionStyle;
// zIndex on the item subtree does not reorder sibling cells; hoist it to this wrapper.
const childZIndex = extractZIndexFromListItemElement(element);
const cellStyle =
childZIndex != null
? StyleSheet.compose(baseCellStyle, {zIndex: childZIndex})
: baseCellStyle;
const result = !CellRendererComponent ? (
<View
style={cellStyle}
Expand Down Expand Up @@ -240,6 +246,40 @@ export default class CellRenderer<ItemT> extends React.PureComponent<
}
}

// Returns zIndex from the outermost element returned by renderItem, if any. We only look at
// that element's style prop (not deeper descendants inside a custom component).
function extractZIndexFromListItemElement(element: React.Node): ?number {
if (!isValidElement(element)) {
return null;
}

// $FlowFixMe[prop-missing] React.Element internal inspection
if (element.type === Fragment) {
let maxZIndex: ?number = null;
Children.forEach(
// $FlowFixMe[prop-missing] React.Element internal inspection
element.props.children,
child => {
const zIndex = extractZIndexFromListItemElement(child);
if (zIndex != null && (maxZIndex == null || zIndex > maxZIndex)) {
maxZIndex = zIndex;
}
},
);
return maxZIndex;
}

// $FlowFixMe[prop-missing] React.Element internal inspection
const style = element.props.style;
if (style == null) {
return null;
}

const flatStyle = StyleSheet.flatten(style);
const zIndex = flatStyle?.zIndex;
return typeof zIndex === 'number' ? zIndex : null;
}

const styles = StyleSheet.create({
row: {
flexDirection: 'row',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
* @format
*/

import CellRenderer from '../VirtualizedListCellRenderer';
import * as React from 'react';
import {Fragment} from 'react';
import {act, create} from 'react-test-renderer';
import {StyleSheet, View} from 'react-native';

function getFlattenedZIndex(style) {
if (style == null) {
return undefined;
}
return StyleSheet.flatten(style)?.zIndex;
}

function findCellWrapperViews(root) {
return root.findAll(
node =>
node.type === View &&
typeof node.props.onFocusCapture === 'function',
);
}

function renderCellRenderer(renderItem, extraProps = {}) {
let component;
act(() => {
component = create(
<CellRenderer
cellKey="cell-0"
index={0}
item={{id: 'item-0'}}
horizontal={false}
inversionStyle={null}
onUnmount={() => {}}
onUpdateSeparators={() => {}}
renderItem={renderItem}
{...extraProps}
/>,
);
});
return component;
}

describe('VirtualizedListCellRenderer zIndex', () => {
it('lifts zIndex from renderItem root View onto the cell wrapper', () => {
const component = renderCellRenderer(() => (
<View style={{zIndex: 10}} testID="list-item" />
));

const cellWrappers = findCellWrapperViews(component.root);
expect(cellWrappers).toHaveLength(1);
expect(getFlattenedZIndex(cellWrappers[0].props.style)).toBe(10);
});

it('lifts zIndex from style arrays on the renderItem root View', () => {
const component = renderCellRenderer(() => (
<View style={[{backgroundColor: 'red'}, {zIndex: 5}]} testID="list-item" />
));

const cellWrappers = findCellWrapperViews(component.root);
expect(getFlattenedZIndex(cellWrappers[0].props.style)).toBe(5);
});

it('lifts the maximum zIndex from Fragment children', () => {
const component = renderCellRenderer(() => (
<Fragment>
<View style={{zIndex: 2}} />
<View style={{zIndex: 7}} />
</Fragment>
));

const cellWrappers = findCellWrapperViews(component.root);
expect(getFlattenedZIndex(cellWrappers[0].props.style)).toBe(7);
});

it('does not add zIndex to the cell wrapper when renderItem has none', () => {
const component = renderCellRenderer(() => (
<View style={{backgroundColor: 'blue'}} testID="list-item" />
));

const cellWrappers = findCellWrapperViews(component.root);
expect(getFlattenedZIndex(cellWrappers[0].props.style)).toBeUndefined();
});

it('passes lifted zIndex to CellRendererComponent style prop', () => {
function CustomCellRenderer({children, style}) {
return (
<View testID="custom-cell" style={style}>
{children}
</View>
);
}

const component = renderCellRenderer(
() => <View style={{zIndex: 42}} />,
{CellRendererComponent: CustomCellRenderer},
);

const customCell = component.root.findByProps({testID: 'custom-cell'});
expect(getFlattenedZIndex(customCell.props.style)).toBe(42);
});
});
Loading