@oxyhq/bloom 0.78.0 → 0.79.1
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/README.md +3 -3
- package/lib/commonjs/provider/index.js +11 -7
- package/lib/commonjs/provider/index.js.map +1 -1
- package/lib/commonjs/scroll/context.js +96 -0
- package/lib/commonjs/scroll/context.js.map +1 -0
- package/lib/commonjs/scroll/expo-router/index.js +133 -0
- package/lib/commonjs/scroll/expo-router/index.js.map +1 -0
- package/lib/commonjs/scroll/index.js +85 -21
- package/lib/commonjs/scroll/index.js.map +1 -1
- package/lib/commonjs/scroll/index.web.js +184 -118
- package/lib/commonjs/scroll/index.web.js.map +1 -1
- package/lib/commonjs/scroll/scrollable.native.js +58 -0
- package/lib/commonjs/scroll/scrollable.native.js.map +1 -0
- package/lib/commonjs/scroll/store.js +47 -25
- package/lib/commonjs/scroll/store.js.map +1 -1
- package/lib/commonjs/theme/color-policy.js +11 -9
- package/lib/commonjs/theme/color-policy.js.map +1 -1
- package/lib/module/provider/index.js +10 -6
- package/lib/module/provider/index.js.map +1 -1
- package/lib/module/scroll/context.js +90 -0
- package/lib/module/scroll/context.js.map +1 -0
- package/lib/module/scroll/expo-router/index.js +128 -0
- package/lib/module/scroll/expo-router/index.js.map +1 -0
- package/lib/module/scroll/index.js +80 -21
- package/lib/module/scroll/index.js.map +1 -1
- package/lib/module/scroll/index.web.js +181 -119
- package/lib/module/scroll/index.web.js.map +1 -1
- package/lib/module/scroll/scrollable.native.js +54 -0
- package/lib/module/scroll/scrollable.native.js.map +1 -0
- package/lib/module/scroll/store.js +47 -25
- package/lib/module/scroll/store.js.map +1 -1
- package/lib/module/theme/color-policy.js +11 -9
- package/lib/module/theme/color-policy.js.map +1 -1
- package/lib/typescript/commonjs/provider/index.d.ts +7 -5
- package/lib/typescript/commonjs/provider/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/context.d.ts +14 -0
- package/lib/typescript/commonjs/scroll/context.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/expo-router/index.d.ts +7 -0
- package/lib/typescript/commonjs/scroll/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/index.d.ts +14 -23
- package/lib/typescript/commonjs/scroll/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/index.web.d.ts +17 -19
- package/lib/typescript/commonjs/scroll/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/scrollable.native.d.ts +23 -0
- package/lib/typescript/commonjs/scroll/scrollable.native.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/store.d.ts +41 -22
- package/lib/typescript/commonjs/scroll/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/types.d.ts +82 -12
- package/lib/typescript/commonjs/scroll/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/module/provider/index.d.ts +7 -5
- package/lib/typescript/module/provider/index.d.ts.map +1 -1
- package/lib/typescript/module/scroll/context.d.ts +14 -0
- package/lib/typescript/module/scroll/context.d.ts.map +1 -0
- package/lib/typescript/module/scroll/expo-router/index.d.ts +7 -0
- package/lib/typescript/module/scroll/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/module/scroll/index.d.ts +14 -23
- package/lib/typescript/module/scroll/index.d.ts.map +1 -1
- package/lib/typescript/module/scroll/index.web.d.ts +17 -19
- package/lib/typescript/module/scroll/index.web.d.ts.map +1 -1
- package/lib/typescript/module/scroll/scrollable.native.d.ts +23 -0
- package/lib/typescript/module/scroll/scrollable.native.d.ts.map +1 -0
- package/lib/typescript/module/scroll/store.d.ts +41 -22
- package/lib/typescript/module/scroll/store.d.ts.map +1 -1
- package/lib/typescript/module/scroll/types.d.ts +82 -12
- package/lib/typescript/module/scroll/types.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
- package/package.json +15 -1
- package/src/__tests__/BloomProvider.web.test.tsx +8 -15
- package/src/__tests__/optional-peer-imports.test.ts +2 -2
- package/src/__tests__/scroll-expo-router-adapter.test.tsx +164 -0
- package/src/__tests__/scroll-native.test.tsx +465 -0
- package/src/__tests__/scroll-store.test.ts +41 -17
- package/src/__tests__/scroll-web.test.tsx +547 -111
- package/src/provider/index.tsx +10 -7
- package/src/scroll/context.tsx +107 -0
- package/src/scroll/expo-router/index.ts +140 -0
- package/src/scroll/index.ts +92 -25
- package/src/scroll/index.web.tsx +219 -162
- package/src/scroll/scrollable.native.ts +64 -0
- package/src/scroll/store.ts +47 -25
- package/src/scroll/types.ts +82 -12
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +27 -27
- package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +55 -55
- package/src/theme/__tests__/policy-legibility.test.ts +21 -1
- package/src/theme/color-policy.ts +13 -9
- package/lib/commonjs/provider/scroll-provider.js +0 -13
- package/lib/commonjs/provider/scroll-provider.js.map +0 -1
- package/lib/commonjs/provider/scroll-provider.web.js +0 -13
- package/lib/commonjs/provider/scroll-provider.web.js.map +0 -1
- package/lib/module/provider/scroll-provider.js +0 -16
- package/lib/module/provider/scroll-provider.js.map +0 -1
- package/lib/module/provider/scroll-provider.web.js +0 -5
- package/lib/module/provider/scroll-provider.web.js.map +0 -1
- package/lib/typescript/commonjs/provider/scroll-provider.d.ts +0 -14
- package/lib/typescript/commonjs/provider/scroll-provider.d.ts.map +0 -1
- package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts +0 -3
- package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts.map +0 -1
- package/lib/typescript/module/provider/scroll-provider.d.ts +0 -14
- package/lib/typescript/module/provider/scroll-provider.d.ts.map +0 -1
- package/lib/typescript/module/provider/scroll-provider.web.d.ts +0 -3
- package/lib/typescript/module/provider/scroll-provider.web.d.ts.map +0 -1
- package/src/__tests__/scroll-native.test.ts +0 -25
- package/src/provider/scroll-provider.ts +0 -13
- package/src/provider/scroll-provider.web.ts +0 -2
|
@@ -3,32 +3,36 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports
|
|
6
|
+
Object.defineProperty(exports, "ScrollRestorationProvider", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _context.ScrollRestorationProvider;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
7
12
|
exports.useScrollRestoration = useScrollRestoration;
|
|
8
13
|
var _react = require("react");
|
|
9
|
-
var
|
|
14
|
+
var _context = require("./context.js");
|
|
10
15
|
var _scrollableWeb = require("./scrollable.web.js");
|
|
11
16
|
var _store = require("./store.js");
|
|
12
|
-
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
17
|
/**
|
|
14
18
|
* Web variant of the scroll-restoration primitive.
|
|
15
19
|
*
|
|
16
|
-
* Mirrors the proven Bluesky pattern (`history.scrollRestoration
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
* Mirrors the proven Bluesky pattern (manual `history.scrollRestoration` plus
|
|
21
|
+
* an in-memory `Map<key, offset>`) with three deliberate differences forced by
|
|
22
|
+
* Oxy's layouts and by the behaviour of the (expo-router-wrapped) React
|
|
23
|
+
* Navigation web stack:
|
|
20
24
|
*
|
|
21
25
|
* 1. Bluesky restores the WINDOW scroller, whereas Oxy apps keep multi-column
|
|
22
26
|
* layouts whose feed scrolls an INNER container. So we restore the offset
|
|
23
27
|
* of a caller-registered scrollable (a ref to an element / RN scroll
|
|
24
|
-
* component, or the `'window'` sentinel)
|
|
28
|
+
* component, or the `'window'` sentinel).
|
|
25
29
|
*
|
|
26
|
-
* 2. The web stack HIDES the background screen on push. While
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* 2. The web stack HIDES the background screen on push. While hidden, the
|
|
31
|
+
* previous screen's scroll container collapses (`scrollHeight ===
|
|
32
|
+
* clientHeight`) and the navigator forces its `scrollTop` to 0. The screen
|
|
33
|
+
* is NOT unmounted, so a virtualized list (e.g. FlashList) keeps its rows
|
|
34
|
+
* but re-lays them out over SEVERAL frames once the screen is re-shown. Two
|
|
35
|
+
* problems follow, both handled here:
|
|
32
36
|
*
|
|
33
37
|
* (a) A blur-time read of `scrollTop` returns the navigator's forced 0,
|
|
34
38
|
* not the user's real offset — saving it would clobber the good
|
|
@@ -42,28 +46,22 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
42
46
|
* sticks (the content has grown tall enough) or a small frame cap is
|
|
43
47
|
* reached.
|
|
44
48
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
49
|
+
* 3. A document-scrolled app (one window scroller shared by every route) has
|
|
50
|
+
* no per-screen container to reset, so an unrecognised key MUST be written
|
|
51
|
+
* to 0 explicitly. Doing nothing leaves the previous screen's offset in
|
|
52
|
+
* place and the new screen opens mid-page.
|
|
47
53
|
*
|
|
48
|
-
*
|
|
49
|
-
* `
|
|
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.
|
|
54
|
+
* Native bundlers use `./index.ts`; web bundlers select this file via the
|
|
55
|
+
* `"browser"` export condition in `package.json`.
|
|
55
56
|
*/
|
|
56
57
|
|
|
57
|
-
const ScrollOffsetContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
58
|
-
ScrollOffsetContext.displayName = 'BloomScrollOffsetContext';
|
|
59
|
-
|
|
60
58
|
/**
|
|
61
|
-
* Maximum number of animation frames the
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
59
|
+
* Maximum number of animation frames the restore will re-apply the saved offset
|
|
60
|
+
* before giving up. A virtualized list re-lays out its rows over a handful of
|
|
61
|
+
* frames after its screen is re-shown; ~30 frames (≈0.5s at 60fps) is
|
|
62
|
+
* comfortably longer than any observed relayout while staying short enough that
|
|
63
|
+
* the loop never lingers as a perceptible cost. The loop normally exits far
|
|
64
|
+
* earlier — as soon as the write sticks.
|
|
67
65
|
*/
|
|
68
66
|
const RESTORE_FRAME_CAP = 30;
|
|
69
67
|
|
|
@@ -77,108 +75,167 @@ const RESTORE_FRAME_CAP = 30;
|
|
|
77
75
|
const RESTORE_STICK_TOLERANCE_PX = 2;
|
|
78
76
|
|
|
79
77
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* it is set before any provider mounts, matching Bluesky's module-level call.
|
|
78
|
+
* The web binding is a constant: this platform observes offsets by subscribing
|
|
79
|
+
* to the resolved DOM node's own `scroll` event, so it needs nothing from the
|
|
80
|
+
* caller. It is still returned — and still safe to spread onto a list — so a
|
|
81
|
+
* call site is written once and runs on both platforms.
|
|
85
82
|
*/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
83
|
+
const WEB_BINDING = {
|
|
84
|
+
onScroll: () => undefined
|
|
85
|
+
};
|
|
89
86
|
|
|
90
87
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
88
|
+
* Events that mean the USER has taken over the scroller, and the restore must
|
|
89
|
+
* stop trying to put them back.
|
|
90
|
+
*
|
|
91
|
+
* The browser's own restoration is specified the same way: HTML §7.4.6.5 has
|
|
92
|
+
* the UA re-attempt the saved position and "may continue to attempt to do so
|
|
93
|
+
* periodically, until document's has been scrolled by the user becomes true".
|
|
94
|
+
* Without this the loop fights anyone who starts scrolling inside the frame
|
|
95
|
+
* budget, yanking them back once per frame for up to {@link RESTORE_FRAME_CAP}
|
|
96
|
+
* frames.
|
|
97
|
+
*
|
|
98
|
+
* Two designs were rejected, and the reasons are the point:
|
|
99
|
+
*
|
|
100
|
+
* - **"Any `scroll` event aborts"** — the obvious version, and it aborts on the
|
|
101
|
+
* FIRST frame every time: `setOffset` is itself a scroll, so the loop would
|
|
102
|
+
* cancel on the event its own write produced and no restore would ever
|
|
103
|
+
* complete.
|
|
104
|
+
* - **"Abort on an offset we did not write"** — tempting because it needs no
|
|
105
|
+
* listeners, but a virtualized list inserting rows ABOVE the viewport makes
|
|
106
|
+
* the browser's scroll anchoring move `scrollTop` on its own. That is
|
|
107
|
+
* precisely the situation the loop exists for, so the unexplained-delta test
|
|
108
|
+
* would abort exactly when it must not.
|
|
109
|
+
*
|
|
110
|
+
* What is being detected is user INPUT, so this detects user input. `pointerdown`
|
|
111
|
+
* covers a scrollbar drag and a touch-less press; `keydown` covers arrow/page
|
|
112
|
+
* keys, and reaches us whenever focus is inside the scroller it would scroll.
|
|
94
113
|
*/
|
|
95
|
-
|
|
96
|
-
children
|
|
97
|
-
}) {
|
|
98
|
-
const store = (0, _react.useMemo)(() => new _store.ScrollOffsetStore(), []);
|
|
99
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(ScrollOffsetContext.Provider, {
|
|
100
|
-
value: store,
|
|
101
|
-
children: children
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
function useScrollOffsetStore() {
|
|
105
|
-
const store = (0, _react.useContext)(ScrollOffsetContext);
|
|
106
|
-
if (store === null) {
|
|
107
|
-
throw new Error('useScrollRestoration must be used within a <ScrollRestorationProvider>.');
|
|
108
|
-
}
|
|
109
|
-
return store;
|
|
110
|
-
}
|
|
114
|
+
const USER_TAKEOVER_EVENTS = ['wheel', 'touchstart', 'pointerdown', 'keydown'];
|
|
111
115
|
|
|
112
116
|
/**
|
|
113
117
|
* Preserve and restore the scroll offset of `target` across navigation, keyed
|
|
114
|
-
* by the
|
|
115
|
-
* multiple scrollables).
|
|
118
|
+
* by the content its screen is showing plus an optional `options.key`.
|
|
116
119
|
*
|
|
117
120
|
* Behaviour (web):
|
|
118
121
|
* - On every scroll while the screen is focused, the current offset is recorded
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* saved
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
122
|
+
* under the key that was live when the listener was attached. This stream of
|
|
123
|
+
* saves is the source of truth.
|
|
124
|
+
* - On focus — and again whenever the key changes while the screen STAYS
|
|
125
|
+
* focused, which is how an in-screen tab or folder swap is seen at all — the
|
|
126
|
+
* offset saved for the new key is re-applied across a bounded run of
|
|
127
|
+
* animation frames, stopping as soon as the write sticks or
|
|
128
|
+
* {@link RESTORE_FRAME_CAP} is reached. Content never seen this session is
|
|
129
|
+
* written to 0 once instead; a write to 0 always sticks, so it needs no loop.
|
|
130
|
+
* - On blur (and on a key change), the LAST OBSERVED offset is persisted under
|
|
131
|
+
* the OUTGOING key as a final safety net — not a fresh `scrollTop` read,
|
|
132
|
+
* which the navigator may already have forced to 0 while collapsing the
|
|
133
|
+
* hidden screen.
|
|
127
134
|
*/
|
|
128
135
|
function useScrollRestoration(target, options) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
const {
|
|
137
|
+
store,
|
|
138
|
+
adapter
|
|
139
|
+
} = (0, _context.useScrollRestorationContext)();
|
|
140
|
+
const contentId = adapter.useScreenContentId();
|
|
132
141
|
const enabled = options?.enabled ?? true;
|
|
133
|
-
const scrollKey = (0, _store.deriveScrollKey)(
|
|
142
|
+
const scrollKey = (0, _store.deriveScrollKey)(contentId, options?.key);
|
|
134
143
|
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
// each focus.
|
|
144
|
+
// The key this hook instance has already sent to the top. Writing 0 is a
|
|
145
|
+
// one-time ARRIVAL action: a later re-run for the SAME key (`enabled` turning
|
|
146
|
+
// on once the rows exist, a screen regaining focus) must not repeat it,
|
|
147
|
+
// because by then the user may have scrolled somewhere the hook never got to
|
|
148
|
+
// record. Restoring twice is harmless; resetting twice is data loss.
|
|
149
|
+
const resetKeyRef = (0, _react.useRef)(null);
|
|
150
|
+
adapter.useScreenFocusEffect(
|
|
151
|
+
// Every varying input is a dependency rather than a ref read, so the
|
|
152
|
+
// session below CLOSES OVER the key it belongs to. That is what makes a
|
|
153
|
+
// key change mid-focus correct in both directions: the outgoing session's
|
|
154
|
+
// cleanup persists to the old key, and the incoming one restores the new.
|
|
147
155
|
(0, _react.useCallback)(() => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const element = targetRef.current === 'window' ? typeof window !== 'undefined' ? window : null : resolveScrollEventTarget(targetRef.current);
|
|
156
|
+
if (!enabled || scrollKey === null) return undefined;
|
|
157
|
+
const scroller = (0, _scrollableWeb.createScroller)(target);
|
|
158
|
+
const element = target === 'window' ? typeof window === 'undefined' ? null : window : resolveScrollEventTarget(target);
|
|
152
159
|
|
|
153
|
-
// The last offset the live scroll listener observed for this
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
160
|
+
// The last offset the live scroll listener observed for this session.
|
|
161
|
+
// This — not a cleanup-time `getOffset()` — is what we persist at the
|
|
162
|
+
// end, because by then the navigator may have collapsed the hidden
|
|
163
|
+
// screen and forced its `scrollTop` to 0 (problem A). `null` means the
|
|
164
|
+
// user never scrolled this session, so there is nothing newer to persist
|
|
165
|
+
// than what the scroll listener already saved live.
|
|
159
166
|
let lastObservedOffset = null;
|
|
167
|
+
|
|
168
|
+
// The 0 WE wrote, which a real browser dispatches back as a `scroll`
|
|
169
|
+
// event (jsdom does not, so the test for this emits it explicitly).
|
|
170
|
+
// Persisting it would record an offset the user never chose, under a key
|
|
171
|
+
// another LIVE screen may share — the two-live-entries trade content
|
|
172
|
+
// keying makes (see `deriveScrollKey`) means a sibling showing the same
|
|
173
|
+
// content can save a real offset between our write and its echo, and the
|
|
174
|
+
// echo would clobber it.
|
|
175
|
+
let echoOffset = null;
|
|
160
176
|
const save = () => {
|
|
161
|
-
const currentKey = scrollKeyRef.current;
|
|
162
|
-
if (!enabledRef.current || currentKey === null) return;
|
|
163
177
|
const offset = scroller.getOffset();
|
|
164
178
|
// Ignore a spurious 0 produced by the navigator collapsing a hidden
|
|
165
179
|
// background screen: while collapsed the container cannot scroll, so
|
|
166
|
-
// its `scrollTop` is forced to 0. Persisting it would clobber the
|
|
167
|
-
//
|
|
180
|
+
// its `scrollTop` is forced to 0. Persisting it would clobber the good
|
|
181
|
+
// offset recorded by earlier live saves (problem A). A genuine
|
|
168
182
|
// scroll-to-top keeps the container scrollable and is saved normally.
|
|
169
183
|
if (offset === 0 && !scroller.canScroll()) return;
|
|
184
|
+
if (offset === echoOffset) {
|
|
185
|
+
// Our own write coming back. Consume the arming so a real scroll to
|
|
186
|
+
// the same offset later is saved normally.
|
|
187
|
+
echoOffset = null;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
echoOffset = null;
|
|
170
191
|
lastObservedOffset = offset;
|
|
171
|
-
store.save(
|
|
192
|
+
store.save(scrollKey, offset);
|
|
172
193
|
};
|
|
173
194
|
|
|
174
|
-
// Restore
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
195
|
+
// Restore and reset are ONE write. A miss reads 0, and 0 is exactly what
|
|
196
|
+
// an unseen screen must be written to — left alone it would show the
|
|
197
|
+
// previous screen's offset, since a document-scrolled app shares one
|
|
198
|
+
// scroller across every route.
|
|
199
|
+
const targetOffset = store.read(scrollKey);
|
|
200
|
+
|
|
201
|
+
// They remain two DECISIONS, though, and `read` cannot tell them apart:
|
|
202
|
+
// a key never seen and a key deliberately saved at the top both read 0.
|
|
203
|
+
// Only the first is a RESET, and a reset is a one-time ARRIVAL action
|
|
204
|
+
// gated on `resetKeyRef` — repeating it on a later re-run for the same
|
|
205
|
+
// key would send a user who has since scrolled back to the top, and the
|
|
206
|
+
// hook was inert while they did it so there is nothing to restore them
|
|
207
|
+
// to afterwards. A 0 the user deliberately saved is theirs, and always
|
|
208
|
+
// re-applies. `has` is the only thing that answers this.
|
|
209
|
+
const isReset = !store.has(scrollKey);
|
|
210
|
+
const alreadyReset = isReset && resetKeyRef.current === scrollKey;
|
|
211
|
+
if (isReset) resetKeyRef.current = scrollKey;
|
|
180
212
|
let rafId = null;
|
|
181
|
-
|
|
213
|
+
const stopRestore = () => {
|
|
214
|
+
if (rafId === null) return;
|
|
215
|
+
cancelAnimationFrame(rafId);
|
|
216
|
+
rafId = null;
|
|
217
|
+
};
|
|
218
|
+
if (alreadyReset) {
|
|
219
|
+
// Nothing to write: this content has already been sent to the top once
|
|
220
|
+
// and the user is wherever they have moved to since.
|
|
221
|
+
} else if (targetOffset === 0) {
|
|
222
|
+
// A write to 0 is never clamped away, so it needs no re-apply loop.
|
|
223
|
+
echoOffset = 0;
|
|
224
|
+
scroller.setOffset(0);
|
|
225
|
+
} else if (typeof requestAnimationFrame !== 'undefined') {
|
|
226
|
+
// Re-apply across a bounded run of frames. A freshly re-shown
|
|
227
|
+
// virtualized list re-lays out its rows over several frames, so a
|
|
228
|
+
// single write while it is still collapsed would be clamped to 0 and
|
|
229
|
+
// never re-applied (problem B).
|
|
230
|
+
//
|
|
231
|
+
// KNOWN PROPERTY, not a bug: each of these writes echoes back as a
|
|
232
|
+
// `scroll` event and IS persisted (only the reset arms `echoOffset`,
|
|
233
|
+
// and `canScroll()` is always true for the `'window'` sentinel so the
|
|
234
|
+
// collapsed-container guard never fires there). A loop that reaches
|
|
235
|
+
// the target self-heals on its last echo. A loop that exhausts the cap
|
|
236
|
+
// leaves the CLAMPED offset stored — which is the right thing to
|
|
237
|
+
// remember when the content genuinely shrank, since that is as far as
|
|
238
|
+
// the user can now scroll.
|
|
182
239
|
let framesLeft = RESTORE_FRAME_CAP;
|
|
183
240
|
const applyOffset = () => {
|
|
184
241
|
rafId = null;
|
|
@@ -192,26 +249,35 @@ function useScrollRestoration(target, options) {
|
|
|
192
249
|
}
|
|
193
250
|
};
|
|
194
251
|
rafId = requestAnimationFrame(applyOffset);
|
|
252
|
+
// Only while a loop is actually in flight — the reset path has nothing
|
|
253
|
+
// to abort, and the listeners would be idle on every screen.
|
|
254
|
+
for (const type of USER_TAKEOVER_EVENTS) {
|
|
255
|
+
element?.addEventListener(type, stopRestore, {
|
|
256
|
+
passive: true
|
|
257
|
+
});
|
|
258
|
+
}
|
|
195
259
|
}
|
|
196
260
|
element?.addEventListener('scroll', save, {
|
|
197
261
|
passive: true
|
|
198
262
|
});
|
|
199
263
|
return () => {
|
|
200
|
-
|
|
264
|
+
stopRestore();
|
|
201
265
|
element?.removeEventListener('scroll', save);
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
266
|
+
// Removing a listener that was never added is a no-op, so this needs no
|
|
267
|
+
// flag tracking whether the loop branch ran.
|
|
268
|
+
for (const type of USER_TAKEOVER_EVENTS) {
|
|
269
|
+
element?.removeEventListener(type, stopRestore);
|
|
270
|
+
}
|
|
271
|
+
// Final capture: persist the last offset the scroll listener OBSERVED,
|
|
272
|
+
// never a fresh read (which the navigator may have forced to 0 while
|
|
273
|
+
// collapsing the hidden screen). When the user never scrolled this
|
|
274
|
+
// session there is nothing newer than the live saves already recorded.
|
|
207
275
|
if (lastObservedOffset !== null) {
|
|
208
|
-
|
|
209
|
-
if (enabledRef.current && currentKey !== null) {
|
|
210
|
-
store.save(currentKey, lastObservedOffset);
|
|
211
|
-
}
|
|
276
|
+
store.save(scrollKey, lastObservedOffset);
|
|
212
277
|
}
|
|
213
278
|
};
|
|
214
|
-
}, [store]));
|
|
279
|
+
}, [store, scrollKey, enabled, target]));
|
|
280
|
+
return WEB_BINDING;
|
|
215
281
|
}
|
|
216
282
|
|
|
217
283
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","require","
|
|
1
|
+
{"version":3,"names":["_react","require","_context","_scrollableWeb","_store","RESTORE_FRAME_CAP","RESTORE_STICK_TOLERANCE_PX","WEB_BINDING","onScroll","undefined","USER_TAKEOVER_EVENTS","useScrollRestoration","target","options","store","adapter","useScrollRestorationContext","contentId","useScreenContentId","enabled","scrollKey","deriveScrollKey","key","resetKeyRef","useRef","useScreenFocusEffect","useCallback","scroller","createScroller","element","window","resolveScrollEventTarget","lastObservedOffset","echoOffset","save","offset","getOffset","canScroll","targetOffset","read","isReset","has","alreadyReset","current","rafId","stopRestore","cancelAnimationFrame","setOffset","requestAnimationFrame","framesLeft","applyOffset","reached","Math","abs","type","addEventListener","passive","removeEventListener","EventTarget","handle","getScrollableNode","node"],"sourceRoot":"../../../src","sources":["scroll/index.web.tsx"],"mappings":";;;;;;;;;;;;AAwCA,IAAAA,MAAA,GAAAC,OAAA;AAEA,IAAAC,QAAA,GAAAD,OAAA;AACA,IAAAE,cAAA,GAAAF,OAAA;AACA,IAAAG,MAAA,GAAAH,OAAA;AA5CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAwBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMI,iBAAiB,GAAG,EAAE;;AAE5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,0BAA0B,GAAG,CAAC;;AAEpC;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,WAAqC,GAAG;EAAEC,QAAQ,EAAEA,CAAA,KAAMC;AAAU,CAAC;;AAE3E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,oBAAoB,GAAG,CAAC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,CAAC;;AAE9E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,oBAAoBA,CAClCC,MAA+B,EAC/BC,OAAqC,EACX;EAC1B,MAAM;IAAEC,KAAK;IAAEC;EAAQ,CAAC,GAAG,IAAAC,oCAA2B,EAAC,CAAC;EACxD,MAAMC,SAAS,GAAGF,OAAO,CAACG,kBAAkB,CAAC,CAAC;EAC9C,MAAMC,OAAO,GAAGN,OAAO,EAAEM,OAAO,IAAI,IAAI;EACxC,MAAMC,SAAS,GAAG,IAAAC,sBAAe,EAACJ,SAAS,EAAEJ,OAAO,EAAES,GAAG,CAAC;;EAE1D;EACA;EACA;EACA;EACA;EACA,MAAMC,WAAW,GAAG,IAAAC,aAAM,EA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","ignoreList":[]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.createScroller = createScroller;
|
|
7
|
+
/**
|
|
8
|
+
* The write half of a scrollable, on native. There is no read half: native
|
|
9
|
+
* offers no way to sample a list's offset from outside it, so offsets arrive
|
|
10
|
+
* through the `onScroll` binding the hook returns instead.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function hasScrollToOffset(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null && typeof value.scrollToOffset === 'function';
|
|
15
|
+
}
|
|
16
|
+
function hasScrollTo(value) {
|
|
17
|
+
return typeof value === 'object' && value !== null && typeof value.scrollTo === 'function';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build a {@link NativeScroller} for a target.
|
|
22
|
+
*
|
|
23
|
+
* `scrollToOffset` (`FlatList`, `FlashList`) is preferred over `scrollTo`
|
|
24
|
+
* (`ScrollView`) because a list exposes both shapes on some versions and only
|
|
25
|
+
* the former accounts for the list's own header. The `'window'` sentinel has no
|
|
26
|
+
* native meaning — there is no document scroller — so it resolves to a no-op
|
|
27
|
+
* rather than an error: a call site shared with web legitimately passes it.
|
|
28
|
+
*
|
|
29
|
+
* The handle is re-resolved on every write; it is never cached, because a ref
|
|
30
|
+
* can be swapped or detached between the schedule and the write.
|
|
31
|
+
*/
|
|
32
|
+
function createScroller(target) {
|
|
33
|
+
if (target === 'window') {
|
|
34
|
+
return {
|
|
35
|
+
setOffset: () => undefined
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
setOffset: offset => {
|
|
40
|
+
const current = target.current;
|
|
41
|
+
if (current == null) return;
|
|
42
|
+
if (hasScrollToOffset(current)) {
|
|
43
|
+
current.scrollToOffset({
|
|
44
|
+
offset,
|
|
45
|
+
animated: false
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (hasScrollTo(current)) {
|
|
50
|
+
current.scrollTo({
|
|
51
|
+
y: offset,
|
|
52
|
+
animated: false
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=scrollable.native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["hasScrollToOffset","value","scrollToOffset","hasScrollTo","scrollTo","createScroller","target","setOffset","undefined","offset","current","animated","y"],"sourceRoot":"../../../src","sources":["scroll/scrollable.native.ts"],"mappings":";;;;;;AAEA;AACA;AACA;AACA;AACA;;AAKA,SAASA,iBAAiBA,CACxBC,KAAc,EAC+C;EAC7D,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,OAAQA,KAAK,CAAsBC,cAAc,KAAK,UAAU;AAEpE;AAEA,SAASC,WAAWA,CAClBF,KAAc,EACyC;EACvD,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,OAAQA,KAAK,CAAsBG,QAAQ,KAAK,UAAU;AAE9D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,cAAcA,CAC5BC,MAA+B,EACf;EAChB,IAAIA,MAAM,KAAK,QAAQ,EAAE;IACvB,OAAO;MAAEC,SAAS,EAAEA,CAAA,KAAMC;IAAU,CAAC;EACvC;EAEA,OAAO;IACLD,SAAS,EAAGE,MAAM,IAAK;MACrB,MAAMC,OAAO,GAAGJ,MAAM,CAACI,OAAO;MAC9B,IAAIA,OAAO,IAAI,IAAI,EAAE;MACrB,IAAIV,iBAAiB,CAACU,OAAO,CAAC,EAAE;QAC9BA,OAAO,CAACR,cAAc,CAAC;UAAEO,MAAM;UAAEE,QAAQ,EAAE;QAAM,CAAC,CAAC;QACnD;MACF;MACA,IAAIR,WAAW,CAACO,OAAO,CAAC,EAAE;QACxBA,OAAO,CAACN,QAAQ,CAAC;UAAEQ,CAAC,EAAEH,MAAM;UAAEE,QAAQ,EAAE;QAAM,CAAC,CAAC;MAClD;IACF;EACF,CAAC;AACH","ignoreList":[]}
|
|
@@ -10,16 +10,12 @@ exports.deriveScrollKey = deriveScrollKey;
|
|
|
10
10
|
*
|
|
11
11
|
* This file deliberately contains NO React and NO DOM/native imports so the
|
|
12
12
|
* logic can be unit-tested in isolation and shared verbatim by the web and
|
|
13
|
-
* native barrels.
|
|
14
|
-
* native barrel never instantiates it (native-stack already restores scroll).
|
|
13
|
+
* native barrels.
|
|
15
14
|
*/
|
|
16
|
-
|
|
17
15
|
/**
|
|
18
|
-
* Separator between the
|
|
19
|
-
* sub-key. A route may host more than one independently-scrolling list (e.g.
|
|
20
|
-
* a tabbed profile screen), so each list contributes its own sub-key.
|
|
16
|
+
* Separator between the two components of a storage key.
|
|
21
17
|
*
|
|
22
|
-
* `\0` (NUL) can never appear in a
|
|
18
|
+
* `\0` (NUL) can never appear in a router-derived content id or in a
|
|
23
19
|
* developer-authored sub-key, so it is collision-free as a delimiter. It is
|
|
24
20
|
* written as an escape sequence (not a literal byte) so the source stays
|
|
25
21
|
* text-diffable.
|
|
@@ -27,28 +23,48 @@ exports.deriveScrollKey = deriveScrollKey;
|
|
|
27
23
|
const COMPOSITE_KEY_SEPARATOR = '\0';
|
|
28
24
|
|
|
29
25
|
/**
|
|
30
|
-
* Derive the storage key for a scrollable from its
|
|
31
|
-
* optional caller sub-key.
|
|
26
|
+
* Derive the storage key for a scrollable from WHAT its screen is showing plus
|
|
27
|
+
* an optional caller sub-key.
|
|
28
|
+
*
|
|
29
|
+
* The key is CONTENT identity, deliberately not the navigation entry. An offset
|
|
30
|
+
* belongs to what the user was looking at, so returning to it restores however
|
|
31
|
+
* they got there — browser Back or Forward, a tab press, an in-app link — and
|
|
32
|
+
* only content never seen this session opens at the top. Keying on the entry
|
|
33
|
+
* would defeat that: a tab press and `router.replace` both mint a fresh entry,
|
|
34
|
+
* so both would open at the top even though the user has been there.
|
|
35
|
+
*
|
|
36
|
+
* The trade, chosen rather than overlooked: the same content occupying two LIVE
|
|
37
|
+
* entries now shares one offset. Pushing `/@alice` on top of `/@alice` in a
|
|
38
|
+
* native stack restores the first one's position instead of opening at the top,
|
|
39
|
+
* and while both are mounted each writes to the same key. That is the price of
|
|
40
|
+
* a tab press restoring, which is the behaviour that was asked for.
|
|
32
41
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
42
|
+
* The key is always the same two-field composite, with an absent sub-key
|
|
43
|
+
* written as empty, so two different identities can never collapse onto one key
|
|
44
|
+
* by omission.
|
|
35
45
|
*
|
|
36
|
-
* Returns `null` when there is no
|
|
37
|
-
* is not inside a navigator
|
|
46
|
+
* Returns `null` when there is no content id to anchor against (the scrollable
|
|
47
|
+
* is not inside a navigator, or the adapter cannot answer), which every caller
|
|
48
|
+
* treats as "do not persist and do not restore".
|
|
38
49
|
*/
|
|
39
|
-
function deriveScrollKey(
|
|
40
|
-
if (!
|
|
41
|
-
|
|
42
|
-
return `${routeKey}${COMPOSITE_KEY_SEPARATOR}${subKey}`;
|
|
50
|
+
function deriveScrollKey(contentId, subKey) {
|
|
51
|
+
if (!contentId) return null;
|
|
52
|
+
return [contentId, subKey ?? ''].join(COMPOSITE_KEY_SEPARATOR);
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
/**
|
|
46
56
|
* In-memory map of `scrollKey -> last-known scroll offset`.
|
|
47
57
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
58
|
+
* Offsets live only for the lifetime of the document/session. We never persist
|
|
59
|
+
* them — a full reload should start at the top — and we never auto-evict,
|
|
60
|
+
* because an entry can be revisited via browser Forward/Back long after it
|
|
61
|
+
* blurred.
|
|
62
|
+
*
|
|
63
|
+
* Deliberately an instance held in React context rather than a module-level
|
|
64
|
+
* singleton: a dependency tree can legitimately contain two copies of Bloom
|
|
65
|
+
* (a hoisted one plus a nested one under another Oxy package), and a
|
|
66
|
+
* module-global would then exist twice with no way for either half to see the
|
|
67
|
+
* other's offsets.
|
|
52
68
|
*/
|
|
53
69
|
class ScrollOffsetStore {
|
|
54
70
|
offsets = new Map();
|
|
@@ -59,19 +75,25 @@ class ScrollOffsetStore {
|
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
/**
|
|
62
|
-
* Read the saved offset for a key. Returns `0` when nothing was saved,
|
|
63
|
-
*
|
|
78
|
+
* Read the saved offset for a key. Returns `0` when nothing was saved, which
|
|
79
|
+
* is the same value a caller writes when {@link has} is false — an unseen
|
|
80
|
+
* list restores to the top.
|
|
64
81
|
*/
|
|
65
82
|
read(key) {
|
|
66
83
|
return this.offsets.get(key) ?? 0;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Whether an offset was ever saved for this key. This — not `read() > 0` —
|
|
88
|
+
* is what separates "restore" from "reset": a key that was never seen and a
|
|
89
|
+
* key deliberately saved at the top both read 0, and only the store knows
|
|
90
|
+
* which is which.
|
|
91
|
+
*/
|
|
70
92
|
has(key) {
|
|
71
93
|
return this.offsets.has(key);
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
/** Drop a saved offset (e.g. when
|
|
96
|
+
/** Drop a saved offset (e.g. when an entry is permanently removed). */
|
|
75
97
|
forget(key) {
|
|
76
98
|
this.offsets.delete(key);
|
|
77
99
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["COMPOSITE_KEY_SEPARATOR","deriveScrollKey","
|
|
1
|
+
{"version":3,"names":["COMPOSITE_KEY_SEPARATOR","deriveScrollKey","contentId","subKey","join","ScrollOffsetStore","offsets","Map","save","key","offset","set","read","get","has","forget","delete","clear","size","exports"],"sourceRoot":"../../../src","sources":["scroll/store.ts"],"mappings":";;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMA,uBAAuB,GAAG,IAAI;;AAEpC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,eAAeA,CAC7BC,SAAwB,EACxBC,MAAe,EACA;EACf,IAAI,CAACD,SAAS,EAAE,OAAO,IAAI;EAC3B,OAAO,CAACA,SAAS,EAAEC,MAAM,IAAI,EAAE,CAAC,CAACC,IAAI,CAACJ,uBAAuB,CAAC;AAChE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAMK,iBAAiB,CAAC;EACZC,OAAO,GAAG,IAAIC,GAAG,CAAiB,CAAC;;EAEpD;EACAC,IAAIA,CAACC,GAAW,EAAEC,MAAc,EAAQ;IACtC,IAAI,CAACJ,OAAO,CAACK,GAAG,CAACF,GAAG,EAAEC,MAAM,GAAG,CAAC,GAAGA,MAAM,GAAG,CAAC,CAAC;EAChD;;EAEA;AACF;AACA;AACA;AACA;EACEE,IAAIA,CAACH,GAAW,EAAU;IACxB,OAAO,IAAI,CAACH,OAAO,CAACO,GAAG,CAACJ,GAAG,CAAC,IAAI,CAAC;EACnC;;EAEA;AACF;AACA;AACA;AACA;AACA;EACEK,GAAGA,CAACL,GAAW,EAAW;IACxB,OAAO,IAAI,CAACH,OAAO,CAACQ,GAAG,CAACL,GAAG,CAAC;EAC9B;;EAEA;EACAM,MAAMA,CAACN,GAAW,EAAQ;IACxB,IAAI,CAACH,OAAO,CAACU,MAAM,CAACP,GAAG,CAAC;EAC1B;;EAEA;EACAQ,KAAKA,CAAA,EAAS;IACZ,IAAI,CAACX,OAAO,CAACW,KAAK,CAAC,CAAC;EACtB;;EAEA;EACA,IAAIC,IAAIA,CAAA,EAAW;IACjB,OAAO,IAAI,CAACZ,OAAO,CAACY,IAAI;EAC1B;AACF;AAACC,OAAA,CAAAd,iBAAA,GAAAA,iBAAA","ignoreList":[]}
|