@oxyhq/bloom 0.6.22 → 0.6.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/scroll/index.web.js +120 -27
- package/lib/commonjs/scroll/index.web.js.map +1 -1
- package/lib/commonjs/scroll/scrollable.web.js +8 -3
- package/lib/commonjs/scroll/scrollable.web.js.map +1 -1
- package/lib/module/scroll/index.web.js +118 -25
- package/lib/module/scroll/index.web.js.map +1 -1
- package/lib/module/scroll/scrollable.web.js +8 -3
- package/lib/module/scroll/scrollable.web.js.map +1 -1
- package/lib/typescript/commonjs/scroll/index.web.d.ts +9 -5
- package/lib/typescript/commonjs/scroll/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/scrollable.web.d.ts +14 -2
- package/lib/typescript/commonjs/scroll/scrollable.web.d.ts.map +1 -1
- package/lib/typescript/module/scroll/index.web.d.ts +9 -5
- package/lib/typescript/module/scroll/index.web.d.ts.map +1 -1
- package/lib/typescript/module/scroll/scrollable.web.d.ts +14 -2
- package/lib/typescript/module/scroll/scrollable.web.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/scroll-web.test.tsx +334 -0
- package/src/scroll/index.web.tsx +121 -25
- package/src/scroll/scrollable.web.ts +19 -2
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.ScrollRestorationProvider = ScrollRestorationProvider;
|
|
7
7
|
exports.useScrollRestoration = useScrollRestoration;
|
|
8
8
|
var _react = require("react");
|
|
9
|
-
var
|
|
9
|
+
var _expoRouter = require("expo-router");
|
|
10
10
|
var _scrollableWeb = require("./scrollable.web.js");
|
|
11
11
|
var _store = require("./store.js");
|
|
12
12
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
@@ -14,20 +14,68 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
14
14
|
* Web variant of the scroll-restoration primitive.
|
|
15
15
|
*
|
|
16
16
|
* Mirrors the proven Bluesky pattern (`history.scrollRestoration = 'manual'`
|
|
17
|
-
* plus an in-memory `Map<routeKey, offset>`
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* plus an in-memory `Map<routeKey, offset>`) with two deliberate differences
|
|
18
|
+
* forced by Oxy's layouts and the behaviour of the (expo-router-wrapped)
|
|
19
|
+
* React Navigation web stack:
|
|
20
|
+
*
|
|
21
|
+
* 1. Bluesky restores the WINDOW scroller, whereas Oxy apps keep multi-column
|
|
22
|
+
* layouts whose feed scrolls an INNER container. So we restore the offset
|
|
23
|
+
* of a caller-registered scrollable (a ref to an element / RN scroll
|
|
24
|
+
* component, or the `'window'` sentinel), keyed by the active route.
|
|
25
|
+
*
|
|
26
|
+
* 2. The web stack HIDES the background screen on push. While
|
|
27
|
+
* hidden, the previous screen's scroll container collapses
|
|
28
|
+
* (`scrollHeight === clientHeight`) and the navigator forces its
|
|
29
|
+
* `scrollTop` to 0. The screen is NOT unmounted, so a virtualized list
|
|
30
|
+
* (e.g. FlashList) keeps its rows but re-lays them out over SEVERAL frames
|
|
31
|
+
* once the screen is re-shown. Two problems follow, both handled here:
|
|
32
|
+
*
|
|
33
|
+
* (a) A blur-time read of `scrollTop` returns the navigator's forced 0,
|
|
34
|
+
* not the user's real offset — saving it would clobber the good
|
|
35
|
+
* value. We therefore persist the last offset OBSERVED by the live
|
|
36
|
+
* scroll listener, never a fresh read taken at blur time.
|
|
37
|
+
*
|
|
38
|
+
* (b) A single-frame restore writes `scrollTop` while the list is still
|
|
39
|
+
* collapsed; the write is clamped to 0 and never re-applied once the
|
|
40
|
+
* content grows. We therefore re-apply the target offset across a
|
|
41
|
+
* bounded run of animation frames, stopping as soon as the write
|
|
42
|
+
* sticks (the content has grown tall enough) or a small frame cap is
|
|
43
|
+
* reached.
|
|
23
44
|
*
|
|
24
45
|
* Native bundlers use `./index.ts` (a no-op); web bundlers select this file via
|
|
25
46
|
* the `"browser"` export condition in `package.json`.
|
|
47
|
+
*
|
|
48
|
+
* The navigation hooks are imported from `expo-router` (which re-exports
|
|
49
|
+
* `useFocusEffect` and `useRoute` from its bundled React Navigation core) rather
|
|
50
|
+
* than from `@react-navigation/native` directly. Every Oxy app uses expo-router
|
|
51
|
+
* as its router, so it is always a DIRECT, top-level dependency that resolves
|
|
52
|
+
* cleanly under Bun's isolated linker — whereas `@react-navigation/native` is
|
|
53
|
+
* only a nested/transitive dependency of expo-router and would fail to resolve
|
|
54
|
+
* when bundling those apps.
|
|
26
55
|
*/
|
|
27
56
|
|
|
28
57
|
const ScrollOffsetContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
29
58
|
ScrollOffsetContext.displayName = 'BloomScrollOffsetContext';
|
|
30
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Maximum number of animation frames the focus restore will re-apply the saved
|
|
62
|
+
* offset before giving up. A virtualized list re-lays out its rows over a
|
|
63
|
+
* handful of frames after its screen is re-shown; ~30 frames (≈0.5s at 60fps)
|
|
64
|
+
* is comfortably longer than any observed relayout while staying short enough
|
|
65
|
+
* that the loop never lingers as a perceptible cost. The loop normally exits
|
|
66
|
+
* far earlier — as soon as the write sticks.
|
|
67
|
+
*/
|
|
68
|
+
const RESTORE_FRAME_CAP = 30;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tolerance (in CSS pixels) for considering a restore "stuck". After writing
|
|
72
|
+
* `element.scrollTop = target`, the browser may clamp it to the current
|
|
73
|
+
* `scrollHeight - clientHeight`; if the resulting offset is within this many
|
|
74
|
+
* pixels of the target we treat the restore as complete. Sub-pixel rounding and
|
|
75
|
+
* fractional device-pixel ratios make an exact equality check unreliable.
|
|
76
|
+
*/
|
|
77
|
+
const RESTORE_STICK_TOLERANCE_PX = 2;
|
|
78
|
+
|
|
31
79
|
/**
|
|
32
80
|
* Switch the browser to manual scroll restoration exactly once per document.
|
|
33
81
|
*
|
|
@@ -67,15 +115,19 @@ function useScrollOffsetStore() {
|
|
|
67
115
|
* multiple scrollables).
|
|
68
116
|
*
|
|
69
117
|
* Behaviour (web):
|
|
70
|
-
* - On every scroll while the screen is focused, the current offset is
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
118
|
+
* - On every scroll while the screen is focused, the current offset is recorded
|
|
119
|
+
* in memory and persisted. This live stream of saves is the source of truth.
|
|
120
|
+
* - On focus, the saved offset is re-applied across a bounded run of animation
|
|
121
|
+
* frames, stopping as soon as the write sticks (the list has re-rendered its
|
|
122
|
+
* rows and grown tall enough) or {@link RESTORE_FRAME_CAP} is reached. A
|
|
123
|
+
* saved offset of 0 is a no-op (nothing to restore).
|
|
124
|
+
* - On blur, the LAST OBSERVED offset is persisted as a final safety net — not
|
|
125
|
+
* a fresh `scrollTop` read, which the navigator may already have forced to 0
|
|
126
|
+
* while collapsing the hidden screen.
|
|
75
127
|
*/
|
|
76
128
|
function useScrollRestoration(target, options) {
|
|
77
129
|
const store = useScrollOffsetStore();
|
|
78
|
-
const route = (0,
|
|
130
|
+
const route = (0, _expoRouter.useRoute)();
|
|
79
131
|
const subKey = options?.key;
|
|
80
132
|
const enabled = options?.enabled ?? true;
|
|
81
133
|
const scrollKey = (0, _store.deriveScrollKey)(route.key, subKey);
|
|
@@ -88,35 +140,76 @@ function useScrollRestoration(target, options) {
|
|
|
88
140
|
enabledRef.current = enabled;
|
|
89
141
|
const scrollKeyRef = (0, _react.useRef)(scrollKey);
|
|
90
142
|
scrollKeyRef.current = scrollKey;
|
|
91
|
-
(0,
|
|
143
|
+
(0, _expoRouter.useFocusEffect)(
|
|
92
144
|
// The effect identity is intentionally stable across renders: it reads all
|
|
93
|
-
// varying inputs from refs.
|
|
145
|
+
// varying inputs from refs. expo-router's `useFocusEffect` re-runs it on
|
|
146
|
+
// each focus.
|
|
94
147
|
(0, _react.useCallback)(() => {
|
|
95
148
|
const key = scrollKeyRef.current;
|
|
96
149
|
if (!enabledRef.current || key === null) return undefined;
|
|
97
150
|
const scroller = (0, _scrollableWeb.createScroller)(targetRef.current);
|
|
98
151
|
const element = targetRef.current === 'window' ? typeof window !== 'undefined' ? window : null : resolveScrollEventTarget(targetRef.current);
|
|
152
|
+
|
|
153
|
+
// The last offset the live scroll listener observed for this focus
|
|
154
|
+
// session. This — not a blur-time `getOffset()` — is what we persist on
|
|
155
|
+
// blur, because by blur time the navigator may have collapsed the
|
|
156
|
+
// hidden screen and forced its `scrollTop` to 0 (bug A). `null` means
|
|
157
|
+
// the user never scrolled this session, so there is nothing newer to
|
|
158
|
+
// persist than what the scroll listener already saved live.
|
|
159
|
+
let lastObservedOffset = null;
|
|
99
160
|
const save = () => {
|
|
100
161
|
const currentKey = scrollKeyRef.current;
|
|
101
|
-
if (enabledRef.current
|
|
102
|
-
|
|
103
|
-
|
|
162
|
+
if (!enabledRef.current || currentKey === null) return;
|
|
163
|
+
const offset = scroller.getOffset();
|
|
164
|
+
// Ignore a spurious 0 produced by the navigator collapsing a hidden
|
|
165
|
+
// background screen: while collapsed the container cannot scroll, so
|
|
166
|
+
// its `scrollTop` is forced to 0. Persisting it would clobber the
|
|
167
|
+
// good offset recorded by earlier live saves (bug A). A genuine
|
|
168
|
+
// scroll-to-top keeps the container scrollable and is saved normally.
|
|
169
|
+
if (offset === 0 && !scroller.canScroll()) return;
|
|
170
|
+
lastObservedOffset = offset;
|
|
171
|
+
store.save(currentKey, offset);
|
|
104
172
|
};
|
|
105
173
|
|
|
106
|
-
// Restore
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
174
|
+
// Restore across a bounded run of frames. A freshly re-shown
|
|
175
|
+
// virtualized list re-lays out its rows over several frames, so a
|
|
176
|
+
// single write while it is still collapsed would be clamped to 0 and
|
|
177
|
+
// never re-applied (bug B). We re-apply each frame until the write
|
|
178
|
+
// sticks or the frame cap is hit.
|
|
179
|
+
const targetOffset = store.read(key);
|
|
180
|
+
let rafId = null;
|
|
181
|
+
if (targetOffset > 0 && typeof requestAnimationFrame !== 'undefined') {
|
|
182
|
+
let framesLeft = RESTORE_FRAME_CAP;
|
|
183
|
+
const applyOffset = () => {
|
|
184
|
+
rafId = null;
|
|
185
|
+
scroller.setOffset(targetOffset);
|
|
186
|
+
framesLeft -= 1;
|
|
187
|
+
// Stop once the write took effect (content grew tall enough) or we
|
|
188
|
+
// exhaust the frame budget. `getOffset` re-reads the clamped value.
|
|
189
|
+
const reached = Math.abs(scroller.getOffset() - targetOffset) <= RESTORE_STICK_TOLERANCE_PX;
|
|
190
|
+
if (!reached && framesLeft > 0) {
|
|
191
|
+
rafId = requestAnimationFrame(applyOffset);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
rafId = requestAnimationFrame(applyOffset);
|
|
195
|
+
}
|
|
111
196
|
element?.addEventListener('scroll', save, {
|
|
112
197
|
passive: true
|
|
113
198
|
});
|
|
114
199
|
return () => {
|
|
115
|
-
cancelAnimationFrame(
|
|
200
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
116
201
|
element?.removeEventListener('scroll', save);
|
|
117
|
-
// Final capture on blur
|
|
118
|
-
//
|
|
119
|
-
|
|
202
|
+
// Final capture on blur: persist the last offset the scroll listener
|
|
203
|
+
// OBSERVED, never a fresh read (which the navigator may have forced
|
|
204
|
+
// to 0 while collapsing the hidden screen). When the user never
|
|
205
|
+
// scrolled this session there is nothing newer to persist than the
|
|
206
|
+
// live saves already recorded.
|
|
207
|
+
if (lastObservedOffset !== null) {
|
|
208
|
+
const currentKey = scrollKeyRef.current;
|
|
209
|
+
if (enabledRef.current && currentKey !== null) {
|
|
210
|
+
store.save(currentKey, lastObservedOffset);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
120
213
|
};
|
|
121
214
|
}, [store]));
|
|
122
215
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","require","
|
|
1
|
+
{"version":3,"names":["_react","require","_expoRouter","_scrollableWeb","_store","_jsxRuntime","ScrollOffsetContext","createContext","displayName","RESTORE_FRAME_CAP","RESTORE_STICK_TOLERANCE_PX","history","scrollRestoration","ScrollRestorationProvider","children","store","useMemo","ScrollOffsetStore","jsx","Provider","value","useScrollOffsetStore","useContext","Error","useScrollRestoration","target","options","route","useRoute","subKey","key","enabled","scrollKey","deriveScrollKey","targetRef","useRef","current","enabledRef","scrollKeyRef","useFocusEffect","useCallback","undefined","scroller","createScroller","element","window","resolveScrollEventTarget","lastObservedOffset","save","currentKey","offset","getOffset","canScroll","targetOffset","read","rafId","requestAnimationFrame","framesLeft","applyOffset","setOffset","reached","Math","abs","addEventListener","passive","cancelAnimationFrame","removeEventListener","EventTarget","handle","getScrollableNode","node"],"sourceRoot":"../../../src","sources":["scroll/index.web.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.createScroller = createScroller;
|
|
7
7
|
/**
|
|
8
8
|
* A normalized read/write interface over whatever the caller registered, so
|
|
9
|
-
* the hook does not branch on target shape.
|
|
10
|
-
* underlying element is not yet (or no longer) attached.
|
|
9
|
+
* the hook does not branch on target shape. All methods are safe no-ops when
|
|
10
|
+
* the underlying element is not yet (or no longer) attached.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
function isElement(value) {
|
|
@@ -47,7 +47,8 @@ function createScroller(target) {
|
|
|
47
47
|
getOffset: () => typeof window === 'undefined' ? 0 : window.scrollY,
|
|
48
48
|
setOffset: offset => {
|
|
49
49
|
if (typeof window !== 'undefined') window.scrollTo(0, offset);
|
|
50
|
-
}
|
|
50
|
+
},
|
|
51
|
+
canScroll: () => true
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
return {
|
|
@@ -58,6 +59,10 @@ function createScroller(target) {
|
|
|
58
59
|
setOffset: offset => {
|
|
59
60
|
const element = resolveElement(target);
|
|
60
61
|
if (element) element.scrollTop = offset;
|
|
62
|
+
},
|
|
63
|
+
canScroll: () => {
|
|
64
|
+
const element = resolveElement(target);
|
|
65
|
+
return element ? element.scrollHeight > element.clientHeight : false;
|
|
61
66
|
}
|
|
62
67
|
};
|
|
63
68
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["isElement","value","HTMLElement","hasGetScrollableNode","getScrollableNode","resolveElement","target","current","node","createScroller","getOffset","window","scrollY","setOffset","offset","scrollTo","element","scrollTop"],"sourceRoot":"../../../src","sources":["scroll/scrollable.web.ts"],"mappings":";;;;;;AAEA;AACA;AACA;AACA;AACA;;
|
|
1
|
+
{"version":3,"names":["isElement","value","HTMLElement","hasGetScrollableNode","getScrollableNode","resolveElement","target","current","node","createScroller","getOffset","window","scrollY","setOffset","offset","scrollTo","canScroll","element","scrollTop","scrollHeight","clientHeight"],"sourceRoot":"../../../src","sources":["scroll/scrollable.web.ts"],"mappings":";;;;;;AAEA;AACA;AACA;AACA;AACA;;AAkBA,SAASA,SAASA,CAACC,KAAc,EAAwB;EACvD,OAAO,OAAOC,WAAW,KAAK,WAAW,IAAID,KAAK,YAAYC,WAAW;AAC3E;AAEA,SAASC,oBAAoBA,CAC3BF,KAAc,EACkD;EAChE,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,OAAQA,KAAK,CAAsBG,iBAAiB,KAAK,UAAU;AAEvE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,cAAcA,CAACC,MAA+B,EAAsB;EAC3E,IAAIA,MAAM,KAAK,QAAQ,EAAE,OAAO,IAAI;EAEpC,MAAMC,OAAO,GAAID,MAAM,CAA0BC,OAAO;EACxD,IAAIA,OAAO,IAAI,IAAI,EAAE,OAAO,IAAI;EAEhC,IAAIP,SAAS,CAACO,OAAO,CAAC,EAAE,OAAOA,OAAO;EAEtC,IAAIJ,oBAAoB,CAACI,OAAO,CAAC,EAAE;IACjC,MAAMC,IAAI,GAAGD,OAAO,CAACH,iBAAiB,CAAC,CAAC;IACxC,IAAIJ,SAAS,CAACQ,IAAI,CAAC,EAAE,OAAOA,IAAI;EAClC;EAEA,OAAO,IAAI;AACb;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASC,cAAcA,CAACH,MAA+B,EAAoB;EAChF,IAAIA,MAAM,KAAK,QAAQ,EAAE;IACvB,OAAO;MACLI,SAAS,EAAEA,CAAA,KAAO,OAAOC,MAAM,KAAK,WAAW,GAAG,CAAC,GAAGA,MAAM,CAACC,OAAQ;MACrEC,SAAS,EAAGC,MAAM,IAAK;QACrB,IAAI,OAAOH,MAAM,KAAK,WAAW,EAAEA,MAAM,CAACI,QAAQ,CAAC,CAAC,EAAED,MAAM,CAAC;MAC/D,CAAC;MACDE,SAAS,EAAEA,CAAA,KAAM;IACnB,CAAC;EACH;EAEA,OAAO;IACLN,SAAS,EAAEA,CAAA,KAAM;MACf,MAAMO,OAAO,GAAGZ,cAAc,CAACC,MAAM,CAAC;MACtC,OAAOW,OAAO,GAAGA,OAAO,CAACC,SAAS,GAAG,CAAC;IACxC,CAAC;IACDL,SAAS,EAAGC,MAAM,IAAK;MACrB,MAAMG,OAAO,GAAGZ,cAAc,CAACC,MAAM,CAAC;MACtC,IAAIW,OAAO,EAAEA,OAAO,CAACC,SAAS,GAAGJ,MAAM;IACzC,CAAC;IACDE,SAAS,EAAEA,CAAA,KAAM;MACf,MAAMC,OAAO,GAAGZ,cAAc,CAACC,MAAM,CAAC;MACtC,OAAOW,OAAO,GAAGA,OAAO,CAACE,YAAY,GAAGF,OAAO,CAACG,YAAY,GAAG,KAAK;IACtE;EACF,CAAC;AACH","ignoreList":[]}
|
|
@@ -4,24 +4,72 @@
|
|
|
4
4
|
* Web variant of the scroll-restoration primitive.
|
|
5
5
|
*
|
|
6
6
|
* Mirrors the proven Bluesky pattern (`history.scrollRestoration = 'manual'`
|
|
7
|
-
* plus an in-memory `Map<routeKey, offset>`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* plus an in-memory `Map<routeKey, offset>`) with two deliberate differences
|
|
8
|
+
* forced by Oxy's layouts and the behaviour of the (expo-router-wrapped)
|
|
9
|
+
* React Navigation web stack:
|
|
10
|
+
*
|
|
11
|
+
* 1. Bluesky restores the WINDOW scroller, whereas Oxy apps keep multi-column
|
|
12
|
+
* layouts whose feed scrolls an INNER container. So we restore the offset
|
|
13
|
+
* of a caller-registered scrollable (a ref to an element / RN scroll
|
|
14
|
+
* component, or the `'window'` sentinel), keyed by the active route.
|
|
15
|
+
*
|
|
16
|
+
* 2. The web stack HIDES the background screen on push. While
|
|
17
|
+
* hidden, the previous screen's scroll container collapses
|
|
18
|
+
* (`scrollHeight === clientHeight`) and the navigator forces its
|
|
19
|
+
* `scrollTop` to 0. The screen is NOT unmounted, so a virtualized list
|
|
20
|
+
* (e.g. FlashList) keeps its rows but re-lays them out over SEVERAL frames
|
|
21
|
+
* once the screen is re-shown. Two problems follow, both handled here:
|
|
22
|
+
*
|
|
23
|
+
* (a) A blur-time read of `scrollTop` returns the navigator's forced 0,
|
|
24
|
+
* not the user's real offset — saving it would clobber the good
|
|
25
|
+
* value. We therefore persist the last offset OBSERVED by the live
|
|
26
|
+
* scroll listener, never a fresh read taken at blur time.
|
|
27
|
+
*
|
|
28
|
+
* (b) A single-frame restore writes `scrollTop` while the list is still
|
|
29
|
+
* collapsed; the write is clamped to 0 and never re-applied once the
|
|
30
|
+
* content grows. We therefore re-apply the target offset across a
|
|
31
|
+
* bounded run of animation frames, stopping as soon as the write
|
|
32
|
+
* sticks (the content has grown tall enough) or a small frame cap is
|
|
33
|
+
* reached.
|
|
13
34
|
*
|
|
14
35
|
* Native bundlers use `./index.ts` (a no-op); web bundlers select this file via
|
|
15
36
|
* the `"browser"` export condition in `package.json`.
|
|
37
|
+
*
|
|
38
|
+
* The navigation hooks are imported from `expo-router` (which re-exports
|
|
39
|
+
* `useFocusEffect` and `useRoute` from its bundled React Navigation core) rather
|
|
40
|
+
* than from `@react-navigation/native` directly. Every Oxy app uses expo-router
|
|
41
|
+
* as its router, so it is always a DIRECT, top-level dependency that resolves
|
|
42
|
+
* cleanly under Bun's isolated linker — whereas `@react-navigation/native` is
|
|
43
|
+
* only a nested/transitive dependency of expo-router and would fail to resolve
|
|
44
|
+
* when bundling those apps.
|
|
16
45
|
*/
|
|
17
46
|
import { createContext, useCallback, useContext, useMemo, useRef } from 'react';
|
|
18
|
-
import { useFocusEffect, useRoute } from '
|
|
47
|
+
import { useFocusEffect, useRoute } from 'expo-router';
|
|
19
48
|
import { createScroller } from "./scrollable.web.js";
|
|
20
49
|
import { ScrollOffsetStore, deriveScrollKey } from "./store.js";
|
|
21
50
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
22
51
|
const ScrollOffsetContext = /*#__PURE__*/createContext(null);
|
|
23
52
|
ScrollOffsetContext.displayName = 'BloomScrollOffsetContext';
|
|
24
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Maximum number of animation frames the focus restore will re-apply the saved
|
|
56
|
+
* offset before giving up. A virtualized list re-lays out its rows over a
|
|
57
|
+
* handful of frames after its screen is re-shown; ~30 frames (≈0.5s at 60fps)
|
|
58
|
+
* is comfortably longer than any observed relayout while staying short enough
|
|
59
|
+
* that the loop never lingers as a perceptible cost. The loop normally exits
|
|
60
|
+
* far earlier — as soon as the write sticks.
|
|
61
|
+
*/
|
|
62
|
+
const RESTORE_FRAME_CAP = 30;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tolerance (in CSS pixels) for considering a restore "stuck". After writing
|
|
66
|
+
* `element.scrollTop = target`, the browser may clamp it to the current
|
|
67
|
+
* `scrollHeight - clientHeight`; if the resulting offset is within this many
|
|
68
|
+
* pixels of the target we treat the restore as complete. Sub-pixel rounding and
|
|
69
|
+
* fractional device-pixel ratios make an exact equality check unreliable.
|
|
70
|
+
*/
|
|
71
|
+
const RESTORE_STICK_TOLERANCE_PX = 2;
|
|
72
|
+
|
|
25
73
|
/**
|
|
26
74
|
* Switch the browser to manual scroll restoration exactly once per document.
|
|
27
75
|
*
|
|
@@ -61,11 +109,15 @@ function useScrollOffsetStore() {
|
|
|
61
109
|
* multiple scrollables).
|
|
62
110
|
*
|
|
63
111
|
* Behaviour (web):
|
|
64
|
-
* - On every scroll while the screen is focused, the current offset is
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
112
|
+
* - On every scroll while the screen is focused, the current offset is recorded
|
|
113
|
+
* in memory and persisted. This live stream of saves is the source of truth.
|
|
114
|
+
* - On focus, the saved offset is re-applied across a bounded run of animation
|
|
115
|
+
* frames, stopping as soon as the write sticks (the list has re-rendered its
|
|
116
|
+
* rows and grown tall enough) or {@link RESTORE_FRAME_CAP} is reached. A
|
|
117
|
+
* saved offset of 0 is a no-op (nothing to restore).
|
|
118
|
+
* - On blur, the LAST OBSERVED offset is persisted as a final safety net — not
|
|
119
|
+
* a fresh `scrollTop` read, which the navigator may already have forced to 0
|
|
120
|
+
* while collapsing the hidden screen.
|
|
69
121
|
*/
|
|
70
122
|
export function useScrollRestoration(target, options) {
|
|
71
123
|
const store = useScrollOffsetStore();
|
|
@@ -84,33 +136,74 @@ export function useScrollRestoration(target, options) {
|
|
|
84
136
|
scrollKeyRef.current = scrollKey;
|
|
85
137
|
useFocusEffect(
|
|
86
138
|
// The effect identity is intentionally stable across renders: it reads all
|
|
87
|
-
// varying inputs from refs.
|
|
139
|
+
// varying inputs from refs. expo-router's `useFocusEffect` re-runs it on
|
|
140
|
+
// each focus.
|
|
88
141
|
useCallback(() => {
|
|
89
142
|
const key = scrollKeyRef.current;
|
|
90
143
|
if (!enabledRef.current || key === null) return undefined;
|
|
91
144
|
const scroller = createScroller(targetRef.current);
|
|
92
145
|
const element = targetRef.current === 'window' ? typeof window !== 'undefined' ? window : null : resolveScrollEventTarget(targetRef.current);
|
|
146
|
+
|
|
147
|
+
// The last offset the live scroll listener observed for this focus
|
|
148
|
+
// session. This — not a blur-time `getOffset()` — is what we persist on
|
|
149
|
+
// blur, because by blur time the navigator may have collapsed the
|
|
150
|
+
// hidden screen and forced its `scrollTop` to 0 (bug A). `null` means
|
|
151
|
+
// the user never scrolled this session, so there is nothing newer to
|
|
152
|
+
// persist than what the scroll listener already saved live.
|
|
153
|
+
let lastObservedOffset = null;
|
|
93
154
|
const save = () => {
|
|
94
155
|
const currentKey = scrollKeyRef.current;
|
|
95
|
-
if (enabledRef.current
|
|
96
|
-
|
|
97
|
-
|
|
156
|
+
if (!enabledRef.current || currentKey === null) return;
|
|
157
|
+
const offset = scroller.getOffset();
|
|
158
|
+
// Ignore a spurious 0 produced by the navigator collapsing a hidden
|
|
159
|
+
// background screen: while collapsed the container cannot scroll, so
|
|
160
|
+
// its `scrollTop` is forced to 0. Persisting it would clobber the
|
|
161
|
+
// good offset recorded by earlier live saves (bug A). A genuine
|
|
162
|
+
// scroll-to-top keeps the container scrollable and is saved normally.
|
|
163
|
+
if (offset === 0 && !scroller.canScroll()) return;
|
|
164
|
+
lastObservedOffset = offset;
|
|
165
|
+
store.save(currentKey, offset);
|
|
98
166
|
};
|
|
99
167
|
|
|
100
|
-
// Restore
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
168
|
+
// Restore across a bounded run of frames. A freshly re-shown
|
|
169
|
+
// virtualized list re-lays out its rows over several frames, so a
|
|
170
|
+
// single write while it is still collapsed would be clamped to 0 and
|
|
171
|
+
// never re-applied (bug B). We re-apply each frame until the write
|
|
172
|
+
// sticks or the frame cap is hit.
|
|
173
|
+
const targetOffset = store.read(key);
|
|
174
|
+
let rafId = null;
|
|
175
|
+
if (targetOffset > 0 && typeof requestAnimationFrame !== 'undefined') {
|
|
176
|
+
let framesLeft = RESTORE_FRAME_CAP;
|
|
177
|
+
const applyOffset = () => {
|
|
178
|
+
rafId = null;
|
|
179
|
+
scroller.setOffset(targetOffset);
|
|
180
|
+
framesLeft -= 1;
|
|
181
|
+
// Stop once the write took effect (content grew tall enough) or we
|
|
182
|
+
// exhaust the frame budget. `getOffset` re-reads the clamped value.
|
|
183
|
+
const reached = Math.abs(scroller.getOffset() - targetOffset) <= RESTORE_STICK_TOLERANCE_PX;
|
|
184
|
+
if (!reached && framesLeft > 0) {
|
|
185
|
+
rafId = requestAnimationFrame(applyOffset);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
rafId = requestAnimationFrame(applyOffset);
|
|
189
|
+
}
|
|
105
190
|
element?.addEventListener('scroll', save, {
|
|
106
191
|
passive: true
|
|
107
192
|
});
|
|
108
193
|
return () => {
|
|
109
|
-
cancelAnimationFrame(
|
|
194
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
110
195
|
element?.removeEventListener('scroll', save);
|
|
111
|
-
// Final capture on blur
|
|
112
|
-
//
|
|
113
|
-
|
|
196
|
+
// Final capture on blur: persist the last offset the scroll listener
|
|
197
|
+
// OBSERVED, never a fresh read (which the navigator may have forced
|
|
198
|
+
// to 0 while collapsing the hidden screen). When the user never
|
|
199
|
+
// scrolled this session there is nothing newer to persist than the
|
|
200
|
+
// live saves already recorded.
|
|
201
|
+
if (lastObservedOffset !== null) {
|
|
202
|
+
const currentKey = scrollKeyRef.current;
|
|
203
|
+
if (enabledRef.current && currentKey !== null) {
|
|
204
|
+
store.save(currentKey, lastObservedOffset);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
114
207
|
};
|
|
115
208
|
}, [store]));
|
|
116
209
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["createContext","useCallback","useContext","useMemo","useRef","useFocusEffect","useRoute","createScroller","ScrollOffsetStore","deriveScrollKey","jsx","_jsx","ScrollOffsetContext","displayName","history","scrollRestoration","ScrollRestorationProvider","children","store","Provider","value","useScrollOffsetStore","Error","useScrollRestoration","target","options","route","subKey","key","enabled","scrollKey","targetRef","current","enabledRef","scrollKeyRef","undefined","scroller","element","window","resolveScrollEventTarget","save","currentKey","getOffset","
|
|
1
|
+
{"version":3,"names":["createContext","useCallback","useContext","useMemo","useRef","useFocusEffect","useRoute","createScroller","ScrollOffsetStore","deriveScrollKey","jsx","_jsx","ScrollOffsetContext","displayName","RESTORE_FRAME_CAP","RESTORE_STICK_TOLERANCE_PX","history","scrollRestoration","ScrollRestorationProvider","children","store","Provider","value","useScrollOffsetStore","Error","useScrollRestoration","target","options","route","subKey","key","enabled","scrollKey","targetRef","current","enabledRef","scrollKeyRef","undefined","scroller","element","window","resolveScrollEventTarget","lastObservedOffset","save","currentKey","offset","getOffset","canScroll","targetOffset","read","rafId","requestAnimationFrame","framesLeft","applyOffset","setOffset","reached","Math","abs","addEventListener","passive","cancelAnimationFrame","removeEventListener","EventTarget","handle","getScrollableNode","node"],"sourceRoot":"../../../src","sources":["scroll/index.web.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A normalized read/write interface over whatever the caller registered, so
|
|
5
|
-
* the hook does not branch on target shape.
|
|
6
|
-
* underlying element is not yet (or no longer) attached.
|
|
5
|
+
* the hook does not branch on target shape. All methods are safe no-ops when
|
|
6
|
+
* the underlying element is not yet (or no longer) attached.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
function isElement(value) {
|
|
@@ -43,7 +43,8 @@ export function createScroller(target) {
|
|
|
43
43
|
getOffset: () => typeof window === 'undefined' ? 0 : window.scrollY,
|
|
44
44
|
setOffset: offset => {
|
|
45
45
|
if (typeof window !== 'undefined') window.scrollTo(0, offset);
|
|
46
|
-
}
|
|
46
|
+
},
|
|
47
|
+
canScroll: () => true
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
return {
|
|
@@ -54,6 +55,10 @@ export function createScroller(target) {
|
|
|
54
55
|
setOffset: offset => {
|
|
55
56
|
const element = resolveElement(target);
|
|
56
57
|
if (element) element.scrollTop = offset;
|
|
58
|
+
},
|
|
59
|
+
canScroll: () => {
|
|
60
|
+
const element = resolveElement(target);
|
|
61
|
+
return element ? element.scrollHeight > element.clientHeight : false;
|
|
57
62
|
}
|
|
58
63
|
};
|
|
59
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["isElement","value","HTMLElement","hasGetScrollableNode","getScrollableNode","resolveElement","target","current","node","createScroller","getOffset","window","scrollY","setOffset","offset","scrollTo","element","scrollTop"],"sourceRoot":"../../../src","sources":["scroll/scrollable.web.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;;
|
|
1
|
+
{"version":3,"names":["isElement","value","HTMLElement","hasGetScrollableNode","getScrollableNode","resolveElement","target","current","node","createScroller","getOffset","window","scrollY","setOffset","offset","scrollTo","canScroll","element","scrollTop","scrollHeight","clientHeight"],"sourceRoot":"../../../src","sources":["scroll/scrollable.web.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;;AAkBA,SAASA,SAASA,CAACC,KAAc,EAAwB;EACvD,OAAO,OAAOC,WAAW,KAAK,WAAW,IAAID,KAAK,YAAYC,WAAW;AAC3E;AAEA,SAASC,oBAAoBA,CAC3BF,KAAc,EACkD;EAChE,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,OAAQA,KAAK,CAAsBG,iBAAiB,KAAK,UAAU;AAEvE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,cAAcA,CAACC,MAA+B,EAAsB;EAC3E,IAAIA,MAAM,KAAK,QAAQ,EAAE,OAAO,IAAI;EAEpC,MAAMC,OAAO,GAAID,MAAM,CAA0BC,OAAO;EACxD,IAAIA,OAAO,IAAI,IAAI,EAAE,OAAO,IAAI;EAEhC,IAAIP,SAAS,CAACO,OAAO,CAAC,EAAE,OAAOA,OAAO;EAEtC,IAAIJ,oBAAoB,CAACI,OAAO,CAAC,EAAE;IACjC,MAAMC,IAAI,GAAGD,OAAO,CAACH,iBAAiB,CAAC,CAAC;IACxC,IAAIJ,SAAS,CAACQ,IAAI,CAAC,EAAE,OAAOA,IAAI;EAClC;EAEA,OAAO,IAAI;AACb;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,cAAcA,CAACH,MAA+B,EAAoB;EAChF,IAAIA,MAAM,KAAK,QAAQ,EAAE;IACvB,OAAO;MACLI,SAAS,EAAEA,CAAA,KAAO,OAAOC,MAAM,KAAK,WAAW,GAAG,CAAC,GAAGA,MAAM,CAACC,OAAQ;MACrEC,SAAS,EAAGC,MAAM,IAAK;QACrB,IAAI,OAAOH,MAAM,KAAK,WAAW,EAAEA,MAAM,CAACI,QAAQ,CAAC,CAAC,EAAED,MAAM,CAAC;MAC/D,CAAC;MACDE,SAAS,EAAEA,CAAA,KAAM;IACnB,CAAC;EACH;EAEA,OAAO;IACLN,SAAS,EAAEA,CAAA,KAAM;MACf,MAAMO,OAAO,GAAGZ,cAAc,CAACC,MAAM,CAAC;MACtC,OAAOW,OAAO,GAAGA,OAAO,CAACC,SAAS,GAAG,CAAC;IACxC,CAAC;IACDL,SAAS,EAAGC,MAAM,IAAK;MACrB,MAAMG,OAAO,GAAGZ,cAAc,CAACC,MAAM,CAAC;MACtC,IAAIW,OAAO,EAAEA,OAAO,CAACC,SAAS,GAAGJ,MAAM;IACzC,CAAC;IACDE,SAAS,EAAEA,CAAA,KAAM;MACf,MAAMC,OAAO,GAAGZ,cAAc,CAACC,MAAM,CAAC;MACtC,OAAOW,OAAO,GAAGA,OAAO,CAACE,YAAY,GAAGF,OAAO,CAACG,YAAY,GAAG,KAAK;IACtE;EACF,CAAC;AACH","ignoreList":[]}
|
|
@@ -12,11 +12,15 @@ export declare function ScrollRestorationProvider({ children, }: ScrollRestorati
|
|
|
12
12
|
* multiple scrollables).
|
|
13
13
|
*
|
|
14
14
|
* Behaviour (web):
|
|
15
|
-
* - On every scroll while the screen is focused, the current offset is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* - On every scroll while the screen is focused, the current offset is recorded
|
|
16
|
+
* in memory and persisted. This live stream of saves is the source of truth.
|
|
17
|
+
* - On focus, the saved offset is re-applied across a bounded run of animation
|
|
18
|
+
* frames, stopping as soon as the write sticks (the list has re-rendered its
|
|
19
|
+
* rows and grown tall enough) or {@link RESTORE_FRAME_CAP} is reached. A
|
|
20
|
+
* saved offset of 0 is a no-op (nothing to restore).
|
|
21
|
+
* - On blur, the LAST OBSERVED offset is persisted as a final safety net — not
|
|
22
|
+
* a fresh `scrollTop` read, which the navigator may already have forced to 0
|
|
23
|
+
* while collapsing the hidden screen.
|
|
20
24
|
*/
|
|
21
25
|
export declare function useScrollRestoration(target: ScrollRestorationTarget, options?: UseScrollRestorationOptions): void;
|
|
22
26
|
//# sourceMappingURL=index.web.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.web.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.web.tsx"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EACV,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,gBAAgB,EAChB,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,SAAS,CAAC;AAmCjB;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,GACT,EAAE,8BAA8B,2CAOhC;AAYD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI,CAqGN"}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import type { ScrollRestorationTarget } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* A normalized read/write interface over whatever the caller registered, so
|
|
4
|
-
* the hook does not branch on target shape.
|
|
5
|
-
* underlying element is not yet (or no longer) attached.
|
|
4
|
+
* the hook does not branch on target shape. All methods are safe no-ops when
|
|
5
|
+
* the underlying element is not yet (or no longer) attached.
|
|
6
6
|
*/
|
|
7
7
|
export interface ResolvedScroller {
|
|
8
8
|
getOffset: () => number;
|
|
9
9
|
setOffset: (offset: number) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Whether the scroll container can currently hold a non-zero offset, i.e.
|
|
12
|
+
* its content is taller than its viewport (`scrollHeight > clientHeight`).
|
|
13
|
+
*
|
|
14
|
+
* React Navigation's web stack collapses a hidden background screen so its
|
|
15
|
+
* content height drops to the viewport height; while collapsed the container
|
|
16
|
+
* cannot be scrolled and its `scrollTop` is forced to 0. The hook uses this
|
|
17
|
+
* to ignore a spurious 0 read coming from a collapsed container rather than
|
|
18
|
+
* persisting it over a previously-saved good offset. The `'window'` scroller
|
|
19
|
+
* is never collapsed by the navigator, so it always reports `true`.
|
|
20
|
+
*/
|
|
21
|
+
canScroll: () => boolean;
|
|
10
22
|
}
|
|
11
23
|
/**
|
|
12
24
|
* Build a {@link ResolvedScroller} for a target. The window sentinel reads and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scrollable.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/scrollable.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"scrollable.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/scrollable.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;;;;;;;OAUG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;CAC1B;AAuCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,uBAAuB,GAAG,gBAAgB,CAyBhF"}
|
|
@@ -12,11 +12,15 @@ export declare function ScrollRestorationProvider({ children, }: ScrollRestorati
|
|
|
12
12
|
* multiple scrollables).
|
|
13
13
|
*
|
|
14
14
|
* Behaviour (web):
|
|
15
|
-
* - On every scroll while the screen is focused, the current offset is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* - On every scroll while the screen is focused, the current offset is recorded
|
|
16
|
+
* in memory and persisted. This live stream of saves is the source of truth.
|
|
17
|
+
* - On focus, the saved offset is re-applied across a bounded run of animation
|
|
18
|
+
* frames, stopping as soon as the write sticks (the list has re-rendered its
|
|
19
|
+
* rows and grown tall enough) or {@link RESTORE_FRAME_CAP} is reached. A
|
|
20
|
+
* saved offset of 0 is a no-op (nothing to restore).
|
|
21
|
+
* - On blur, the LAST OBSERVED offset is persisted as a final safety net — not
|
|
22
|
+
* a fresh `scrollTop` read, which the navigator may already have forced to 0
|
|
23
|
+
* while collapsing the hidden screen.
|
|
20
24
|
*/
|
|
21
25
|
export declare function useScrollRestoration(target: ScrollRestorationTarget, options?: UseScrollRestorationOptions): void;
|
|
22
26
|
//# sourceMappingURL=index.web.d.ts.map
|