Skip to content

Fix: inverted option causing sticky headers, ListEmptyComponent to appear inverted, and causing scroll direction to be inverted on web #1487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

mtourj
Copy link

@mtourj mtourj commented Jan 23, 2025

Description

This fixes three problems that occur when inverted is enabled on FlashList:

  • ListEmptyComponent appearing inverted (upside-down)
  • Sticky headers appearing inverted
  • Scroll position on web becoming inverted

Tested in the expo fixture as well as in my own application.

Resolves #1511 #1351 #1177 #611

Checklist

@mtourj
Copy link
Author

mtourj commented Jan 23, 2025

I have signed the CLA!

@ActaV-N
Copy link

ActaV-N commented Feb 20, 2025

I patched package shopify/[email protected] based on this pr but it didn't resolve the problem Scroll position on web becoming inverted

Oh sorry I just patched files that already has been built(js files only) and it works.
#1351 (comment)

@aicopdev
Copy link

Hi, this patch fixed the inverted scroll in Web. But created a different issue.

if there is any item with scrollable content, the scroll behaviour will break. You can check this behaviour by rendering any item as scrollable.

Verified with and without the patch. Without the patch, scrollable item is working as expected. But overall scroll behaviour is reveresed.

With the patch, overall scroll behaviour is fixed. But scrollable item is broken..

@mtourj
Copy link
Author

mtourj commented Feb 28, 2025

Thanks @aicopdev , I fixed it by checking event target for scrollability using getComputedStyles and letting those nested elements handle wheel events if they are scrollable.

@aicopdev
Copy link

aicopdev commented Mar 5, 2025

Thanks @mtourj

Can you please update the patch file with this changes ?? Does it work with the latest changes also ?? Not able to do so by just changing based on the PR and last patch file.

I mean, I tried applying the patch for components based on the PR. But vertical scroll behaviour stopped working. Have you verified scrolls in both directions ??

@mtourj
Copy link
Author

mtourj commented Mar 14, 2025

Thanks @mtourj

Can you please update the patch file with this changes ?? Does it work with the latest changes also ?? Not able to do so by just changing based on the PR and last patch file.

I mean, I tried applying the patch for components based on the PR. But vertical scroll behaviour stopped working. Have you verified scrolls in both directions ??

yes it works with the latest. here u go:

