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 10, 2023
1 parent f41b76f commit 828206d
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Total",
"path": "lib/index.mjs",
"import": "*",
"limit": "5.25 kB"
"limit": "5.5 kB"
},
{
"name": "VList",
Expand Down
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
5 changes: 5 additions & 0 deletions src/core/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ export const isRTLDocument = /*#__PURE__*/ once((): boolean => {
// TODO support SSR in rtl
return isBrowser ? computeStyle(document.body).direction === "rtl" : false;
});

// Currently, all browsers on iOS/iPadOS are WebKit, including WebView.
export const isIOSWebKit = /*#__PURE__*/ once((): boolean => {
return isBrowser ? /iP(hone|od|ad)/.test(navigator.userAgent) : false;
});
17 changes: 17 additions & 0 deletions src/core/scroller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,17 @@ export const createScroller = (
_initRoot(root) {
rootElement = root;

let touching = false;

const syncViewportToScrollPosition = () => {
store._update(ACTION_SCROLL, normalizeOffset(root[scrollToKey]));
};

const onScrollStopped = debounce(() => {
if (touching) {
onScrollStopped();
return;
}
// Check scroll position once just after scrolling stopped
syncViewportToScrollPosition();
store._update(ACTION_SCROLL_END);
Expand All @@ -154,12 +160,23 @@ export const createScroller = (

const onWheel = createOnWheel(store, isHorizontal, onScrollStopped);

const onTouchStart = () => {
touching = true;
};
const onTouchEnd = () => {
touching = false;
};

root.addEventListener("scroll", onScroll);
root.addEventListener("wheel", onWheel, { passive: true });
root.addEventListener("touchstart", onTouchStart, { passive: true });
root.addEventListener("touchend", onTouchEnd, { passive: true });

return () => {
root.removeEventListener("scroll", onScroll);
root.removeEventListener("wheel", onWheel);
root.removeEventListener("touchstart", onTouchStart);
root.removeEventListener("touchend", onTouchEnd);
onScrollStopped._cancel();
};
},
Expand Down
47 changes: 40 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 shouldFlushPendingJump: 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 @@ -317,11 +329,22 @@ export const createVirtualStore = (
}
}

if (
pendingJump &&
((_scrollDirection === SCROLL_UP &&
payload - max(pendingJump, 0) <= 0) ||
(_scrollDirection === SCROLL_DOWN &&
payload - min(pendingJump, 0) >= getScrollOffsetMax()))
) {
// Flush if almost reached to start or end
shouldFlushPendingJump = 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 +359,7 @@ export const createVirtualStore = (
}
case ACTION_SCROLL_END: {
if (updateScrollDirection(SCROLL_IDLE)) {
shouldFlushPendingJump = true;
mutated = UPDATE_SCROLL_DIRECTION;
}
_isShifting = _isManualScrolling = false;
Expand All @@ -348,6 +372,15 @@ export const createVirtualStore = (
}

if (mutated) {
if (shouldFlushPendingJump && 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 @@ -48,13 +53,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 @@ -82,37 +82,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 @@ -304,12 +304,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 @@ -226,14 +226,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

0 comments on commit 828206d

Please sign in to comment.