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
4 changes: 4 additions & 0 deletions src/recyclerview/RecyclerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { getInvertedTransformStyle } from "./utils/getInvertedTransformStyle";
import { StickyHeaders, StickyHeaderRef } from "./components/StickyHeaders";
import { ScrollAnchor, ScrollAnchorRef } from "./components/ScrollAnchor";
import { useRecyclerViewController } from "./hooks/useRecyclerViewController";
import { useInvertedWheelFix } from "./hooks/useInvertedWheelFix";
import { RenderTimeTracker } from "./helpers/RenderTimeTracker";

/**
Expand Down Expand Up @@ -483,6 +484,9 @@ const RecyclerViewComponent = <T,>(
return onScrollHandler;
}, [onScrollHandler, scrollY, stickyHeaders, stickyHeaderUseNativeDriver]);

// Re-invert mouse-wheel direction for inverted lists on web (no-op native).
useInvertedWheelFix(scrollViewRef, inverted, horizontal);

const shouldMaintainVisibleContentPosition =
recyclerViewManager.shouldMaintainVisibleContentPosition();

Expand Down
13 changes: 13 additions & 0 deletions src/recyclerview/hooks/useInvertedWheelFix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { RefObject } from "react";

import { CompatScroller } from "../components/CompatScroller";

/**
* Inverted lists only need a mouse-wheel direction fix on web. No-op native.
* See useInvertedWheelFix.web.ts.
*/
export function useInvertedWheelFix(
scrollViewRef: RefObject<CompatScroller>,
inverted: boolean | null | undefined,
horizontal: boolean | null | undefined
): void {}
38 changes: 38 additions & 0 deletions src/recyclerview/hooks/useInvertedWheelFix.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RefObject, useEffect } from "react";

import { CompatScroller } from "../components/CompatScroller";

/**
* On web an inverted list is flipped with transform: scaleY(-1) (scaleX(-1)
* when horizontal). The native mouse wheel is not flipped, so it scrolls the
* opposite way. Re-invert the wheel delta on the scroll node. Setting scrollTop
* fires the scroll event, so virtualization keeps working.
*/
export function useInvertedWheelFix(
scrollViewRef: RefObject<CompatScroller>,
inverted: boolean | null | undefined,
horizontal: boolean | null | undefined
): void {
useEffect(() => {
if (!inverted) return;
const scrollNode = scrollViewRef.current?.getScrollableNode?.() as
| HTMLElement
| undefined;
if (!scrollNode?.addEventListener) return;

const onWheel = (event: WheelEvent) => {
if (horizontal) {
if (!event.deltaX) return;
event.preventDefault();
scrollNode.scrollLeft -= event.deltaX;
} else {
if (!event.deltaY) return;
event.preventDefault();
scrollNode.scrollTop -= event.deltaY;
}
};

scrollNode.addEventListener("wheel", onWheel, { passive: false });
return () => scrollNode.removeEventListener("wheel", onWheel);
}, [scrollViewRef, inverted, horizontal]);
}
24 changes: 23 additions & 1 deletion src/recyclerview/utils/measureLayout.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ export function measureParentSize(view: Element): Size {
};
}

/**
* Detects whether an element is flipped vertically via a scaleY(-1) transform
* (used by inverted lists on web). Reads the computed transform matrix.
*/
function isVerticallyFlipped(element: Element): boolean {
const transform = getComputedStyle(element as HTMLElement).transform;
if (!transform || transform === "none") return false;
const match = transform.match(/matrix(3d)?\(([^)]+)\)/);
if (!match) return false;
const values = match[2].split(",").map((value) => parseFloat(value));
// matrix(a,b,c,d,e,f) -> scaleY = d (index 3); matrix3d -> m22 (index 5)
const scaleY = match[1] ? values[5] : values[3];
return scaleY < 0;
}

/**
* Measures the layout of child container of RecyclerView
*/
Expand All @@ -70,9 +85,16 @@ export function measureFirstChildLayout(
// Get scroll offsets for child container (max 3 parents)
const scrollOffsets = getScrollOffsets(childContainerView, parentView);

// When inverted on web the outer container is flipped with scaleY(-1), so
// getBoundingClientRect returns mirrored coordinates. Measure from the bottom
// edge in that case so firstItemOffset stays ~0 (matches non-inverted).
const y = isVerticallyFlipped(parentView)
? parentRect.bottom - childRect.bottom + scrollOffsets.scrollY
: childRect.top - parentRect.top + scrollOffsets.scrollY;

return {
x: childRect.left - parentRect.left + scrollOffsets.scrollX,
y: childRect.top - parentRect.top + scrollOffsets.scrollY,
y,
width: roundOffPixel(childRect.width),
height: roundOffPixel(childRect.height),
};
Expand Down
Loading