diff --git a/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js b/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js index 35573a9d703c..c09feb8a6afa 100644 --- a/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js +++ b/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js @@ -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 = { @@ -204,13 +204,19 @@ export default class CellRenderer extends React.PureComponent< // $FlowFixMe[not-a-component] ); - 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 ? ( 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', diff --git a/packages/virtualized-lists/Lists/__tests__/VirtualizedListCellRenderer-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizedListCellRenderer-test.js new file mode 100644 index 000000000000..e9d009b3becc --- /dev/null +++ b/packages/virtualized-lists/Lists/__tests__/VirtualizedListCellRenderer-test.js @@ -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( + {}} + onUpdateSeparators={() => {}} + renderItem={renderItem} + {...extraProps} + />, + ); + }); + return component; +} + +describe('VirtualizedListCellRenderer zIndex', () => { + it('lifts zIndex from renderItem root View onto the cell wrapper', () => { + const component = renderCellRenderer(() => ( + + )); + + 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(() => ( + + )); + + const cellWrappers = findCellWrapperViews(component.root); + expect(getFlattenedZIndex(cellWrappers[0].props.style)).toBe(5); + }); + + it('lifts the maximum zIndex from Fragment children', () => { + const component = renderCellRenderer(() => ( + + + + + )); + + 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(() => ( + + )); + + 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 ( + + {children} + + ); + } + + const component = renderCellRenderer( + () => , + {CellRendererComponent: CustomCellRenderer}, + ); + + const customCell = component.root.findByProps({testID: 'custom-cell'}); + expect(getFlattenedZIndex(customCell.props.style)).toBe(42); + }); +});