diff --git a/patches/react-native-web/details.md b/patches/react-native-web/details.md index df0cad52449ef..6450a4ae92a97 100644 --- a/patches/react-native-web/details.md +++ b/patches/react-native-web/details.md @@ -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 \ No newline at end of file diff --git a/patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch b/patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch new file mode 100644 index 0000000000000..2a296cff0dba3 --- /dev/null +++ b/patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch @@ -0,0 +1,194 @@ +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..fa32056 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..caf22bb 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,14 @@ 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) { ++ cellsAroundViewport = state.cellsAroundViewport; ++ } ++ + 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 +700,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 +1012,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 +1095,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 +1126,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 +1166,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 +1189,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 +1200,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 +1382,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 +1555,12 @@ var styles = StyleSheet.create({ + horizontallyInverted: { + transform: 'scaleX(-1)' + }, ++ rowReverse: { ++ flexDirection: 'row-reverse', ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse', ++ }, + debug: { + flex: 1 + }, \ No newline at end of file diff --git a/src/components/FlatList/FlatList/index.tsx b/src/components/FlatList/FlatList/index.tsx index 022bc2047acc5..e30c50b128c1d 100644 --- a/src/components/FlatList/FlatList/index.tsx +++ b/src/components/FlatList/FlatList/index.tsx @@ -112,7 +112,7 @@ function MVCPFlatList({ 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; @@ -120,7 +120,7 @@ function MVCPFlatList({ break; } } - }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]); + }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal, restProps.inverted]); const adjustForMaintainVisibleContentPosition = useCallback( (animated = true) => { diff --git a/src/components/FlatList/InvertedFlatList/index.native.tsx b/src/components/FlatList/InvertedFlatList/index.native.tsx new file mode 100644 index 0000000000000..28a7040859570 --- /dev/null +++ b/src/components/FlatList/InvertedFlatList/index.native.tsx @@ -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(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({ + ref, + shouldEnableAutoScrollToTopThreshold, + shouldFocusToTopOnMount = false, + initialScrollKey, + data, + onStartReached, + renderItem, + keyExtractor = defaultKeyExtractor, + ...restProps +}: InvertedFlatListProps) { + const {displayedData, maintainVisibleContentPosition, handleStartReached, handleRenderItem, listRef} = useFlatListScrollKey({ + data, + keyExtractor, + initialScrollKey, + inverted: true, + onStartReached, + shouldEnableAutoScrollToTopThreshold, + renderItem, + ref, + }); + const styles = useThemeStyles(); + + return ( + + // 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; diff --git a/src/components/FlatList/InvertedFlatList/index.tsx b/src/components/FlatList/InvertedFlatList/index.tsx index 87af6b05d9576..335cc526c41f9 100644 --- a/src/components/FlatList/InvertedFlatList/index.tsx +++ b/src/components/FlatList/InvertedFlatList/index.tsx @@ -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'; @@ -21,6 +22,7 @@ function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: n function InvertedFlatList({ ref, shouldEnableAutoScrollToTopThreshold, + shouldFocusToTopOnMount = false, initialScrollKey, data, onStartReached, @@ -38,6 +40,7 @@ function InvertedFlatList({ renderItem, ref, }); + const styles = useThemeStyles(); return ( @@ -52,6 +55,11 @@ function InvertedFlatList({ onStartReached={handleStartReached} CellRendererComponent={CellRendererComponent} removeClippedSubviews={shouldRemoveClippedSubviews} + contentContainerStyle={[ + restProps.contentContainerStyle, + restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse, + !shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined, + ]} /> ); } diff --git a/src/components/FlatList/InvertedFlatList/types.ts b/src/components/FlatList/InvertedFlatList/types.ts index a779bc247bd92..eb17bdeb533d2 100644 --- a/src/components/FlatList/InvertedFlatList/types.ts +++ b/src/components/FlatList/InvertedFlatList/types.ts @@ -4,6 +4,7 @@ import type {CustomFlatListProps} from '@components/FlatList/FlatList/types'; type InvertedFlatListProps = Omit, 'data' | 'renderItem' | 'initialScrollIndex'> & { shouldEnableAutoScrollToTopThreshold?: boolean; + shouldFocusToTopOnMount?: boolean; data: T[]; renderItem: ListRenderItem; initialScrollKey?: string | null; diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 1c18f15eff511..fbebbe7b19284 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -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'; @@ -546,6 +547,8 @@ function PureReportActionItem({ const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); const {translate, formatPhoneNumber, localeCompare, formatTravelDate, getLocalDateFromDatetime, datetimeToCalendarTime} = useLocalize(); const {showConfirmModal} = useConfirmModal(); + const platform = getPlatform(); + const isWeb = platform === CONST.PLATFORM.WEB; const personalDetail = useCurrentUserPersonalDetails(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const reportID = report?.reportID ?? action?.reportID; @@ -2059,7 +2062,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} >