Skip to content

Commit

Permalink
Defer applying jump to scroll position in iOS Safari
Browse files Browse the repository at this point in the history
  • Loading branch information
inokawa committed Oct 7, 2023
1 parent 8bd37cf commit e00f66d
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 29 deletions.
8 changes: 4 additions & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ export default defineConfig({
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
{
name: "Mobile Safari",
use: { ...devices["iPhone 13"] },
},

/* Test against branded browsers. */
// {
Expand Down
9 changes: 8 additions & 1 deletion src/core/environment.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { once } from "./utils";

export const isBrowser = typeof window !== "undefined";

// The scroll position may be negative value in rtl direction.
//
// left right result
// -100 0 true spec compliant
// 0 100 false probably Chrome earlier than v85
// https://github.com/othree/jquery.rtl-scroll-type
export const hasNegativeOffsetInRtl = /*#__PURE__*/ once(
(scrollable: HTMLElement) => {
(scrollable: HTMLElement): boolean => {
const key = "scrollLeft";
const prev = scrollable[key];
scrollable[key] = 1;
Expand All @@ -17,3 +19,8 @@ export const hasNegativeOffsetInRtl = /*#__PURE__*/ once(
return isNegative;
}
);

// Currently, all browsers on iOS/iPadOS are WebKit.
export const isIOSWebKit = /*#__PURE__*/ once((): boolean => {
return isBrowser ? /iP(hone|od|ad)/.test(navigator.userAgent) : false;
});
49 changes: 42 additions & 7 deletions src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
updateCacheLength,
computeRange,
} from "./cache";
import { isIOSWebKit } from "./environment";
import type { CacheSnapshot, Writeable } from "./types";
import { abs, clamp, max, min } from "./utils";

Expand Down Expand Up @@ -103,12 +104,14 @@ export const createVirtualStore = (
let scrollOffset = 0;
let jumpCount = 0;
let jump: ScrollJump = 0;
let pendingJump: ScrollJump = 0;
let _scrollDirection: ScrollDirection = SCROLL_IDLE;
let _isShifting = false;
let _isManualScrolling = false;
let _resized = false;
let _prevRange: ItemsRange = [0, initialItemCount];

const shouldDeferJump = isIOSWebKit();
const subscribers = new Set<[number, Subscriber]>();
const getScrollSize = (): number =>
computeTotalSize(cache as Writeable<Cache>);
Expand All @@ -118,6 +121,14 @@ export const createVirtualStore = (
// Scroll offset may exceed min or max especially in Safari's elastic scrolling.
return clamp(value, 0, getScrollOffsetMax());
};
const applyJump = (j: ScrollJump) => {
if (shouldDeferJump && _scrollDirection !== SCROLL_IDLE) {
pendingJump += j;
} else {
jump += j;
jumpCount++;
}
};
const updateScrollDirection = (dir: ScrollDirection): boolean => {
const prev = _scrollDirection;
_scrollDirection = dir;
Expand All @@ -133,7 +144,7 @@ export const createVirtualStore = (
const [prevStartIndex, prevEndIndex] = _prevRange;
const [start, end] = computeRange(
cache as Writeable<Cache>,
scrollOffset,
scrollOffset + pendingJump,
prevStartIndex,
viewportSize
);
Expand All @@ -155,7 +166,8 @@ export const createVirtualStore = (
return hasUnmeasuredItemsInRange(cache, startIndex, endIndex);
},
_getItemOffset(index) {
const offset = computeStartOffset(cache as Writeable<Cache>, index);
const offset =
computeStartOffset(cache as Writeable<Cache>, index) - pendingJump;
if (isReverse) {
return offset + max(0, viewportSize - getScrollSize());
}
Expand Down Expand Up @@ -196,6 +208,7 @@ export const createVirtualStore = (
};
},
_update(type, payload): void {
let shouldFlushJump: boolean | undefined;
let shouldSync: boolean | undefined;
let mutated = 0;

Expand Down Expand Up @@ -237,8 +250,7 @@ export const createVirtualStore = (
// Do nothing
}
if (diff) {
jump = diff;
jumpCount++;
applyJump(diff);
mutated += UPDATE_JUMP;
}

Expand Down Expand Up @@ -281,9 +293,8 @@ export const createVirtualStore = (
true
);
const diff = isRemove ? -min(shift, distanceToEnd) : shift;
jump += diff;
applyJump(diff);
scrollOffset = clampScrollOffset(scrollOffset + diff);
jumpCount++;

mutated = UPDATE_SCROLL + UPDATE_JUMP;
_isShifting = true;
Expand All @@ -301,6 +312,7 @@ export const createVirtualStore = (

if (type === ACTION_SCROLL) {
const delta = payload - scrollOffset;
const distance = abs(delta);
// Scrolling after resizing will be caused by jump compensation
const isJustResized = _resized;
_resized = false;
Expand All @@ -313,15 +325,28 @@ export const createVirtualStore = (
!_isManualScrolling
) {
if (updateScrollDirection(delta < 0 ? SCROLL_UP : SCROLL_DOWN)) {
// shouldFlushJump = true;
mutated += UPDATE_SCROLL_DIRECTION;
}
}

if (distance > viewportSize * 2) {
// When scrolled a lot, we would not recognize the amount of jump so we can discard them.
// TODO reset pending
// jump = 0;
} else if (
payload - max(jump, 0) <= 0 ||
payload - min(jump, 0) >= getScrollOffsetMax()
) {
// Flush if almost reached to start or end
shouldFlushJump = true;
}

// Ignore manual scroll because it may be called in useEffect/useLayoutEffect and cause the warn below.
// Warning: flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.
//
// Update synchronously if scrolled a lot
shouldSync = abs(delta) > viewportSize;
shouldSync = distance > viewportSize;

mutated += UPDATE_SCROLL_WITH_EVENT;

Expand All @@ -336,6 +361,7 @@ export const createVirtualStore = (
}
case ACTION_SCROLL_END: {
if (updateScrollDirection(SCROLL_IDLE)) {
shouldFlushJump = true;
mutated = UPDATE_SCROLL_DIRECTION;
}
_isShifting = _isManualScrolling = false;
Expand All @@ -348,6 +374,15 @@ export const createVirtualStore = (
}

if (mutated) {
if (shouldFlushJump && pendingJump) {
_resized = true;
_isShifting = true;
jump += pendingJump;
pendingJump = 0;
jumpCount++;
mutated += UPDATE_JUMP;
}

const update = () => {
subscribers.forEach(([target, cb]) => {
// Early return to skip React's computation
Expand Down
11 changes: 8 additions & 3 deletions src/react/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
ReactElement,
ReactNode,
} from "react";
import { UPDATE_SIZE, VirtualStore } from "../core/store";
import {
UPDATE_JUMP,
UPDATE_SCROLL,
UPDATE_SIZE,
VirtualStore,
} from "../core/store";
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
import { useSelector } from "./useSelector";
import { ListResizer } from "../core/resizer";
Expand Down Expand Up @@ -49,13 +54,13 @@ export const ListItem = memo(
const offset = useSelector(
store,
() => store._getItemOffset(index),
UPDATE_SIZE,
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP,
true
);
const hide = useSelector(
store,
() => store._isUnmeasuredItem(index),
UPDATE_SIZE,
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP,
true
);

Expand Down
16 changes: 8 additions & 8 deletions src/react/VGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,37 +83,37 @@ const Cell = memo(
const top = useSelector(
vStore,
() => vStore._getItemOffset(rowIndex),
UPDATE_SIZE,
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP,
true
);
const left = useSelector(
hStore,
() => hStore._getItemOffset(colIndex),
UPDATE_SIZE,
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP,
true
);
const vHide = useSelector(
vStore,
() => vStore._isUnmeasuredItem(rowIndex),
UPDATE_SIZE,
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP,
true
);
const hHide = useSelector(
hStore,
() => hStore._isUnmeasuredItem(colIndex),
UPDATE_SIZE,
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP,
true
);
const height = useSelector(
vStore,
() => vStore._getItemSize(rowIndex),
UPDATE_SIZE,
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP,
true
);
const width = useSelector(
hStore,
() => hStore._getItemSize(colIndex),
UPDATE_SIZE,
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP,
true
);

Expand Down Expand Up @@ -314,12 +314,12 @@ export const VGrid = forwardRef<VGridHandle, VGridProps>(
const [startRowIndex, endRowIndex] = useSelector(
vStore,
vStore._getRange,
UPDATE_SCROLL + UPDATE_SIZE
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP
);
const [startColIndex, endColIndex] = useSelector(
hStore,
hStore._getRange,
UPDATE_SCROLL + UPDATE_SIZE
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP
);
const vScrollDirection = useSelector(
vStore,
Expand Down
13 changes: 10 additions & 3 deletions src/react/VList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,21 @@ export const VList = forwardRef<VListHandle, VListProps>(
const [startIndex, endIndex] = useSelector(
store,
store._getRange,
UPDATE_SCROLL + UPDATE_SIZE
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP,
true
);
const scrollDirection = useSelector(
store,
store._getScrollDirection,
UPDATE_SCROLL_DIRECTION
UPDATE_SCROLL_DIRECTION,
true
);
const jumpCount = useSelector(
store,
store._getJumpCount,
UPDATE_JUMP,
true
);
const jumpCount = useSelector(store, store._getJumpCount, UPDATE_JUMP);
const scrollSize = useSelector(
store,
store._getCorrectedScrollSize,
Expand Down
2 changes: 1 addition & 1 deletion src/react/WVList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export const WVList = forwardRef<WVListHandle, WVListProps>(
const [startIndex, endIndex] = useSelector(
store,
store._getRange,
UPDATE_SCROLL + UPDATE_SIZE
UPDATE_SCROLL + UPDATE_SIZE + UPDATE_JUMP
);
const scrollDirection = useSelector(
store,
Expand Down
4 changes: 2 additions & 2 deletions src/react/useIsomorphicLayoutEffect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useLayoutEffect } from "react";
import { isBrowser } from "../core/environment";

// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
export const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
export const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;

0 comments on commit e00f66d

Please sign in to comment.