diff --git a/packages/react-native-web-examples/pages/_document.js b/packages/react-native-web-examples/pages/_document.js
index 82d7ad731f..864f08fc93 100644
--- a/packages/react-native-web-examples/pages/_document.js
+++ b/packages/react-native-web-examples/pages/_document.js
@@ -1,10 +1,18 @@
import { Html, Head, Main, NextScript } from 'next/document';
import { AppRegistry } from 'react-native';
+const style = `
+body,html,#__next{height:100%}
+body{overflow:hidden}
+#__next{display:flex;flex-direction:column}
+`.trim();
+
export default function MyDocument() {
return (
-
diff --git a/packages/react-native-web/src/exports/VirtualizedList/__tests__/index-test.js b/packages/react-native-web/src/exports/VirtualizedList/__tests__/index-test.js
new file mode 100644
index 0000000000..863aa1b20b
--- /dev/null
+++ b/packages/react-native-web/src/exports/VirtualizedList/__tests__/index-test.js
@@ -0,0 +1,109 @@
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+import VirtualizedList from '../';
+
+function mockScrollMetrics(node, offset, length, horizontal = false) {
+ const clientLength = 100;
+ let current = offset;
+ Object.defineProperties(node, {
+ [horizontal ? 'clientWidth' : 'clientHeight']: {
+ configurable: true,
+ value: clientLength
+ },
+ [horizontal ? 'scrollWidth' : 'scrollHeight']: {
+ configurable: true,
+ value: length
+ },
+ [horizontal ? 'scrollLeft' : 'scrollTop']: {
+ configurable: true,
+ get: () => current,
+ set: (value) => {
+ current = Math.max(0, Math.min(value, length - clientLength));
+ }
+ }
+ });
+}
+
+function renderList({ horizontal = false, inverted = true } = {}) {
+ return render(
+ data[index]}
+ getItemCount={(data) => data.length}
+ getItemLayout={(data, index) => ({ length: 500, offset: 0, index })}
+ horizontal={horizontal}
+ inverted={inverted}
+ keyExtractor={String}
+ renderItem={() => }
+ testID="list"
+ />
+ );
+}
+
+function wheel(target, delta, horizontal = false) {
+ const event = new WheelEvent('wheel', {
+ bubbles: true,
+ cancelable: true,
+ [horizontal ? 'deltaX' : 'deltaY']: delta
+ });
+ fireEvent(target, event);
+ return event;
+}
+
+describe('components/VirtualizedList', () => {
+ describe('prop "inverted"', () => {
+ test('wheel over the list itself scrolls in the inverted direction', () => {
+ const { getByTestId } = renderList();
+ const list = getByTestId('list');
+ mockScrollMetrics(list, 200, 500);
+ expect(wheel(list, 20).defaultPrevented).toBe(true);
+ expect(list.scrollTop).toBe(180);
+ });
+
+ test('wheel over the list itself near the end does not oscillate', () => {
+ const { getByTestId } = renderList();
+ const list = getByTestId('list');
+ mockScrollMetrics(list, 390, 500);
+ expect(wheel(list, 20).defaultPrevented).toBe(true);
+ expect(list.scrollTop).toBe(370);
+ });
+
+ test('wheel over a nested scrollable scrolls it first, then the list', () => {
+ const { getByTestId } = renderList();
+ const list = getByTestId('list');
+ const nested = getByTestId('nested');
+ mockScrollMetrics(list, 200, 500);
+ mockScrollMetrics(nested, 195, 300);
+ expect(wheel(nested, 20).defaultPrevented).toBe(true);
+ expect(nested.scrollTop).toBe(200);
+ expect(list.scrollTop).toBe(185);
+ });
+
+ test('horizontal wheel over the list itself scrolls in the inverted direction', () => {
+ const { getByTestId } = renderList({ horizontal: true });
+ const list = getByTestId('list');
+ mockScrollMetrics(list, 200, 500, true);
+ expect(wheel(list, 20, true).defaultPrevented).toBe(true);
+ expect(list.scrollLeft).toBe(180);
+ });
+
+ test('wheel over a non-inverted list is left to the browser', () => {
+ const { getByTestId } = renderList({ inverted: false });
+ const list = getByTestId('list');
+ mockScrollMetrics(list, 200, 500);
+ expect(wheel(list, 20).defaultPrevented).toBe(false);
+ expect(list.scrollTop).toBe(200);
+ });
+
+ test('negative wheel over a nested scrollable scrolls it first, then the list', () => {
+ const { getByTestId } = renderList();
+ const list = getByTestId('list');
+ const nested = getByTestId('nested');
+ mockScrollMetrics(list, 200, 500);
+ mockScrollMetrics(nested, 5, 300);
+ expect(wheel(nested, -20).defaultPrevented).toBe(true);
+ expect(nested.scrollTop).toBe(0);
+ expect(list.scrollTop).toBe(215);
+ });
+ });
+});
diff --git a/packages/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/packages/react-native-web/src/vendor/react-native/VirtualizedList/index.js
index b7576d1250..d12e91c08e 100644
--- a/packages/react-native-web/src/vendor/react-native/VirtualizedList/index.js
+++ b/packages/react-native-web/src/vendor/react-native/VirtualizedList/index.js
@@ -457,7 +457,7 @@ class VirtualizedList extends StateSafePureComponent {
const scrollOffset = this.props.horizontal ? ev.target.scrollLeft : ev.target.scrollTop;
const scrollLength = this.props.horizontal ? ev.target.scrollWidth : ev.target.scrollHeight;
const clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight;
- const isEventTargetScrollable = scrollLength > clientLength;
+ const isEventTargetScrollable = ev.target !== ev.currentTarget && scrollLength > clientLength;
const delta = this.props.horizontal
? ev.deltaX || ev.wheelDeltaX
: ev.deltaY || ev.wheelDeltaY;