@oxyhq/bloom 0.6.21 → 0.6.22
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.js +39 -0
- package/lib/commonjs/scroll/index.js.map +1 -0
- package/lib/commonjs/scroll/index.web.js +143 -0
- package/lib/commonjs/scroll/index.web.js.map +1 -0
- package/lib/commonjs/scroll/scrollable.web.js +64 -0
- package/lib/commonjs/scroll/scrollable.web.js.map +1 -0
- package/lib/commonjs/scroll/store.js +90 -0
- package/lib/commonjs/scroll/store.js.map +1 -0
- package/lib/commonjs/scroll/types.js +6 -0
- package/lib/commonjs/scroll/types.js.map +1 -0
- package/lib/module/scroll/index.js +34 -0
- package/lib/module/scroll/index.js.map +1 -0
- package/lib/module/scroll/index.web.js +137 -0
- package/lib/module/scroll/index.web.js.map +1 -0
- package/lib/module/scroll/scrollable.web.js +60 -0
- package/lib/module/scroll/scrollable.web.js.map +1 -0
- package/lib/module/scroll/store.js +84 -0
- package/lib/module/scroll/store.js.map +1 -0
- package/lib/module/scroll/types.js +4 -0
- package/lib/module/scroll/types.js.map +1 -0
- package/lib/typescript/commonjs/scroll/index.d.ts +27 -0
- package/lib/typescript/commonjs/scroll/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/index.web.d.ts +22 -0
- package/lib/typescript/commonjs/scroll/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/scrollable.web.d.ts +17 -0
- package/lib/typescript/commonjs/scroll/scrollable.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/store.d.ts +46 -0
- package/lib/typescript/commonjs/scroll/store.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/types.d.ts +46 -0
- package/lib/typescript/commonjs/scroll/types.d.ts.map +1 -0
- package/lib/typescript/module/scroll/index.d.ts +27 -0
- package/lib/typescript/module/scroll/index.d.ts.map +1 -0
- package/lib/typescript/module/scroll/index.web.d.ts +22 -0
- package/lib/typescript/module/scroll/index.web.d.ts.map +1 -0
- package/lib/typescript/module/scroll/scrollable.web.d.ts +17 -0
- package/lib/typescript/module/scroll/scrollable.web.d.ts.map +1 -0
- package/lib/typescript/module/scroll/store.d.ts +46 -0
- package/lib/typescript/module/scroll/store.d.ts.map +1 -0
- package/lib/typescript/module/scroll/types.d.ts +46 -0
- package/lib/typescript/module/scroll/types.d.ts.map +1 -0
- package/package.json +22 -1
- package/src/__tests__/scroll-native.test.ts +25 -0
- package/src/__tests__/scroll-store.test.ts +85 -0
- package/src/scroll/index.ts +47 -0
- package/src/scroll/index.web.tsx +167 -0
- package/src/scroll/scrollable.web.ts +75 -0
- package/src/scroll/store.ts +84 -0
- package/src/scroll/types.ts +48 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure, platform-agnostic core of the scroll-restoration primitive.
|
|
5
|
+
*
|
|
6
|
+
* This file deliberately contains NO React and NO DOM/native imports so the
|
|
7
|
+
* logic can be unit-tested in isolation and shared verbatim by the web and
|
|
8
|
+
* native barrels. The web barrel drives it with real scroll offsets; the
|
|
9
|
+
* native barrel never instantiates it (native-stack already restores scroll).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Separator between the navigation route key and an optional caller-supplied
|
|
14
|
+
* sub-key. A route may host more than one independently-scrolling list (e.g.
|
|
15
|
+
* a tabbed profile screen), so each list contributes its own sub-key.
|
|
16
|
+
*
|
|
17
|
+
* `\0` (NUL) can never appear in a React Navigation route key or in a
|
|
18
|
+
* developer-authored sub-key, so it is collision-free as a delimiter. It is
|
|
19
|
+
* written as an escape sequence (not a literal byte) so the source stays
|
|
20
|
+
* text-diffable.
|
|
21
|
+
*/
|
|
22
|
+
const COMPOSITE_KEY_SEPARATOR = '\0';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Derive the storage key for a scrollable from its owning route key and an
|
|
26
|
+
* optional caller sub-key.
|
|
27
|
+
*
|
|
28
|
+
* - `routeKey` is React Navigation's stable per-route `route.key`.
|
|
29
|
+
* - `subKey` distinguishes multiple scrollables that share a single route.
|
|
30
|
+
*
|
|
31
|
+
* Returns `null` when there is no route key to anchor against (the scrollable
|
|
32
|
+
* is not inside a navigator), which the caller treats as "do not persist".
|
|
33
|
+
*/
|
|
34
|
+
export function deriveScrollKey(routeKey, subKey) {
|
|
35
|
+
if (!routeKey) return null;
|
|
36
|
+
if (subKey === undefined || subKey === '') return routeKey;
|
|
37
|
+
return `${routeKey}${COMPOSITE_KEY_SEPARATOR}${subKey}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* In-memory map of `scrollKey -> last-known scroll offset`.
|
|
42
|
+
*
|
|
43
|
+
* Mirrors the semantics of Bluesky's `Map<screenKey, scrollY>`: offsets live
|
|
44
|
+
* only for the lifetime of the document/session. We never persist them — a
|
|
45
|
+
* full reload should start at the top — and we never auto-evict, because a
|
|
46
|
+
* route can be revisited via browser Forward/Back long after it blurred.
|
|
47
|
+
*/
|
|
48
|
+
export class ScrollOffsetStore {
|
|
49
|
+
offsets = new Map();
|
|
50
|
+
|
|
51
|
+
/** Persist the offset for a key. A negative offset is clamped to 0. */
|
|
52
|
+
save(key, offset) {
|
|
53
|
+
this.offsets.set(key, offset > 0 ? offset : 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read the saved offset for a key. Returns `0` when nothing was saved, so
|
|
58
|
+
* callers can restore unconditionally (an unseen list restores to the top).
|
|
59
|
+
*/
|
|
60
|
+
read(key) {
|
|
61
|
+
return this.offsets.get(key) ?? 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether an offset was ever saved for this key. */
|
|
65
|
+
has(key) {
|
|
66
|
+
return this.offsets.has(key);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Drop a saved offset (e.g. when a route is permanently removed). */
|
|
70
|
+
forget(key) {
|
|
71
|
+
this.offsets.delete(key);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Clear every saved offset. Primarily for tests and full resets. */
|
|
75
|
+
clear() {
|
|
76
|
+
this.offsets.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Number of distinct keys currently tracked. */
|
|
80
|
+
get size() {
|
|
81
|
+
return this.offsets.size;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["COMPOSITE_KEY_SEPARATOR","deriveScrollKey","routeKey","subKey","undefined","ScrollOffsetStore","offsets","Map","save","key","offset","set","read","get","has","forget","delete","clear","size"],"sourceRoot":"../../../src","sources":["scroll/store.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;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,OAAO,SAASC,eAAeA,CAC7BC,QAA4B,EAC5BC,MAAe,EACA;EACf,IAAI,CAACD,QAAQ,EAAE,OAAO,IAAI;EAC1B,IAAIC,MAAM,KAAKC,SAAS,IAAID,MAAM,KAAK,EAAE,EAAE,OAAOD,QAAQ;EAC1D,OAAO,GAAGA,QAAQ,GAAGF,uBAAuB,GAAGG,MAAM,EAAE;AACzD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAME,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;EACEE,IAAIA,CAACH,GAAW,EAAU;IACxB,OAAO,IAAI,CAACH,OAAO,CAACO,GAAG,CAACJ,GAAG,CAAC,IAAI,CAAC;EACnC;;EAEA;EACAK,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","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["scroll/types.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native variant of the scroll-restoration primitive — a deliberate no-op.
|
|
3
|
+
*
|
|
4
|
+
* React Navigation's native-stack keeps every screen mounted while it is in the
|
|
5
|
+
* stack, so a list's scroll position survives a push/pop for free. There is
|
|
6
|
+
* nothing to save or restore. We still ship the full API surface (provider +
|
|
7
|
+
* hook) so consumers write one set of call sites that compile and run on every
|
|
8
|
+
* platform; on native the provider just renders its children and the hook does
|
|
9
|
+
* nothing.
|
|
10
|
+
*
|
|
11
|
+
* Web bundlers select `./index.web` via the `"browser"` export condition in
|
|
12
|
+
* `package.json`; native bundlers fall through to this file.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactElement } from 'react';
|
|
15
|
+
import type { ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions } from './types';
|
|
16
|
+
export type { ScrollableHandle, ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions, } from './types';
|
|
17
|
+
/**
|
|
18
|
+
* No-op provider. Renders children unchanged — native scroll persistence is
|
|
19
|
+
* handled by the navigator, so no per-route state is kept.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ScrollRestorationProvider({ children, }: ScrollRestorationProviderProps): ReactElement;
|
|
22
|
+
/**
|
|
23
|
+
* No-op hook. Accepts the same arguments as the web implementation so call
|
|
24
|
+
* sites are identical across platforms.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useScrollRestoration(_target: ScrollRestorationTarget, _options?: UseScrollRestorationOptions): void;
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAC1C,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;AAEjB;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,GACT,EAAE,8BAA8B,GAAG,YAAY,CAE/C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,2BAA2B,GACrC,IAAI,CAEN"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions } from './types';
|
|
2
|
+
export type { ScrollableHandle, ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions, } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Holds the per-route offset map for the subtree. One provider near the app
|
|
5
|
+
* root is enough; the store lives for the document's lifetime so offsets
|
|
6
|
+
* survive navigating away and back (including browser Back/Forward).
|
|
7
|
+
*/
|
|
8
|
+
export declare function ScrollRestorationProvider({ children, }: ScrollRestorationProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
/**
|
|
10
|
+
* Preserve and restore the scroll offset of `target` across navigation, keyed
|
|
11
|
+
* by the active route (plus an optional `options.key` for routes that host
|
|
12
|
+
* multiple scrollables).
|
|
13
|
+
*
|
|
14
|
+
* Behaviour (web):
|
|
15
|
+
* - On every scroll while the screen is focused, the current offset is saved.
|
|
16
|
+
* - On focus, the saved offset is applied in a single `requestAnimationFrame`,
|
|
17
|
+
* giving a remounted list one frame to lay out its content first (no retry
|
|
18
|
+
* loops, no hide/show tricks).
|
|
19
|
+
* - On blur, the latest offset is captured as a final safety net.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useScrollRestoration(target: ScrollRestorationTarget, options?: UseScrollRestorationOptions): void;
|
|
22
|
+
//# sourceMappingURL=index.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.web.tsx"],"names":[],"mappings":"AAmBA,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;AAgBjB;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,GACT,EAAE,8BAA8B,2CAOhC;AAYD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI,CAyDN"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ScrollRestorationTarget } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* A normalized read/write interface over whatever the caller registered, so
|
|
4
|
+
* the hook does not branch on target shape. Both methods are no-ops when the
|
|
5
|
+
* underlying element is not yet (or no longer) attached.
|
|
6
|
+
*/
|
|
7
|
+
export interface ResolvedScroller {
|
|
8
|
+
getOffset: () => number;
|
|
9
|
+
setOffset: (offset: number) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build a {@link ResolvedScroller} for a target. The window sentinel reads and
|
|
13
|
+
* writes the document scroller; everything else operates on the resolved
|
|
14
|
+
* element's `scrollTop`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createScroller(target: ScrollRestorationTarget): ResolvedScroller;
|
|
17
|
+
//# sourceMappingURL=scrollable.web.d.ts.map
|
|
@@ -0,0 +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;CACrC;AAuCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,uBAAuB,GAAG,gBAAgB,CAoBhF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, platform-agnostic core of the scroll-restoration primitive.
|
|
3
|
+
*
|
|
4
|
+
* This file deliberately contains NO React and NO DOM/native imports so the
|
|
5
|
+
* logic can be unit-tested in isolation and shared verbatim by the web and
|
|
6
|
+
* native barrels. The web barrel drives it with real scroll offsets; the
|
|
7
|
+
* native barrel never instantiates it (native-stack already restores scroll).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Derive the storage key for a scrollable from its owning route key and an
|
|
11
|
+
* optional caller sub-key.
|
|
12
|
+
*
|
|
13
|
+
* - `routeKey` is React Navigation's stable per-route `route.key`.
|
|
14
|
+
* - `subKey` distinguishes multiple scrollables that share a single route.
|
|
15
|
+
*
|
|
16
|
+
* Returns `null` when there is no route key to anchor against (the scrollable
|
|
17
|
+
* is not inside a navigator), which the caller treats as "do not persist".
|
|
18
|
+
*/
|
|
19
|
+
export declare function deriveScrollKey(routeKey: string | undefined, subKey?: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* In-memory map of `scrollKey -> last-known scroll offset`.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors the semantics of Bluesky's `Map<screenKey, scrollY>`: offsets live
|
|
24
|
+
* only for the lifetime of the document/session. We never persist them — a
|
|
25
|
+
* full reload should start at the top — and we never auto-evict, because a
|
|
26
|
+
* route can be revisited via browser Forward/Back long after it blurred.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ScrollOffsetStore {
|
|
29
|
+
private readonly offsets;
|
|
30
|
+
/** Persist the offset for a key. A negative offset is clamped to 0. */
|
|
31
|
+
save(key: string, offset: number): void;
|
|
32
|
+
/**
|
|
33
|
+
* Read the saved offset for a key. Returns `0` when nothing was saved, so
|
|
34
|
+
* callers can restore unconditionally (an unseen list restores to the top).
|
|
35
|
+
*/
|
|
36
|
+
read(key: string): number;
|
|
37
|
+
/** Whether an offset was ever saved for this key. */
|
|
38
|
+
has(key: string): boolean;
|
|
39
|
+
/** Drop a saved offset (e.g. when a route is permanently removed). */
|
|
40
|
+
forget(key: string): void;
|
|
41
|
+
/** Clear every saved offset. Primarily for tests and full resets. */
|
|
42
|
+
clear(): void;
|
|
43
|
+
/** Number of distinct keys currently tracked. */
|
|
44
|
+
get size(): number;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../../src/scroll/store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAcH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAIf;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,uEAAuE;IACvE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAIvC;;;OAGG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAIzB,qDAAqD;IACrD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB,sEAAsE;IACtE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,qEAAqE;IACrE,KAAK,IAAI,IAAI;IAIb,iDAAiD;IACjD,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode, RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Anything `useScrollRestoration` knows how to read/write a vertical scroll
|
|
4
|
+
* offset from. The hook accepts a ref to one of these:
|
|
5
|
+
*
|
|
6
|
+
* - a React Native `ScrollView` / `FlatList` ref (which on web is backed by a
|
|
7
|
+
* DOM node and exposes `getScrollableNode()`),
|
|
8
|
+
* - a raw DOM element ref (when a component renders its own scroll container),
|
|
9
|
+
* - the literal `'window'` sentinel, for the rare layout where the list IS the
|
|
10
|
+
* window scroller (matches Bluesky's default).
|
|
11
|
+
*
|
|
12
|
+
* Native never reads any of these — the native hook is a no-op — so the type is
|
|
13
|
+
* intentionally permissive rather than coupled to a specific RN class.
|
|
14
|
+
*/
|
|
15
|
+
export interface ScrollableHandle {
|
|
16
|
+
/** Imperative scroll API exposed by RN scrollables. */
|
|
17
|
+
scrollTo?: (options: {
|
|
18
|
+
x?: number;
|
|
19
|
+
y?: number;
|
|
20
|
+
animated?: boolean;
|
|
21
|
+
}) => void;
|
|
22
|
+
/** Web/RNW path: returns the underlying DOM node. */
|
|
23
|
+
getScrollableNode?: () => unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The accepted ref shapes. `'window'` is a sentinel for the document scroller.
|
|
27
|
+
*/
|
|
28
|
+
export type ScrollRestorationTarget = RefObject<ScrollableHandle | null> | RefObject<unknown> | 'window';
|
|
29
|
+
export interface UseScrollRestorationOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Sub-key to disambiguate multiple scrollables that live on the same route
|
|
32
|
+
* (e.g. the tabs of a profile screen). Combined with the active route key to
|
|
33
|
+
* form the storage key. Omit when a route has a single scrollable.
|
|
34
|
+
*/
|
|
35
|
+
key?: string;
|
|
36
|
+
/**
|
|
37
|
+
* When `false`, the hook is inert (saves and restores are skipped). Useful to
|
|
38
|
+
* gate restoration behind a feature flag without changing call sites.
|
|
39
|
+
* Defaults to `true`.
|
|
40
|
+
*/
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface ScrollRestorationProviderProps {
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/scroll/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7E,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAC/B,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAClC,SAAS,CAAC,OAAO,CAAC,GAClB,QAAQ,CAAC;AAEb,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native variant of the scroll-restoration primitive — a deliberate no-op.
|
|
3
|
+
*
|
|
4
|
+
* React Navigation's native-stack keeps every screen mounted while it is in the
|
|
5
|
+
* stack, so a list's scroll position survives a push/pop for free. There is
|
|
6
|
+
* nothing to save or restore. We still ship the full API surface (provider +
|
|
7
|
+
* hook) so consumers write one set of call sites that compile and run on every
|
|
8
|
+
* platform; on native the provider just renders its children and the hook does
|
|
9
|
+
* nothing.
|
|
10
|
+
*
|
|
11
|
+
* Web bundlers select `./index.web` via the `"browser"` export condition in
|
|
12
|
+
* `package.json`; native bundlers fall through to this file.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactElement } from 'react';
|
|
15
|
+
import type { ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions } from './types';
|
|
16
|
+
export type { ScrollableHandle, ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions, } from './types';
|
|
17
|
+
/**
|
|
18
|
+
* No-op provider. Renders children unchanged — native scroll persistence is
|
|
19
|
+
* handled by the navigator, so no per-route state is kept.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ScrollRestorationProvider({ children, }: ScrollRestorationProviderProps): ReactElement;
|
|
22
|
+
/**
|
|
23
|
+
* No-op hook. Accepts the same arguments as the web implementation so call
|
|
24
|
+
* sites are identical across platforms.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useScrollRestoration(_target: ScrollRestorationTarget, _options?: UseScrollRestorationOptions): void;
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAC1C,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;AAEjB;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,GACT,EAAE,8BAA8B,GAAG,YAAY,CAE/C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,2BAA2B,GACrC,IAAI,CAEN"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions } from './types';
|
|
2
|
+
export type { ScrollableHandle, ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions, } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Holds the per-route offset map for the subtree. One provider near the app
|
|
5
|
+
* root is enough; the store lives for the document's lifetime so offsets
|
|
6
|
+
* survive navigating away and back (including browser Back/Forward).
|
|
7
|
+
*/
|
|
8
|
+
export declare function ScrollRestorationProvider({ children, }: ScrollRestorationProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
/**
|
|
10
|
+
* Preserve and restore the scroll offset of `target` across navigation, keyed
|
|
11
|
+
* by the active route (plus an optional `options.key` for routes that host
|
|
12
|
+
* multiple scrollables).
|
|
13
|
+
*
|
|
14
|
+
* Behaviour (web):
|
|
15
|
+
* - On every scroll while the screen is focused, the current offset is saved.
|
|
16
|
+
* - On focus, the saved offset is applied in a single `requestAnimationFrame`,
|
|
17
|
+
* giving a remounted list one frame to lay out its content first (no retry
|
|
18
|
+
* loops, no hide/show tricks).
|
|
19
|
+
* - On blur, the latest offset is captured as a final safety net.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useScrollRestoration(target: ScrollRestorationTarget, options?: UseScrollRestorationOptions): void;
|
|
22
|
+
//# sourceMappingURL=index.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.web.tsx"],"names":[],"mappings":"AAmBA,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;AAgBjB;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,GACT,EAAE,8BAA8B,2CAOhC;AAYD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI,CAyDN"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ScrollRestorationTarget } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* A normalized read/write interface over whatever the caller registered, so
|
|
4
|
+
* the hook does not branch on target shape. Both methods are no-ops when the
|
|
5
|
+
* underlying element is not yet (or no longer) attached.
|
|
6
|
+
*/
|
|
7
|
+
export interface ResolvedScroller {
|
|
8
|
+
getOffset: () => number;
|
|
9
|
+
setOffset: (offset: number) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build a {@link ResolvedScroller} for a target. The window sentinel reads and
|
|
13
|
+
* writes the document scroller; everything else operates on the resolved
|
|
14
|
+
* element's `scrollTop`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createScroller(target: ScrollRestorationTarget): ResolvedScroller;
|
|
17
|
+
//# sourceMappingURL=scrollable.web.d.ts.map
|
|
@@ -0,0 +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;CACrC;AAuCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,uBAAuB,GAAG,gBAAgB,CAoBhF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, platform-agnostic core of the scroll-restoration primitive.
|
|
3
|
+
*
|
|
4
|
+
* This file deliberately contains NO React and NO DOM/native imports so the
|
|
5
|
+
* logic can be unit-tested in isolation and shared verbatim by the web and
|
|
6
|
+
* native barrels. The web barrel drives it with real scroll offsets; the
|
|
7
|
+
* native barrel never instantiates it (native-stack already restores scroll).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Derive the storage key for a scrollable from its owning route key and an
|
|
11
|
+
* optional caller sub-key.
|
|
12
|
+
*
|
|
13
|
+
* - `routeKey` is React Navigation's stable per-route `route.key`.
|
|
14
|
+
* - `subKey` distinguishes multiple scrollables that share a single route.
|
|
15
|
+
*
|
|
16
|
+
* Returns `null` when there is no route key to anchor against (the scrollable
|
|
17
|
+
* is not inside a navigator), which the caller treats as "do not persist".
|
|
18
|
+
*/
|
|
19
|
+
export declare function deriveScrollKey(routeKey: string | undefined, subKey?: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* In-memory map of `scrollKey -> last-known scroll offset`.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors the semantics of Bluesky's `Map<screenKey, scrollY>`: offsets live
|
|
24
|
+
* only for the lifetime of the document/session. We never persist them — a
|
|
25
|
+
* full reload should start at the top — and we never auto-evict, because a
|
|
26
|
+
* route can be revisited via browser Forward/Back long after it blurred.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ScrollOffsetStore {
|
|
29
|
+
private readonly offsets;
|
|
30
|
+
/** Persist the offset for a key. A negative offset is clamped to 0. */
|
|
31
|
+
save(key: string, offset: number): void;
|
|
32
|
+
/**
|
|
33
|
+
* Read the saved offset for a key. Returns `0` when nothing was saved, so
|
|
34
|
+
* callers can restore unconditionally (an unseen list restores to the top).
|
|
35
|
+
*/
|
|
36
|
+
read(key: string): number;
|
|
37
|
+
/** Whether an offset was ever saved for this key. */
|
|
38
|
+
has(key: string): boolean;
|
|
39
|
+
/** Drop a saved offset (e.g. when a route is permanently removed). */
|
|
40
|
+
forget(key: string): void;
|
|
41
|
+
/** Clear every saved offset. Primarily for tests and full resets. */
|
|
42
|
+
clear(): void;
|
|
43
|
+
/** Number of distinct keys currently tracked. */
|
|
44
|
+
get size(): number;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../../src/scroll/store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAcH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAIf;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,uEAAuE;IACvE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAIvC;;;OAGG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAIzB,qDAAqD;IACrD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB,sEAAsE;IACtE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,qEAAqE;IACrE,KAAK,IAAI,IAAI;IAIb,iDAAiD;IACjD,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode, RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Anything `useScrollRestoration` knows how to read/write a vertical scroll
|
|
4
|
+
* offset from. The hook accepts a ref to one of these:
|
|
5
|
+
*
|
|
6
|
+
* - a React Native `ScrollView` / `FlatList` ref (which on web is backed by a
|
|
7
|
+
* DOM node and exposes `getScrollableNode()`),
|
|
8
|
+
* - a raw DOM element ref (when a component renders its own scroll container),
|
|
9
|
+
* - the literal `'window'` sentinel, for the rare layout where the list IS the
|
|
10
|
+
* window scroller (matches Bluesky's default).
|
|
11
|
+
*
|
|
12
|
+
* Native never reads any of these — the native hook is a no-op — so the type is
|
|
13
|
+
* intentionally permissive rather than coupled to a specific RN class.
|
|
14
|
+
*/
|
|
15
|
+
export interface ScrollableHandle {
|
|
16
|
+
/** Imperative scroll API exposed by RN scrollables. */
|
|
17
|
+
scrollTo?: (options: {
|
|
18
|
+
x?: number;
|
|
19
|
+
y?: number;
|
|
20
|
+
animated?: boolean;
|
|
21
|
+
}) => void;
|
|
22
|
+
/** Web/RNW path: returns the underlying DOM node. */
|
|
23
|
+
getScrollableNode?: () => unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The accepted ref shapes. `'window'` is a sentinel for the document scroller.
|
|
27
|
+
*/
|
|
28
|
+
export type ScrollRestorationTarget = RefObject<ScrollableHandle | null> | RefObject<unknown> | 'window';
|
|
29
|
+
export interface UseScrollRestorationOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Sub-key to disambiguate multiple scrollables that live on the same route
|
|
32
|
+
* (e.g. the tabs of a profile screen). Combined with the active route key to
|
|
33
|
+
* form the storage key. Omit when a route has a single scrollable.
|
|
34
|
+
*/
|
|
35
|
+
key?: string;
|
|
36
|
+
/**
|
|
37
|
+
* When `false`, the hook is inert (saves and restores are skipped). Useful to
|
|
38
|
+
* gate restoration behind a feature flag without changing call sites.
|
|
39
|
+
* Defaults to `true`.
|
|
40
|
+
*/
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface ScrollRestorationProviderProps {
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/scroll/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7E,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAC/B,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAClC,SAAS,CAAC,OAAO,CAAC,GAClB,QAAQ,CAAC;AAEb,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/bloom",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.22",
|
|
4
4
|
"description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -535,6 +535,22 @@
|
|
|
535
535
|
"default": "./lib/commonjs/fonts/index.js"
|
|
536
536
|
}
|
|
537
537
|
},
|
|
538
|
+
"./scroll": {
|
|
539
|
+
"react-native": "./src/scroll/index.ts",
|
|
540
|
+
"browser": {
|
|
541
|
+
"types": "./lib/typescript/module/scroll/index.web.d.ts",
|
|
542
|
+
"import": "./lib/module/scroll/index.web.js",
|
|
543
|
+
"require": "./lib/commonjs/scroll/index.web.js"
|
|
544
|
+
},
|
|
545
|
+
"import": {
|
|
546
|
+
"types": "./lib/typescript/module/scroll/index.d.ts",
|
|
547
|
+
"default": "./lib/module/scroll/index.js"
|
|
548
|
+
},
|
|
549
|
+
"require": {
|
|
550
|
+
"types": "./lib/typescript/commonjs/scroll/index.d.ts",
|
|
551
|
+
"default": "./lib/commonjs/scroll/index.js"
|
|
552
|
+
}
|
|
553
|
+
},
|
|
538
554
|
"./package.json": "./package.json"
|
|
539
555
|
},
|
|
540
556
|
"files": [
|
|
@@ -615,6 +631,7 @@
|
|
|
615
631
|
}
|
|
616
632
|
},
|
|
617
633
|
"devDependencies": {
|
|
634
|
+
"@react-navigation/native": "^7.0.0",
|
|
618
635
|
"@storybook/addon-docs": "^10",
|
|
619
636
|
"@storybook/react-vite": "^10",
|
|
620
637
|
"@testing-library/react-native": "^13.3.3",
|
|
@@ -645,6 +662,7 @@
|
|
|
645
662
|
"vite": "^7"
|
|
646
663
|
},
|
|
647
664
|
"peerDependencies": {
|
|
665
|
+
"@react-navigation/native": ">=6.0.0",
|
|
648
666
|
"expo": "*",
|
|
649
667
|
"expo-font": "*",
|
|
650
668
|
"react": ">=18.0.0",
|
|
@@ -658,6 +676,9 @@
|
|
|
658
676
|
"sonner-native": ">=0.17.0"
|
|
659
677
|
},
|
|
660
678
|
"peerDependenciesMeta": {
|
|
679
|
+
"@react-navigation/native": {
|
|
680
|
+
"optional": true
|
|
681
|
+
},
|
|
661
682
|
"expo": {
|
|
662
683
|
"optional": true
|
|
663
684
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ScrollRestorationProvider,
|
|
3
|
+
useScrollRestoration,
|
|
4
|
+
} from '../scroll/index';
|
|
5
|
+
|
|
6
|
+
// The native barrel is a deliberate no-op: native-stack already preserves
|
|
7
|
+
// scroll. These tests pin that contract so a future refactor can't silently
|
|
8
|
+
// turn the native path into something that touches the DOM or throws.
|
|
9
|
+
|
|
10
|
+
describe('native scroll-restoration barrel', () => {
|
|
11
|
+
it('ScrollRestorationProvider renders its children unchanged', () => {
|
|
12
|
+
const child = { sentinel: true } as unknown as React.ReactElement;
|
|
13
|
+
expect(ScrollRestorationProvider({ children: child })).toBe(child);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('useScrollRestoration is a no-op for any target/options', () => {
|
|
17
|
+
const ref = { current: null };
|
|
18
|
+
expect(() => useScrollRestoration(ref)).not.toThrow();
|
|
19
|
+
expect(() => useScrollRestoration('window')).not.toThrow();
|
|
20
|
+
expect(() =>
|
|
21
|
+
useScrollRestoration(ref, { key: 'feed', enabled: true }),
|
|
22
|
+
).not.toThrow();
|
|
23
|
+
expect(useScrollRestoration(ref)).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ScrollOffsetStore, deriveScrollKey } from '../scroll/store';
|
|
2
|
+
|
|
3
|
+
describe('deriveScrollKey', () => {
|
|
4
|
+
it('returns null when there is no route key', () => {
|
|
5
|
+
expect(deriveScrollKey(undefined)).toBeNull();
|
|
6
|
+
expect(deriveScrollKey(undefined, 'feed')).toBeNull();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns the route key verbatim when no sub-key is given', () => {
|
|
10
|
+
expect(deriveScrollKey('Home-abc')).toBe('Home-abc');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('treats an empty sub-key the same as no sub-key', () => {
|
|
14
|
+
expect(deriveScrollKey('Home-abc', '')).toBe('Home-abc');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('composes route key and sub-key for multiple lists on one route', () => {
|
|
18
|
+
const a = deriveScrollKey('Profile-xyz', 'posts');
|
|
19
|
+
const b = deriveScrollKey('Profile-xyz', 'media');
|
|
20
|
+
expect(a).not.toBe(b);
|
|
21
|
+
expect(a).toBe('Profile-xyz\0posts');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does not collide across routes that share a sub-key', () => {
|
|
25
|
+
expect(deriveScrollKey('A', 'feed')).not.toBe(deriveScrollKey('B', 'feed'));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('ScrollOffsetStore', () => {
|
|
30
|
+
it('reads 0 for an unseen key so callers can restore unconditionally', () => {
|
|
31
|
+
const store = new ScrollOffsetStore();
|
|
32
|
+
expect(store.read('missing')).toBe(0);
|
|
33
|
+
expect(store.has('missing')).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('saves and reads an offset back', () => {
|
|
37
|
+
const store = new ScrollOffsetStore();
|
|
38
|
+
store.save('k', 420);
|
|
39
|
+
expect(store.read('k')).toBe(420);
|
|
40
|
+
expect(store.has('k')).toBe(true);
|
|
41
|
+
expect(store.size).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keeps offsets independent per key', () => {
|
|
45
|
+
const store = new ScrollOffsetStore();
|
|
46
|
+
store.save('a', 100);
|
|
47
|
+
store.save('b', 250);
|
|
48
|
+
expect(store.read('a')).toBe(100);
|
|
49
|
+
expect(store.read('b')).toBe(250);
|
|
50
|
+
expect(store.size).toBe(2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('overwrites the offset on subsequent saves', () => {
|
|
54
|
+
const store = new ScrollOffsetStore();
|
|
55
|
+
store.save('k', 100);
|
|
56
|
+
store.save('k', 300);
|
|
57
|
+
expect(store.read('k')).toBe(300);
|
|
58
|
+
expect(store.size).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('clamps negative offsets to 0 (e.g. rubber-band overscroll)', () => {
|
|
62
|
+
const store = new ScrollOffsetStore();
|
|
63
|
+
store.save('k', -50);
|
|
64
|
+
expect(store.read('k')).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('forgets a single key', () => {
|
|
68
|
+
const store = new ScrollOffsetStore();
|
|
69
|
+
store.save('a', 1);
|
|
70
|
+
store.save('b', 2);
|
|
71
|
+
store.forget('a');
|
|
72
|
+
expect(store.has('a')).toBe(false);
|
|
73
|
+
expect(store.read('b')).toBe(2);
|
|
74
|
+
expect(store.size).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('clears every key', () => {
|
|
78
|
+
const store = new ScrollOffsetStore();
|
|
79
|
+
store.save('a', 1);
|
|
80
|
+
store.save('b', 2);
|
|
81
|
+
store.clear();
|
|
82
|
+
expect(store.size).toBe(0);
|
|
83
|
+
expect(store.has('a')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|