diff --git a/node_modules/@shopify/flash-list/dist/FlashList.js b/node_modules/@shopify/flash-list/dist/FlashList.js
index 486a284..7db3206 100644
--- a/node_modules/@shopify/flash-list/dist/FlashList.js
+++ b/node_modules/@shopify/flash-list/dist/FlashList.js
@@ -45,6 +45,7 @@ var FlashList = /** @class */ (function (_super) {
             applyToInitialOffset: true,
         };
         _this.isEmptyList = false;
+        _this.hasInvertedWheelHandler = false;
         _this.onEndReached = function () {
             var _a, _b;
             (_b = (_a = _this.props).onEndReached) === null || _b === void 0 ? void 0 : _b.call(_a);
@@ -86,9 +87,7 @@ var FlashList = /** @class */ (function (_super) {
             return (react_1.default.createElement(react_1.default.Fragment, null,
                 react_1.default.createElement(PureComponentWrapper_1.PureComponentWrapper, { enabled: _this.isListLoaded || children.length > 0 || _this.isEmptyList, contentStyle: _this.props.contentContainerStyle, horizontal: _this.props.horizontal, header: _this.props.ListHeaderComponent, extraData: _this.state.extraData, headerStyle: _this.props.ListHeaderComponentStyle, inverted: _this.props.inverted, renderer: _this.header }),
                 react_1.default.createElement(AutoLayoutView_1.default, tslib_1.__assign({}, props, { onBlankAreaEvent: _this.props.onBlankArea, onLayout: _this.updateDistanceFromWindow, disableAutoLayout: _this.props.disableAutoLayout }), children),
-                _this.isEmptyList
-                    ? _this.getValidComponent(_this.props.ListEmptyComponent)
-                    : null,
+                _this.isEmptyList ? (react_1.default.createElement(react_native_1.View, { style: _this.getTransform() }, _this.getValidComponent(_this.props.ListEmptyComponent))) : null,
                 react_1.default.createElement(PureComponentWrapper_1.PureComponentWrapper, { enabled: _this.isListLoaded || children.length > 0 || _this.isEmptyList, contentStyle: _this.props.contentContainerStyle, horizontal: _this.props.horizontal, header: _this.props.ListFooterComponent, extraData: _this.state.extraData, headerStyle: _this.props.ListFooterComponentStyle, inverted: _this.props.inverted, renderer: _this.footer }),
                 _this.getComponentForHeightMeasurement()));
         };
@@ -159,7 +158,7 @@ var FlashList = /** @class */ (function (_super) {
             (_a = _this.stickyContentContainerRef) === null || _a === void 0 ? void 0 : _a.setEnabled(_this.isStickyEnabled);
         };
         _this.rowRendererSticky = function (index) {
-            return _this.rowRendererWithIndex(index, FlashListProps_1.RenderTargetOptions.StickyHeader);
+            return (react_1.default.createElement(react_native_1.View, { style: tslib_1.__assign({ flexDirection: _this.props.horizontal ? "row" : "column" }, _this.getTransform()) }, _this.rowRendererWithIndex(index, FlashListProps_1.RenderTargetOptions.StickyHeader)));
         };
         _this.rowRendererWithIndex = function (index, target) {
             var _a, _b, _c;
@@ -362,11 +361,22 @@ var FlashList = /** @class */ (function (_super) {
             return mutableLayout === null || mutableLayout === void 0 ? void 0 : mutableLayout.size;
         }, flashListProps);
     };
+    FlashList.prototype.updateWebScrollHandler = function () {
+        if (react_native_1.Platform.OS !== "web" || !this.rlvRef)
+            return;
+        (0, PlatformHelper_1.removeInvertedWheelHandler)(this.rlvRef);
+        this.hasInvertedWheelHandler = false;
+        if (this.props.inverted) {
+            (0, PlatformHelper_1.addInvertedWheelHandler)(this.rlvRef, this.props.horizontal ? "horizontal" : "vertical");
+            this.hasInvertedWheelHandler = true;
+        }
+    };
     FlashList.prototype.componentDidMount = function () {
         var _a;
         if (((_a = this.props.data) === null || _a === void 0 ? void 0 : _a.length) === 0) {
             this.raiseOnLoadEventIfNeeded();
         }
+        this.updateWebScrollHandler();
     };
     FlashList.prototype.componentWillUnmount = function () {
         this.viewabilityManager.dispose();
@@ -375,6 +385,15 @@ var FlashList = /** @class */ (function (_super) {
         if (this.itemSizeWarningTimeoutId !== undefined) {
             clearTimeout(this.itemSizeWarningTimeoutId);
         }
+        if (this.hasInvertedWheelHandler) {
+            (0, PlatformHelper_1.removeInvertedWheelHandler)(this.rlvRef);
+        }
+    };
+    FlashList.prototype.componentDidUpdate = function (prevProps) {
+        if (prevProps.inverted !== this.props.inverted ||
+            prevProps.horizontal !== this.props.horizontal) {
+            this.updateWebScrollHandler();
+        }
     };
     FlashList.prototype.render = function () {
         this.isEmptyList = this.state.dataProvider.getSize() === 0;
diff --git a/node_modules/@shopify/flash-list/dist/native/config/PlatformHelper.js b/node_modules/@shopify/flash-list/dist/native/config/PlatformHelper.js
index b293d52..afaa947 100644
--- a/node_modules/@shopify/flash-list/dist/native/config/PlatformHelper.js
+++ b/node_modules/@shopify/flash-list/dist/native/config/PlatformHelper.js
@@ -1,6 +1,6 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.getFooterContainer = exports.getItemAnimator = exports.getCellContainerPlatformStyles = exports.PlatformConfig = void 0;
+exports.removeInvertedWheelHandler = exports.addInvertedWheelHandler = exports.getFooterContainer = exports.getItemAnimator = exports.getCellContainerPlatformStyles = exports.PlatformConfig = void 0;
 var DefaultJSItemAnimator_1 = require("recyclerlistview/dist/reactnative/platform/reactnative/itemanimators/defaultjsanimator/DefaultJSItemAnimator");
 var PlatformConfig = {
     defaultDrawDistance: 250,
@@ -20,4 +20,12 @@ var getFooterContainer = function () {
     return undefined;
 };
 exports.getFooterContainer = getFooterContainer;
+var addInvertedWheelHandler = function (ref, type) {
+    return undefined;
+};
+exports.addInvertedWheelHandler = addInvertedWheelHandler;
+var removeInvertedWheelHandler = function (ref) {
+    return undefined;
+};
+exports.removeInvertedWheelHandler = removeInvertedWheelHandler;
 //# sourceMappingURL=PlatformHelper.js.map
\ No newline at end of file
diff --git a/node_modules/@shopify/flash-list/dist/native/config/PlatformHelper.web.js b/node_modules/@shopify/flash-list/dist/native/config/PlatformHelper.web.js
index 482e092..5594937 100644
--- a/node_modules/@shopify/flash-list/dist/native/config/PlatformHelper.web.js
+++ b/node_modules/@shopify/flash-list/dist/native/config/PlatformHelper.web.js
@@ -1,8 +1,23 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.getFooterContainer = exports.getItemAnimator = exports.getCellContainerPlatformStyles = exports.PlatformConfig = void 0;
+exports.removeInvertedWheelHandler = exports.addInvertedWheelHandler = exports.getFooterContainer = exports.getItemAnimator = exports.getCellContainerPlatformStyles = exports.PlatformConfig = void 0;
 var react_native_1 = require("react-native");
 var DefaultJSItemAnimator_1 = require("recyclerlistview/dist/reactnative/platform/reactnative/itemanimators/defaultjsanimator/DefaultJSItemAnimator");
+var createInvertedWheelEventHandler = function (type) {
+    return function (event) {
+        var node = event.currentTarget;
+        var deltaX = type === "horizontal" ? -event.deltaX : event.deltaX;
+        var deltaY = type === "vertical" ? -event.deltaY : event.deltaY;
+        node.scrollBy({
+            top: deltaY,
+            left: deltaX,
+            behavior: "auto",
+        });
+        event.preventDefault();
+    };
+};
+var verticalInvertedWheelEventHandler = createInvertedWheelEventHandler("vertical");
+var horizontalInvertedWheelEventHandler = createInvertedWheelEventHandler("horizontal");
 var PlatformConfig = {
     defaultDrawDistance: 2000,
     invertedTransformStyle: { transform: [{ scaleY: -1 }] },
@@ -22,4 +37,31 @@ var getFooterContainer = function () {
     return react_native_1.View;
 };
 exports.getFooterContainer = getFooterContainer;
+var addInvertedWheelHandler = function (ref, type) {
+    if (!ref)
+        return undefined;
+    var node = (0, react_native_1.findNodeHandle)(ref);
+    if (node) {
+        var handler = type === "horizontal"
+            ? horizontalInvertedWheelEventHandler
+            : verticalInvertedWheelEventHandler;
+        node.addEventListener("wheel", handler, {
+            passive: false,
+        });
+        return handler;
+    }
+    return undefined;
+};
+exports.addInvertedWheelHandler = addInvertedWheelHandler;
+var removeInvertedWheelHandler = function (ref) {
+    if (!ref)
+        return undefined;
+    var node = (0, react_native_1.findNodeHandle)(ref);
+    if (node) {
+        node.removeEventListener("wheel", verticalInvertedWheelEventHandler);
+        node.removeEventListener("wheel", horizontalInvertedWheelEventHandler);
+    }
+    return undefined;
+};
+exports.removeInvertedWheelHandler = removeInvertedWheelHandler;
 //# sourceMappingURL=PlatformHelper.web.js.map
\ No newline at end of file

@aicopdev
Copy link

Thanks. With this, when we scroll up and if the nested scrollable element happens to be comes along the way, scroll behaviour of parent and child clashes. And scroll direction changes causing some bouncy behaviour. Please try to use laptop trackpad instead of mouse.

Same goes. when we try to scroll horizontal, then also scroll of the parent will get activated and try to move up and down.. the behaviour is not smooth.

REC-20250317165016.mp4

@mtourj
Copy link
Author

mtourj commented Mar 18, 2025

Thanks. With this, when we scroll up and if the nested scrollable element happens to be comes along the way, scroll behaviour of parent and child clashes. And scroll direction changes causing some bouncy behaviour. Please try to use laptop trackpad instead of mouse.

Same goes. when we try to scroll horizontal, then also scroll of the parent will get activated and try to move up and down.. the behaviour is not smooth.

REC-20250317165016.mp4

Weird, I’ll check it out when I get the chance and lyk

@aicopdev
Copy link

Thanks. With this, when we scroll up and if the nested scrollable element happens to be comes along the way, scroll behaviour of parent and child clashes. And scroll direction changes causing some bouncy behaviour. Please try to use laptop trackpad instead of mouse.
Same goes. when we try to scroll horizontal, then also scroll of the parent will get activated and try to move up and down.. the behaviour is not smooth.
REC-20250317165016.mp4

Weird, I’ll check it out when I get the chance and lyk

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants