@charcuterie/ui 3.34.0 → 4.1.0
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/dist/ActionTiles/ActionTiles.d.ts +66 -8
- package/dist/ActionTiles/ActionTiles.d.ts.map +1 -1
- package/dist/ActionTiles/ActionTiles.js +88 -11
- package/dist/ActionTiles/ActionTiles.js.map +1 -1
- package/dist/Main/Main.d.ts +18 -19
- package/dist/Main/Main.d.ts.map +1 -1
- package/dist/Main/Main.js +21 -10
- package/dist/Main/Main.js.map +1 -1
- package/dist/Main/ScrollMemoryProvider.d.ts +38 -0
- package/dist/Main/ScrollMemoryProvider.d.ts.map +1 -0
- package/dist/Main/ScrollMemoryProvider.js +30 -0
- package/dist/Main/ScrollMemoryProvider.js.map +1 -0
- package/dist/Main/scrollMemory.d.ts +54 -28
- package/dist/Main/scrollMemory.d.ts.map +1 -1
- package/dist/Main/scrollMemory.js +81 -40
- package/dist/Main/scrollMemory.js.map +1 -1
- package/dist/Main/scrollMemoryContext.d.ts +18 -0
- package/dist/Main/scrollMemoryContext.d.ts.map +1 -0
- package/dist/Main/scrollMemoryContext.js +18 -0
- package/dist/Main/scrollMemoryContext.js.map +1 -0
- package/dist/Main/useScrollMemory.d.ts +20 -18
- package/dist/Main/useScrollMemory.d.ts.map +1 -1
- package/dist/Main/useScrollMemory.js +69 -42
- package/dist/Main/useScrollMemory.js.map +1 -1
- package/dist/PortraitTiles/PortraitAvatar.d.ts +21 -0
- package/dist/PortraitTiles/PortraitAvatar.d.ts.map +1 -0
- package/dist/PortraitTiles/PortraitAvatar.js +29 -0
- package/dist/PortraitTiles/PortraitAvatar.js.map +1 -0
- package/dist/PortraitTiles/PortraitTiles.d.ts +162 -0
- package/dist/PortraitTiles/PortraitTiles.d.ts.map +1 -0
- package/dist/PortraitTiles/PortraitTiles.js +263 -0
- package/dist/PortraitTiles/PortraitTiles.js.map +1 -0
- package/dist/PortraitTiles/portraitColour.d.ts +49 -0
- package/dist/PortraitTiles/portraitColour.d.ts.map +1 -0
- package/dist/PortraitTiles/portraitColour.js +55 -0
- package/dist/PortraitTiles/portraitColour.js.map +1 -0
- package/dist/categoricalStyles.d.ts +27 -0
- package/dist/categoricalStyles.d.ts.map +1 -1
- package/dist/categoricalStyles.js +49 -0
- package/dist/categoricalStyles.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/reactRouter/ReactRouterAdapter.d.ts +43 -0
- package/dist/reactRouter/ReactRouterAdapter.d.ts.map +1 -0
- package/dist/reactRouter/ReactRouterAdapter.js +50 -0
- package/dist/reactRouter/ReactRouterAdapter.js.map +1 -0
- package/dist/reactRouter/index.d.ts +14 -8
- package/dist/reactRouter/index.d.ts.map +1 -1
- package/dist/reactRouter/index.js +13 -8
- package/dist/reactRouter/index.js.map +1 -1
- package/dist/tileStyles.d.ts +15 -0
- package/dist/tileStyles.d.ts.map +1 -1
- package/dist/tileStyles.js +26 -1
- package/dist/tileStyles.js.map +1 -1
- package/package.json +2 -2
- package/src/ActionTiles/ActionTiles.mdx +64 -6
- package/src/ActionTiles/ActionTiles.stories.tsx +107 -2
- package/src/ActionTiles/ActionTiles.test.tsx +205 -5
- package/src/ActionTiles/ActionTiles.tsx +274 -110
- package/src/Main/Main.mdx +35 -7
- package/src/Main/Main.stories.tsx +129 -64
- package/src/Main/Main.test.tsx +58 -0
- package/src/Main/Main.tsx +20 -20
- package/src/Main/ScrollMemoryProvider.tsx +48 -0
- package/src/Main/scrollMemory.test.ts +70 -16
- package/src/Main/scrollMemory.ts +98 -40
- package/src/Main/scrollMemoryContext.ts +21 -0
- package/src/Main/useScrollMemory.ts +83 -43
- package/src/PortraitTiles/PortraitAvatar.tsx +49 -0
- package/src/PortraitTiles/PortraitTiles.mdx +153 -0
- package/src/PortraitTiles/PortraitTiles.stories.tsx +403 -0
- package/src/PortraitTiles/PortraitTiles.test.tsx +399 -0
- package/src/PortraitTiles/PortraitTiles.tsx +577 -0
- package/src/PortraitTiles/portraitColour.test.ts +39 -0
- package/src/PortraitTiles/portraitColour.ts +67 -0
- package/src/RadioGroup/RadioGroup.stories.tsx +39 -0
- package/src/RouterLink/RouterLink.mdx +53 -16
- package/src/categoricalStyles.ts +57 -0
- package/src/index.ts +12 -0
- package/src/reactRouter/ReactRouterAdapter.stories.tsx +120 -0
- package/src/reactRouter/ReactRouterAdapter.test.tsx +92 -0
- package/src/reactRouter/ReactRouterAdapter.tsx +66 -0
- package/src/reactRouter/index.ts +14 -8
- package/src/sourceRules.test.ts +19 -3
- package/src/tileStyles.ts +30 -1
|
@@ -1,63 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What the browser will not remember for you
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* does **not** do it for an arbitrary element with
|
|
14
|
-
* `overflow-y: auto`, and it does not do it at all for a
|
|
15
|
-
* same-document `pushState` navigation, which is every link press
|
|
16
|
-
* in a single-page app. So a page that scrolls its own `<main>`
|
|
17
|
-
* loses the reader's place on Back, and no browser setting brings
|
|
18
|
-
* it back:
|
|
2
|
+
* What the browser will not remember for you, and the one thing it
|
|
3
|
+
* would never have got wrong.
|
|
4
|
+
*
|
|
5
|
+
* `Shell` gives `<main>` the page's only vertical scrollport, so the
|
|
6
|
+
* header and the rails stay put while the content moves — that is
|
|
7
|
+
* what `Shell.test.tsx` asserts. It costs exactly one thing, and
|
|
8
|
+
* this module is it. A browser remembers **the document scroller's**
|
|
9
|
+
* offset per history entry and puts it back on Back. It does not do
|
|
10
|
+
* that for an arbitrary element with `overflow-y: auto`, and it does
|
|
11
|
+
* not do it at all for a same-document `pushState` navigation, which
|
|
12
|
+
* is every link press in a single-page app:
|
|
19
13
|
*
|
|
20
14
|
* > *"If I scroll, click on an item, then go back, it resets me to
|
|
21
15
|
* > the top. […] It should remember where the scroll position was
|
|
22
16
|
* > previously. Isn't this handled by Chrome already?"*
|
|
23
17
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
18
|
+
* ## An entry remembers its PATH as well as its offset
|
|
19
|
+
*
|
|
20
|
+
* Not decoration — the second field is what keeps this from being a
|
|
21
|
+
* worse bug than the one it fixes. A history entry is not a page. A
|
|
22
|
+
* filter chip, an expanded group, a selected tab: each writes a
|
|
23
|
+
* search param, and **`setSearchParams` navigates**, so each mints a
|
|
24
|
+
* history entry the memory has never seen. Treating an unseen entry
|
|
25
|
+
* as "start at the top" therefore threw the reader to the top of the
|
|
26
|
+
* list every time they opened a group — which is exactly what
|
|
27
|
+
* shipped, and what the path comparison exists to prevent.
|
|
28
|
+
*
|
|
29
|
+
* So the rule is about the PAGE, not the entry:
|
|
30
|
+
*
|
|
31
|
+
* - **An entry we have seen** → its remembered offset. This is the
|
|
32
|
+
* whole feature: Back and Forward land where you left them.
|
|
33
|
+
* - **A new entry on the SAME path** → leave the scrollport alone.
|
|
34
|
+
* A search param changed; the reader did not go anywhere. This is
|
|
35
|
+
* also precisely what the app did before this memory existed, so
|
|
36
|
+
* it can never be a regression.
|
|
37
|
+
* - **A new entry on a DIFFERENT path** → the top. A page you have
|
|
38
|
+
* not seen starts at its beginning, which is what a browser does
|
|
39
|
+
* for a document.
|
|
40
|
+
*
|
|
41
|
+
* ## The offsets are in memory, never `sessionStorage`
|
|
42
|
+
*
|
|
43
|
+
* A history key survives a reload; the DOM it described does not.
|
|
44
|
+
* The app re-fetches, re-measures and often re-orders, so a pixel
|
|
45
|
+
* offset from before the reload points at a different row. Restoring
|
|
46
|
+
* a stale number is worse than restoring nothing, because it looks
|
|
47
|
+
* deliberate.
|
|
30
48
|
*/
|
|
31
49
|
/**
|
|
32
50
|
* How many history entries keep an offset. A reader goes back
|
|
33
51
|
* through a handful of screens, never fifty, so this is a ceiling
|
|
34
52
|
* against a long session rather than a working set — the map holds
|
|
35
|
-
* one small
|
|
36
|
-
* written.
|
|
53
|
+
* one small record per entry and evicts the least recently written.
|
|
37
54
|
*/
|
|
38
55
|
const REMEMBERED_ENTRY_LIMIT = 50;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
const rememberedEntries = new Map();
|
|
57
|
+
/**
|
|
58
|
+
* The entry the scrollport is showing. Module state rather than a
|
|
59
|
+
* ref, because the question it answers — *did the reader change page
|
|
60
|
+
* or only a search param?* — is asked by the incoming entry's effect
|
|
61
|
+
* about the OUTGOING one, and by then the outgoing effect has
|
|
62
|
+
* already been cleaned up.
|
|
63
|
+
*/
|
|
64
|
+
let currentEntry = null;
|
|
65
|
+
export const rememberScrollOffset = (entry, offset) => {
|
|
66
|
+
// Delete before set, so insertion order is least-recent-first and
|
|
67
|
+
// the eviction below takes the right one. A plain `set` on an
|
|
68
|
+
// existing key keeps its original position.
|
|
69
|
+
rememberedEntries.delete(entry.key);
|
|
70
|
+
rememberedEntries.set(entry.key, {
|
|
71
|
+
offset,
|
|
72
|
+
path: entry.path,
|
|
73
|
+
});
|
|
74
|
+
for (const oldestKey of rememberedEntries.keys()) {
|
|
75
|
+
if (rememberedEntries.size <= REMEMBERED_ENTRY_LIMIT) {
|
|
48
76
|
break;
|
|
49
77
|
}
|
|
50
|
-
|
|
78
|
+
rememberedEntries.delete(oldestKey);
|
|
51
79
|
}
|
|
52
80
|
};
|
|
53
81
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* the
|
|
82
|
+
* Where the scrollport should go for this entry, or `null` for
|
|
83
|
+
* **leave it alone** — which is a third answer rather than a missing
|
|
84
|
+
* one, and the difference between a search param and a page.
|
|
57
85
|
*/
|
|
58
|
-
export const
|
|
86
|
+
export const resolveScrollOffset = (entry) => {
|
|
87
|
+
const remembered = rememberedEntries.get(entry.key);
|
|
88
|
+
if (remembered) {
|
|
89
|
+
return remembered.offset;
|
|
90
|
+
}
|
|
91
|
+
const isSamePage = currentEntry !== null &&
|
|
92
|
+
currentEntry.path === entry.path;
|
|
93
|
+
return isSamePage ? null : 0;
|
|
94
|
+
};
|
|
95
|
+
/** Called by the hook once it owns the scrollport for this entry. */
|
|
96
|
+
export const enterScrollEntry = (entry) => {
|
|
97
|
+
currentEntry = entry;
|
|
98
|
+
};
|
|
59
99
|
/** Test-only. Module state outlives a mount; a test must not. */
|
|
60
100
|
export const forgetScrollOffsets = () => {
|
|
61
|
-
|
|
101
|
+
rememberedEntries.clear();
|
|
102
|
+
currentEntry = null;
|
|
62
103
|
};
|
|
63
104
|
//# sourceMappingURL=scrollMemory.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scrollMemory.js","sourceRoot":"","sources":["../../src/Main/scrollMemory.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"scrollMemory.js","sourceRoot":"","sources":["../../src/Main/scrollMemory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH;;;;;GAKG;AACH,MAAM,sBAAsB,GAAG,EAAE,CAAA;AAcjC,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAA2B,CAAA;AAE5D;;;;;;GAMG;AACH,IAAI,YAAY,GAAuB,IAAI,CAAA;AAE3C,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,KAAkB,EAClB,MAAc,EACd,EAAE;IACF,kEAAkE;IAClE,8DAA8D;IAC9D,4CAA4C;IAC5C,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACnC,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE;QAC/B,MAAM;QACN,IAAI,EAAE,KAAK,CAAC,IAAI;KACjB,CAAC,CAAA;IAEF,KAAK,MAAM,SAAS,IAAI,iBAAiB,CAAC,IAAI,EAAE,EAAE,CAAC;QACjD,IAAI,iBAAiB,CAAC,IAAI,IAAI,sBAAsB,EAAE,CAAC;YACrD,MAAK;QACP,CAAC;QAED,iBAAiB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACrC,CAAC;AACH,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,KAAkB,EAAE,EAAE;IACxD,MAAM,UAAU,GAAG,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAEnD,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,UAAU,CAAC,MAAM,CAAA;IAC1B,CAAC;IAED,MAAM,UAAU,GACd,YAAY,KAAK,IAAI;QACrB,YAAY,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAA;IAElC,OAAO,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;AAC9B,CAAC,CAAA;AAED,qEAAqE;AACrE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAkB,EAAE,EAAE;IACrD,YAAY,GAAG,KAAK,CAAA;AACtB,CAAC,CAAA;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,EAAE;IACtC,iBAAiB,CAAC,KAAK,EAAE,CAAA;IACzB,YAAY,GAAG,IAAI,CAAA;AACrB,CAAC,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ScrollEntry } from "./scrollMemory.ts";
|
|
2
|
+
/**
|
|
3
|
+
* How the history entry reaches `Main` without `Main` knowing a
|
|
4
|
+
* router exists.
|
|
5
|
+
*
|
|
6
|
+
* `@charcuterie/ui`'s main entry is router-free and stays that way —
|
|
7
|
+
* `sourceRules.test.ts` asserts it, and the fleet runs five
|
|
8
|
+
* different routers. So the entry arrives as a plain
|
|
9
|
+
* `{ key, path }`, and the one component that knows how to read it
|
|
10
|
+
* from react-router lives in the `./react-router` subpath.
|
|
11
|
+
*
|
|
12
|
+
* A context rather than a prop, because the alternative is a prop
|
|
13
|
+
* every app has to remember on every `Main` it renders. It was a
|
|
14
|
+
* prop first, exactly once, and three of the four apps that needed
|
|
15
|
+
* it did not have it.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ScrollMemoryContext: import("react").Context<ScrollEntry | null>;
|
|
18
|
+
//# sourceMappingURL=scrollMemoryContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrollMemoryContext.d.ts","sourceRoot":"","sources":["../../src/Main/scrollMemoryContext.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,6CACS,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* How the history entry reaches `Main` without `Main` knowing a
|
|
4
|
+
* router exists.
|
|
5
|
+
*
|
|
6
|
+
* `@charcuterie/ui`'s main entry is router-free and stays that way —
|
|
7
|
+
* `sourceRules.test.ts` asserts it, and the fleet runs five
|
|
8
|
+
* different routers. So the entry arrives as a plain
|
|
9
|
+
* `{ key, path }`, and the one component that knows how to read it
|
|
10
|
+
* from react-router lives in the `./react-router` subpath.
|
|
11
|
+
*
|
|
12
|
+
* A context rather than a prop, because the alternative is a prop
|
|
13
|
+
* every app has to remember on every `Main` it renders. It was a
|
|
14
|
+
* prop first, exactly once, and three of the four apps that needed
|
|
15
|
+
* it did not have it.
|
|
16
|
+
*/
|
|
17
|
+
export const ScrollMemoryContext = createContext(null);
|
|
18
|
+
//# sourceMappingURL=scrollMemoryContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrollMemoryContext.js","sourceRoot":"","sources":["../../src/Main/scrollMemoryContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAIrC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,aAAa,CAAqB,IAAI,CAAC,CAAA"}
|
|
@@ -1,29 +1,31 @@
|
|
|
1
|
+
import { type ScrollEntry } from "./scrollMemory.ts";
|
|
1
2
|
/**
|
|
2
|
-
* Remember where this history entry was scrolled to, and put it
|
|
3
|
-
*
|
|
3
|
+
* Remember where this history entry was scrolled to, and put it back
|
|
4
|
+
* when the reader returns to it.
|
|
4
5
|
*
|
|
5
|
-
* `scrollMemory.ts` says why the browser will not
|
|
6
|
-
*
|
|
6
|
+
* `scrollMemory.ts` says why the browser will not, and what an entry
|
|
7
|
+
* that has never been seen should do. This is the part that has to
|
|
8
|
+
* survive the two things a real page does:
|
|
7
9
|
*
|
|
8
10
|
* 1. **The content arrives late.** An offset applied against a
|
|
9
|
-
* 600px skeleton clamps to `0` and looks like it did nothing.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 2. **The reader scrolls during all that.** Any wheel, touch,
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* 600px skeleton clamps to `0` and looks like it did nothing. A
|
|
12
|
+
* `ResizeObserver` on the scrollport and on its content column
|
|
13
|
+
* re-applies the offset every time either grows, until the
|
|
14
|
+
* offset lands or the window closes.
|
|
15
|
+
* 2. **The reader scrolls during all that.** Any wheel, touch, key
|
|
16
|
+
* or pointer press ends the restore immediately. The alternative
|
|
17
|
+
* is a page that jumps out from under somebody who has already
|
|
18
|
+
* started reading.
|
|
17
19
|
*
|
|
18
20
|
* The save is a passive `scroll` listener, suppressed while a
|
|
19
21
|
* restore is in flight. Without that suppression the feature eats
|
|
20
22
|
* itself: writing `scrollTop` on a still-short page clamps to `0`
|
|
21
|
-
* and fires a `scroll` event reading `0`, which would be saved
|
|
22
|
-
*
|
|
23
|
+
* and fires a `scroll` event reading `0`, which would be saved over
|
|
24
|
+
* the very offset being restored.
|
|
23
25
|
*
|
|
24
|
-
* @param
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
26
|
+
* @param entry The history entry and the page it belongs to.
|
|
27
|
+
* `null` turns the memory off, which is what an app that renders
|
|
28
|
+
* no `ScrollMemoryProvider` gets.
|
|
27
29
|
*/
|
|
28
|
-
export declare const useScrollMemory: (
|
|
30
|
+
export declare const useScrollMemory: (entry: ScrollEntry | null | undefined) => (node: unknown) => void;
|
|
29
31
|
//# sourceMappingURL=useScrollMemory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useScrollMemory.d.ts","sourceRoot":"","sources":["../../src/Main/useScrollMemory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useScrollMemory.d.ts","sourceRoot":"","sources":["../../src/Main/useScrollMemory.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,mBAAmB,CAAA;AAwB1B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,WAAW,GAAG,IAAI,GAAG,SAAS,YAaC,OAAO,SAkI9C,CAAA"}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { enterScrollEntry, rememberScrollOffset, resolveScrollOffset, } from "./scrollMemory.js";
|
|
3
3
|
/**
|
|
4
4
|
* How long a restore keeps trying while the content is still
|
|
5
5
|
* arriving.
|
|
6
6
|
*
|
|
7
7
|
* The offset cannot be applied until the scrollport is tall enough
|
|
8
8
|
* to hold it, and on a client-rendered page it is not: the commit
|
|
9
|
-
* that changes the route draws a skeleton, and the rows land a
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* that changes the route draws a skeleton, and the rows land a fetch
|
|
10
|
+
* later. So the restore re-applies as the content grows, and gives
|
|
11
|
+
* up after this. A page that takes longer than three seconds to fill
|
|
12
|
+
* has already been read from the top, and moving it then is a worse
|
|
13
|
+
* answer than leaving it where the reader put it.
|
|
14
14
|
*/
|
|
15
15
|
const RESTORE_WINDOW_MS = 3000;
|
|
16
16
|
/** A scroll the reader asked for. It ends the restore at once. */
|
|
@@ -21,38 +21,39 @@ const READER_INTENT_EVENTS = [
|
|
|
21
21
|
"wheel",
|
|
22
22
|
];
|
|
23
23
|
/**
|
|
24
|
-
* Remember where this history entry was scrolled to, and put it
|
|
25
|
-
*
|
|
24
|
+
* Remember where this history entry was scrolled to, and put it back
|
|
25
|
+
* when the reader returns to it.
|
|
26
26
|
*
|
|
27
|
-
* `scrollMemory.ts` says why the browser will not
|
|
28
|
-
*
|
|
27
|
+
* `scrollMemory.ts` says why the browser will not, and what an entry
|
|
28
|
+
* that has never been seen should do. This is the part that has to
|
|
29
|
+
* survive the two things a real page does:
|
|
29
30
|
*
|
|
30
31
|
* 1. **The content arrives late.** An offset applied against a
|
|
31
|
-
* 600px skeleton clamps to `0` and looks like it did nothing.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* 2. **The reader scrolls during all that.** Any wheel, touch,
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
32
|
+
* 600px skeleton clamps to `0` and looks like it did nothing. A
|
|
33
|
+
* `ResizeObserver` on the scrollport and on its content column
|
|
34
|
+
* re-applies the offset every time either grows, until the
|
|
35
|
+
* offset lands or the window closes.
|
|
36
|
+
* 2. **The reader scrolls during all that.** Any wheel, touch, key
|
|
37
|
+
* or pointer press ends the restore immediately. The alternative
|
|
38
|
+
* is a page that jumps out from under somebody who has already
|
|
39
|
+
* started reading.
|
|
39
40
|
*
|
|
40
41
|
* The save is a passive `scroll` listener, suppressed while a
|
|
41
42
|
* restore is in flight. Without that suppression the feature eats
|
|
42
43
|
* itself: writing `scrollTop` on a still-short page clamps to `0`
|
|
43
|
-
* and fires a `scroll` event reading `0`, which would be saved
|
|
44
|
-
*
|
|
44
|
+
* and fires a `scroll` event reading `0`, which would be saved over
|
|
45
|
+
* the very offset being restored.
|
|
45
46
|
*
|
|
46
|
-
* @param
|
|
47
|
-
* `
|
|
48
|
-
*
|
|
47
|
+
* @param entry The history entry and the page it belongs to.
|
|
48
|
+
* `null` turns the memory off, which is what an app that renders
|
|
49
|
+
* no `ScrollMemoryProvider` gets.
|
|
49
50
|
*/
|
|
50
|
-
export const useScrollMemory = (
|
|
51
|
+
export const useScrollMemory = (entry) => {
|
|
51
52
|
const elementRef = useRef(null);
|
|
52
53
|
const isRestoringRef = useRef(false);
|
|
53
|
-
// A ref rather than a query for the `<main>` id: the effect
|
|
54
|
-
//
|
|
55
|
-
//
|
|
54
|
+
// A ref rather than a query for the `<main>` id: the effect below
|
|
55
|
+
// runs in the same commit that mounts the node, and only a ref can
|
|
56
|
+
// say which node that was.
|
|
56
57
|
//
|
|
57
58
|
// `unknown` in, narrowed here, so the returned function is both a
|
|
58
59
|
// `RefCallback<HTMLElement>` and something `mergeRefs` accepts —
|
|
@@ -62,34 +63,55 @@ export const useScrollMemory = (scrollKey) => {
|
|
|
62
63
|
elementRef.current =
|
|
63
64
|
node instanceof HTMLElement ? node : null;
|
|
64
65
|
}, []);
|
|
66
|
+
const key = entry?.key;
|
|
67
|
+
const path = entry?.path;
|
|
65
68
|
useLayoutEffect(() => {
|
|
66
69
|
const element = elementRef.current;
|
|
67
|
-
if (!element ||
|
|
70
|
+
if (!element ||
|
|
71
|
+
key === undefined ||
|
|
72
|
+
path === undefined) {
|
|
68
73
|
return undefined;
|
|
69
74
|
}
|
|
70
|
-
const
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
75
|
+
const currentEntry = { key, path };
|
|
76
|
+
// `null` means leave the scrollport alone: a search param
|
|
77
|
+
// changed and the reader did not go anywhere. It is read BEFORE
|
|
78
|
+
// this entry becomes the current one, because the answer depends
|
|
79
|
+
// on the page the reader is arriving from.
|
|
80
|
+
const offset = resolveScrollOffset(currentEntry);
|
|
81
|
+
enterScrollEntry(currentEntry);
|
|
82
|
+
// Seed the entry now rather than waiting for a `scroll` event.
|
|
83
|
+
// A reader who opens a group and then follows a link never
|
|
84
|
+
// scrolls in between, so without this the entry they came from
|
|
85
|
+
// is one the memory has never seen — and Back would send them to
|
|
86
|
+
// the top of a list they were half-way down.
|
|
87
|
+
rememberScrollOffset(currentEntry, offset ?? element.scrollTop);
|
|
88
|
+
// The arrow defers the read of `applyOffset`, so the pieces can
|
|
89
|
+
// be declared in the order they depend on each other rather than
|
|
90
|
+
// in the order they are used.
|
|
74
91
|
const observer = new ResizeObserver(() => {
|
|
75
92
|
applyOffset();
|
|
76
93
|
});
|
|
77
94
|
const deadline = globalThis.setTimeout(() => {
|
|
78
95
|
stopRestoring();
|
|
79
96
|
}, RESTORE_WINDOW_MS);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
isRestoringRef.current = false;
|
|
97
|
+
let isRestoring = offset !== null;
|
|
98
|
+
const teardown = () => {
|
|
85
99
|
observer.disconnect();
|
|
86
100
|
globalThis.clearTimeout(deadline);
|
|
87
101
|
for (const eventName of READER_INTENT_EVENTS) {
|
|
88
102
|
element.removeEventListener(eventName, stopRestoring);
|
|
89
103
|
}
|
|
90
104
|
};
|
|
105
|
+
const stopRestoring = () => {
|
|
106
|
+
if (!isRestoring) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
isRestoring = false;
|
|
110
|
+
isRestoringRef.current = false;
|
|
111
|
+
teardown();
|
|
112
|
+
};
|
|
91
113
|
const applyOffset = () => {
|
|
92
|
-
if (!
|
|
114
|
+
if (!isRestoring || offset === null) {
|
|
93
115
|
return;
|
|
94
116
|
}
|
|
95
117
|
element.scrollTop = offset;
|
|
@@ -102,9 +124,9 @@ export const useScrollMemory = (scrollKey) => {
|
|
|
102
124
|
if (isRestoringRef.current) {
|
|
103
125
|
return;
|
|
104
126
|
}
|
|
105
|
-
rememberScrollOffset(
|
|
127
|
+
rememberScrollOffset(currentEntry, element.scrollTop);
|
|
106
128
|
};
|
|
107
|
-
isRestoringRef.current =
|
|
129
|
+
isRestoringRef.current = isRestoring;
|
|
108
130
|
for (const eventName of READER_INTENT_EVENTS) {
|
|
109
131
|
element.addEventListener(eventName, stopRestoring, {
|
|
110
132
|
passive: true,
|
|
@@ -122,10 +144,15 @@ export const useScrollMemory = (scrollKey) => {
|
|
|
122
144
|
});
|
|
123
145
|
applyOffset();
|
|
124
146
|
return () => {
|
|
125
|
-
stopRestoring
|
|
147
|
+
// Unconditional, unlike `stopRestoring` — a restore that
|
|
148
|
+
// already landed still has an observer and four listeners on
|
|
149
|
+
// an element the next entry is about to use, and a stale
|
|
150
|
+
// observer that survives its effect applies a stale offset.
|
|
151
|
+
isRestoring = false;
|
|
152
|
+
teardown();
|
|
126
153
|
element.removeEventListener("scroll", onScroll);
|
|
127
154
|
};
|
|
128
|
-
}, [
|
|
155
|
+
}, [key, path]);
|
|
129
156
|
return setElement;
|
|
130
157
|
};
|
|
131
158
|
//# sourceMappingURL=useScrollMemory.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useScrollMemory.js","sourceRoot":"","sources":["../../src/Main/useScrollMemory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAE5D,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"useScrollMemory.js","sourceRoot":"","sources":["../../src/Main/useScrollMemory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The face, and the one piece of this component that holds state.
|
|
4
|
+
*
|
|
5
|
+
* A portrait grid is the shape that cannot absorb a broken image:
|
|
6
|
+
* a missing `<img>` is a torn hole where a person's face was, next
|
|
7
|
+
* to nine that rendered. `onError` is the only signal a browser
|
|
8
|
+
* gives for a 404, a wrong content type, or a file the user's
|
|
9
|
+
* network stripped — so the fallback the caller already supplied for
|
|
10
|
+
* "no picture" covers "no picture *today*" as well.
|
|
11
|
+
*
|
|
12
|
+
* Its own component because the state is per portrait. Hoisting it
|
|
13
|
+
* to a `Record<string, boolean>` in the set would re-render every
|
|
14
|
+
* tile whenever any one image failed.
|
|
15
|
+
*/
|
|
16
|
+
export declare const PortraitAvatar: ({ className, imageSrc, initials, }: {
|
|
17
|
+
className: string;
|
|
18
|
+
imageSrc: string | undefined;
|
|
19
|
+
initials: ReactNode;
|
|
20
|
+
}) => ReactNode;
|
|
21
|
+
//# sourceMappingURL=PortraitAvatar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PortraitAvatar.d.ts","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitAvatar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAAI,oCAI5B;IACD,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,QAAQ,EAAE,SAAS,CAAA;CACpB,KAAG,SAuBH,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* The face, and the one piece of this component that holds state.
|
|
5
|
+
*
|
|
6
|
+
* A portrait grid is the shape that cannot absorb a broken image:
|
|
7
|
+
* a missing `<img>` is a torn hole where a person's face was, next
|
|
8
|
+
* to nine that rendered. `onError` is the only signal a browser
|
|
9
|
+
* gives for a 404, a wrong content type, or a file the user's
|
|
10
|
+
* network stripped — so the fallback the caller already supplied for
|
|
11
|
+
* "no picture" covers "no picture *today*" as well.
|
|
12
|
+
*
|
|
13
|
+
* Its own component because the state is per portrait. Hoisting it
|
|
14
|
+
* to a `Record<string, boolean>` in the set would re-render every
|
|
15
|
+
* tile whenever any one image failed.
|
|
16
|
+
*/
|
|
17
|
+
export const PortraitAvatar = ({ className, imageSrc, initials, }) => {
|
|
18
|
+
const [isImageBroken, setIsImageBroken] = useState(false);
|
|
19
|
+
if (imageSrc === undefined || isImageBroken) {
|
|
20
|
+
return (_jsx("span", { "aria-hidden": true, className: className, children: initials }));
|
|
21
|
+
}
|
|
22
|
+
return (_jsx("img", {
|
|
23
|
+
// The name is beside it, inside the same tile, so an alt here
|
|
24
|
+
// is the name announced twice.
|
|
25
|
+
alt: "", className: className, onError: () => {
|
|
26
|
+
setIsImageBroken(true);
|
|
27
|
+
}, src: imageSrc }));
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=PortraitAvatar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PortraitAvatar.js","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitAvatar.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAC7B,SAAS,EACT,QAAQ,EACR,QAAQ,GAKT,EAAa,EAAE;IACd,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEzD,IAAI,QAAQ,KAAK,SAAS,IAAI,aAAa,EAAE,CAAC;QAC5C,OAAO,CACL,oCAAkB,SAAS,EAAE,SAAS,YACnC,QAAQ,GACJ,CACR,CAAA;IACH,CAAC;IAED,OAAO,CACL;QACE,8DAA8D;QAC9D,+BAA+B;QAC/B,GAAG,EAAC,EAAE,EACN,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,GAAG,EAAE;YACZ,gBAAgB,CAAC,IAAI,CAAC,CAAA;QACxB,CAAC,EACD,GAAG,EAAE,QAAQ,GACb,CACH,CAAA;AACH,CAAC,CAAA"}
|