Skip to content
9 changes: 9 additions & 0 deletions patches/react-native-web/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,12 @@
- Upstream PR/issue: https://github.com/necolas/react-native-web/issues/2817
- E/App issue: https://github.com/Expensify/App/issues/73782
- PR introducing patch: https://github.com/Expensify/App/pull/76332

### [react-native-web+0.21.2+013+fix-selection-bug.patch](react-native-web+0.21.2+013+fix-selection-bug.patch)
- Reason:
```
Fix selection bug for InvertedFlatlist by reversing the DOM tree elements using `pushOrUnshift` method
```
- Upstream PR/issue: https://github.com/necolas/react-native-web/issues/1807, it has been closed because of [this](https://github.com/necolas/react-native-web/issues/1807#issuecomment-725689704)
- E/App issue: https://github.com/Expensify/App/issues/37447
- PR introducing patch: https://github.com/Expensify/App/pull/82507
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
diff --git a/node_modules/react-native-web/dist/exports/ScrollView/index.js b/node_modules/react-native-web/dist/exports/ScrollView/index.js
index c4f9b5b..cc5076e 100644
--- a/node_modules/react-native-web/dist/exports/ScrollView/index.js
+++ b/node_modules/react-native-web/dist/exports/ScrollView/index.js
@@ -558,8 +558,9 @@ class ScrollView extends React.Component {
var children = hasStickyHeaderIndices || pagingEnabled ? React.Children.map(this.props.children, (child, i) => {
var isSticky = hasStickyHeaderIndices && stickyHeaderIndices.indexOf(i) > -1;
if (child != null && (isSticky || pagingEnabled)) {
+ var stickyItemIndex = (this.props.children.length - 1) - i + 10;
return /*#__PURE__*/React.createElement(View, {
- style: [isSticky && styles.stickyHeader, pagingEnabled && styles.pagingEnabledChild]
+ style: [isSticky && styles.stickyHeader, pagingEnabled && styles.pagingEnabledChild, isSticky && {zIndex: stickyItemIndex}]
}, child);
} else {
return child;
@@ -636,7 +637,6 @@ var styles = StyleSheet.create({
stickyHeader: {
position: 'sticky',
top: 0,
- zIndex: 10
},
pagingEnabledHorizontal: {
scrollSnapType: 'x mandatory'
diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js
index 42c4984..0124746 100644
--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js
+++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js
@@ -108,6 +108,15 @@ function windowSizeOrDefault(windowSize) {
*
*/
class VirtualizedList extends StateSafePureComponent {
+ // reverse push order logic when props.inverted = true
+ pushOrUnshift(input, item) {
+ if (this.props.inverted) {
+ input.unshift(item);
+ } else {
+ input.push(item);
+ }
+ }
+
// scrollToEnd may be janky without getItemLayout prop
scrollToEnd(params) {
var animated = params ? params.animated : true;
@@ -343,6 +352,7 @@ class VirtualizedList extends StateSafePureComponent {
};
this._defaultRenderScrollComponent = props => {
var onRefresh = props.onRefresh;
+ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null;
if (this._isNestedWithSameOrientation()) {
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
return /*#__PURE__*/React.createElement(View, props);
@@ -354,6 +364,7 @@ class VirtualizedList extends StateSafePureComponent {
// $FlowFixMe[prop-missing] Invalid prop usage
// $FlowFixMe[incompatible-use]
React.createElement(ScrollView, _extends({}, props, {
+ contentContainerStyle: StyleSheet.compose(inversionStyle, this.props.contentContainerStyle),
refreshControl: props.refreshControl == null ? /*#__PURE__*/React.createElement(RefreshControl
// $FlowFixMe[incompatible-type]
, {
@@ -366,7 +377,9 @@ class VirtualizedList extends StateSafePureComponent {
} else {
// $FlowFixMe[prop-missing] Invalid prop usage
// $FlowFixMe[incompatible-use]
- return /*#__PURE__*/React.createElement(ScrollView, props);
+ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, {
+ contentContainerStyle: StyleSheet.compose(inversionStyle, this.props.contentContainerStyle)
+ }));
}
};
this._onCellLayout = (e, cellKey, index) => {
@@ -568,6 +581,17 @@ class VirtualizedList extends StateSafePureComponent {
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
this.setState((state, props) => {
var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount);
+
+ // revert the state if calculations are off
+ // this would only happen on the inverted flatlist (probably a bug with overscroll-behavior)
+ // when scrolled from bottom all the way up until onEndReached is triggered
+ if (cellsAroundViewport.first === cellsAroundViewport.last) {
+ var constrainedPrev = VirtualizedList._constrainToItemCount(state.cellsAroundViewport, props);
+ if (constrainedPrev.last > constrainedPrev.first) {
+ cellsAroundViewport = constrainedPrev;
+ }
+ }
+
var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props));
if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) {
return null;
@@ -679,7 +703,7 @@ class VirtualizedList extends StateSafePureComponent {
onViewableItemsChanged = _this$props3.onViewableItemsChanged,
viewabilityConfig = _this$props3.viewabilityConfig;
if (onViewableItemsChanged) {
- this._viewabilityTuples.push({
+ this.pushOrUnshift(this._viewabilityTuples, {
viewabilityHelper: new ViewabilityHelper(viewabilityConfig),
onViewableItemsChanged: onViewableItemsChanged
});
@@ -991,15 +1015,15 @@ class VirtualizedList extends StateSafePureComponent {
var end = getItemCount(data) - 1;
var prevCellKey;
last = Math.min(end, last);
- var _loop = function _loop() {
+ var _loop = () => {
var item = getItem(data, ii);
var key = VirtualizedList._keyExtractor(item, ii, _this.props);
_this._indicesToKeys.set(ii, key);
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
- stickyHeaderIndices.push(cells.length);
+ this.pushOrUnshift(stickyHeaderIndices, cells.length);
}
var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled();
- cells.push(/*#__PURE__*/React.createElement(CellRenderer, _extends({
+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({
CellRendererComponent: CellRendererComponent,
ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined,
ListItemComponent: ListItemComponent,
@@ -1074,14 +1098,14 @@ class VirtualizedList extends StateSafePureComponent {
// 1. Add cell for ListHeaderComponent
if (ListHeaderComponent) {
if (stickyIndicesFromProps.has(0)) {
- stickyHeaderIndices.push(0);
+ this.pushOrUnshift(stickyHeaderIndices, 0);
}
var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent :
/*#__PURE__*/
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
React.createElement(ListHeaderComponent, null);
- cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, {
+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, {
cellKey: this._getCellKey() + '-header',
key: "$header"
}, /*#__PURE__*/React.createElement(View
@@ -1105,7 +1129,7 @@ class VirtualizedList extends StateSafePureComponent {
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
React.createElement(ListEmptyComponent, null);
- cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, {
+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, {
cellKey: this._getCellKey() + '-empty',
key: "$empty"
}, /*#__PURE__*/React.cloneElement(_element2, {
@@ -1145,7 +1169,7 @@ class VirtualizedList extends StateSafePureComponent {
var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props);
var lastMetrics = this.__getFrameMetricsApprox(last, this.props);
var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset;
- cells.push(/*#__PURE__*/React.createElement(View, {
+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, {
key: "$spacer-" + section.first,
style: {
[spacerKey]: spacerSize
@@ -1168,7 +1192,7 @@ class VirtualizedList extends StateSafePureComponent {
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
React.createElement(ListFooterComponent, null);
- cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, {
+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, {
cellKey: this._getFooterCellKey(),
key: "$footer"
}, /*#__PURE__*/React.createElement(View, {
@@ -1179,6 +1203,14 @@ class VirtualizedList extends StateSafePureComponent {
_element3)));
}

+ if (this.props.inverted && stickyHeaderIndices.length > 0) {
+ var totalCells = cells.length;
+ stickyHeaderIndices = stickyHeaderIndices.map(function(recordedIndex) {
+ return totalCells - 1 - recordedIndex;
+ });
+ }
+
+
// 4. Render the ScrollView
var scrollProps = _objectSpread(_objectSpread({}, this.props), {}, {
onContentSizeChange: this._onContentSizeChange,
@@ -1353,7 +1385,7 @@ class VirtualizedList extends StateSafePureComponent {
* suppresses an error found when Flow v0.68 was deployed. To see the
* error delete this comment and run Flow. */
if (frame.inLayout) {
- framesInLayout.push(frame);
+ this.pushOrUnshift(framesInLayout, frame);
}
}
var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset;
@@ -1526,6 +1558,12 @@ var styles = StyleSheet.create({
horizontallyInverted: {
transform: 'scaleX(-1)'
},
+ rowReverse: {
+ flexDirection: 'row-reverse',
+ },
+ columnReverse: {
+ flexDirection: 'column-reverse',
+ },
debug: {
flex: 1
},
4 changes: 2 additions & 2 deletions src/components/FlatList/FlatList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,15 @@ function MVCPFlatList<T>({

const contentViewLength = contentView.childNodes.length;
for (let i = mvcpMinIndexForVisible; i < contentViewLength; i++) {
const subview = contentView.childNodes[i] as HTMLElement;
const subview = contentView.childNodes[restProps.inverted ? contentViewLength - i - 1 : i] as HTMLElement;
const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop;
if (subviewOffset > scrollOffset) {
prevFirstVisibleOffsetRef.current = subviewOffset;
firstVisibleViewRef.current = subview;
break;
}
}
}, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]);
}, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal, restProps.inverted]);

const adjustForMaintainVisibleContentPosition = useCallback(
(animated = true) => {
Expand Down
63 changes: 63 additions & 0 deletions src/components/FlatList/InvertedFlatList/index.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import FlatList from '@components/FlatList/FlatList';
import useFlatListScrollKey from '@components/FlatList/hooks/useFlatListScrollKey';
import useThemeStyles from '@hooks/useThemeStyles';
import CellRendererComponent from './CellRendererComponent';
import shouldRemoveClippedSubviews from './shouldRemoveClippedSubviews';
import type {InvertedFlatListProps} from './types';

// Adapted from https://github.com/facebook/react-native/blob/29a0d7c3b201318a873db0d1b62923f4ce720049/packages/virtualized-lists/Lists/VirtualizeUtils.js#L237
function defaultKeyExtractor<T>(item: T | {key: string} | {id: string}, index: number): string {
if (item != null) {
if (typeof item === 'object' && 'key' in item) {
return item.key;
}
if (typeof item === 'object' && 'id' in item) {
return item.id;
}
}
return String(index);
}

function InvertedFlatList<T>({
ref,
shouldEnableAutoScrollToTopThreshold,
shouldFocusToTopOnMount = false,
initialScrollKey,
data,
onStartReached,
renderItem,
keyExtractor = defaultKeyExtractor,
...restProps
}: InvertedFlatListProps<T>) {
const {displayedData, maintainVisibleContentPosition, handleStartReached, handleRenderItem, listRef} = useFlatListScrollKey<T>({
data,
keyExtractor,
initialScrollKey,
inverted: true,
onStartReached,
shouldEnableAutoScrollToTopThreshold,
renderItem,
ref,
});
const styles = useThemeStyles();

return (
<FlatList<T>
// eslint-disable-next-line react/jsx-props-no-spreading
{...restProps}
ref={listRef}
maintainVisibleContentPosition={maintainVisibleContentPosition}
inverted
data={displayedData}
renderItem={handleRenderItem}
keyExtractor={keyExtractor}
onStartReached={handleStartReached}
CellRendererComponent={CellRendererComponent}
removeClippedSubviews={shouldRemoveClippedSubviews}
contentContainerStyle={[restProps.contentContainerStyle, shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined]}
/>
);
}

export default InvertedFlatList;
8 changes: 8 additions & 0 deletions src/components/FlatList/InvertedFlatList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import FlatList from '@components/FlatList/FlatList';
import useFlatListScrollKey from '@components/FlatList/hooks/useFlatListScrollKey';
import useThemeStyles from '@hooks/useThemeStyles';
import CellRendererComponent from './CellRendererComponent';
import shouldRemoveClippedSubviews from './shouldRemoveClippedSubviews';
import type {InvertedFlatListProps} from './types';
Expand All @@ -21,6 +22,7 @@ function defaultKeyExtractor<T>(item: T | {key: string} | {id: string}, index: n
function InvertedFlatList<T>({
ref,
shouldEnableAutoScrollToTopThreshold,
shouldFocusToTopOnMount = false,
initialScrollKey,
data,
onStartReached,
Expand All @@ -38,6 +40,7 @@ function InvertedFlatList<T>({
renderItem,
ref,
});
const styles = useThemeStyles();

return (
<FlatList<T>
Expand All @@ -52,6 +55,11 @@ function InvertedFlatList<T>({
onStartReached={handleStartReached}
CellRendererComponent={CellRendererComponent}
removeClippedSubviews={shouldRemoveClippedSubviews}
contentContainerStyle={[
restProps.contentContainerStyle,
restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse,
!shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined,
]}
/>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/components/FlatList/InvertedFlatList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {CustomFlatListProps} from '@components/FlatList/FlatList/types';

type InvertedFlatListProps<T> = Omit<CustomFlatListProps<T>, 'data' | 'renderItem' | 'initialScrollIndex'> & {
shouldEnableAutoScrollToTopThreshold?: boolean;
shouldFocusToTopOnMount?: boolean;
data: T[];
renderItem: ListRenderItem<T>;
initialScrollKey?: string | null;
Expand Down
5 changes: 4 additions & 1 deletion src/pages/inbox/report/PureReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import type {OnyxDataWithErrors} from '@libs/ErrorUtils';
import {getLatestErrorMessageField, isReceiptError} from '@libs/ErrorUtils';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import getPlatform from '@libs/getPlatform';
import {isReportMessageAttachment} from '@libs/isReportMessageAttachment';
import Navigation from '@libs/Navigation/Navigation';
import {getBankAccountLastFourDigits} from '@libs/PaymentUtils';
Expand Down Expand Up @@ -564,6 +565,8 @@ function PureReportActionItem({
const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions();
const {translate, formatPhoneNumber, localeCompare, formatTravelDate, getLocalDateFromDatetime, datetimeToCalendarTime} = useLocalize();
const {showConfirmModal} = useConfirmModal();
const platform = getPlatform();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-1 (docs)

The PR introduces getPlatform() and isWeb inside PureReportActionItem to determine the accessibilityRole. This is a platform-specific check within a heavily rendered component. While the original code already had Platform.OS !== CONST.PLATFORM.WEB, replacing it with getPlatform() (a function call on every render) is strictly worse than the static constant Platform.OS for a component rendered per report action.

If keeping the platform check inline, prefer the original Platform.OS which is a static constant with zero per-render cost:

accessibilityRole={Platform.OS !== CONST.PLATFORM.WEB ? CONST.ROLE.BUTTON : undefined}

Alternatively, extract the platform-specific accessibilityRole logic into a platform-specific file or constant.


Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

const isWeb = platform === CONST.PLATFORM.WEB;
const personalDetail = useCurrentUserPersonalDetails();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const reportID = report?.reportID ?? action?.reportID;
Expand Down Expand Up @@ -2110,7 +2113,7 @@ function PureReportActionItem({
withoutFocusOnSecondaryInteraction
accessibilityLabel={accessibilityLabel}
accessibilityHint={translate('accessibilityHints.chatMessage')}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityRole={!isWeb ? CONST.ROLE.BUTTON : undefined}
sentryLabel={CONST.SENTRY_LABEL.REPORT.PURE_REPORT_ACTION_ITEM}
>
<Hoverable
Expand Down
3 changes: 2 additions & 1 deletion src/pages/inbox/report/ReportActionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -879,8 +879,9 @@ function ReportActionsList({
data={sortedVisibleReportActions}
renderItem={renderItem}
renderScrollComponent={renderActionSheetAwareScrollView}
contentContainerStyle={[styles.chatContentScrollView, shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined]}
contentContainerStyle={styles.chatContentScrollView}
shouldHideContent={shouldScrollToEndAfterLayout}
shouldFocusToTopOnMount={shouldFocusToTopOnMount}
shouldDisableVisibleContentPosition={shouldScrollToEndAfterLayout}
showsVerticalScrollIndicator={!shouldScrollToEndAfterLayout}
keyExtractor={keyExtractor}
Expand Down
Loading