@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.
Files changed (87) hide show
  1. package/dist/ActionTiles/ActionTiles.d.ts +66 -8
  2. package/dist/ActionTiles/ActionTiles.d.ts.map +1 -1
  3. package/dist/ActionTiles/ActionTiles.js +88 -11
  4. package/dist/ActionTiles/ActionTiles.js.map +1 -1
  5. package/dist/Main/Main.d.ts +18 -19
  6. package/dist/Main/Main.d.ts.map +1 -1
  7. package/dist/Main/Main.js +21 -10
  8. package/dist/Main/Main.js.map +1 -1
  9. package/dist/Main/ScrollMemoryProvider.d.ts +38 -0
  10. package/dist/Main/ScrollMemoryProvider.d.ts.map +1 -0
  11. package/dist/Main/ScrollMemoryProvider.js +30 -0
  12. package/dist/Main/ScrollMemoryProvider.js.map +1 -0
  13. package/dist/Main/scrollMemory.d.ts +54 -28
  14. package/dist/Main/scrollMemory.d.ts.map +1 -1
  15. package/dist/Main/scrollMemory.js +81 -40
  16. package/dist/Main/scrollMemory.js.map +1 -1
  17. package/dist/Main/scrollMemoryContext.d.ts +18 -0
  18. package/dist/Main/scrollMemoryContext.d.ts.map +1 -0
  19. package/dist/Main/scrollMemoryContext.js +18 -0
  20. package/dist/Main/scrollMemoryContext.js.map +1 -0
  21. package/dist/Main/useScrollMemory.d.ts +20 -18
  22. package/dist/Main/useScrollMemory.d.ts.map +1 -1
  23. package/dist/Main/useScrollMemory.js +69 -42
  24. package/dist/Main/useScrollMemory.js.map +1 -1
  25. package/dist/PortraitTiles/PortraitAvatar.d.ts +21 -0
  26. package/dist/PortraitTiles/PortraitAvatar.d.ts.map +1 -0
  27. package/dist/PortraitTiles/PortraitAvatar.js +29 -0
  28. package/dist/PortraitTiles/PortraitAvatar.js.map +1 -0
  29. package/dist/PortraitTiles/PortraitTiles.d.ts +162 -0
  30. package/dist/PortraitTiles/PortraitTiles.d.ts.map +1 -0
  31. package/dist/PortraitTiles/PortraitTiles.js +263 -0
  32. package/dist/PortraitTiles/PortraitTiles.js.map +1 -0
  33. package/dist/PortraitTiles/portraitColour.d.ts +49 -0
  34. package/dist/PortraitTiles/portraitColour.d.ts.map +1 -0
  35. package/dist/PortraitTiles/portraitColour.js +55 -0
  36. package/dist/PortraitTiles/portraitColour.js.map +1 -0
  37. package/dist/categoricalStyles.d.ts +27 -0
  38. package/dist/categoricalStyles.d.ts.map +1 -1
  39. package/dist/categoricalStyles.js +49 -0
  40. package/dist/categoricalStyles.js.map +1 -1
  41. package/dist/index.d.ts +7 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/reactRouter/ReactRouterAdapter.d.ts +43 -0
  46. package/dist/reactRouter/ReactRouterAdapter.d.ts.map +1 -0
  47. package/dist/reactRouter/ReactRouterAdapter.js +50 -0
  48. package/dist/reactRouter/ReactRouterAdapter.js.map +1 -0
  49. package/dist/reactRouter/index.d.ts +14 -8
  50. package/dist/reactRouter/index.d.ts.map +1 -1
  51. package/dist/reactRouter/index.js +13 -8
  52. package/dist/reactRouter/index.js.map +1 -1
  53. package/dist/tileStyles.d.ts +15 -0
  54. package/dist/tileStyles.d.ts.map +1 -1
  55. package/dist/tileStyles.js +26 -1
  56. package/dist/tileStyles.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/ActionTiles/ActionTiles.mdx +64 -6
  59. package/src/ActionTiles/ActionTiles.stories.tsx +107 -2
  60. package/src/ActionTiles/ActionTiles.test.tsx +205 -5
  61. package/src/ActionTiles/ActionTiles.tsx +274 -110
  62. package/src/Main/Main.mdx +35 -7
  63. package/src/Main/Main.stories.tsx +129 -64
  64. package/src/Main/Main.test.tsx +58 -0
  65. package/src/Main/Main.tsx +20 -20
  66. package/src/Main/ScrollMemoryProvider.tsx +48 -0
  67. package/src/Main/scrollMemory.test.ts +70 -16
  68. package/src/Main/scrollMemory.ts +98 -40
  69. package/src/Main/scrollMemoryContext.ts +21 -0
  70. package/src/Main/useScrollMemory.ts +83 -43
  71. package/src/PortraitTiles/PortraitAvatar.tsx +49 -0
  72. package/src/PortraitTiles/PortraitTiles.mdx +153 -0
  73. package/src/PortraitTiles/PortraitTiles.stories.tsx +403 -0
  74. package/src/PortraitTiles/PortraitTiles.test.tsx +399 -0
  75. package/src/PortraitTiles/PortraitTiles.tsx +577 -0
  76. package/src/PortraitTiles/portraitColour.test.ts +39 -0
  77. package/src/PortraitTiles/portraitColour.ts +67 -0
  78. package/src/RadioGroup/RadioGroup.stories.tsx +39 -0
  79. package/src/RouterLink/RouterLink.mdx +53 -16
  80. package/src/categoricalStyles.ts +57 -0
  81. package/src/index.ts +12 -0
  82. package/src/reactRouter/ReactRouterAdapter.stories.tsx +120 -0
  83. package/src/reactRouter/ReactRouterAdapter.test.tsx +92 -0
  84. package/src/reactRouter/ReactRouterAdapter.tsx +66 -0
  85. package/src/reactRouter/index.ts +14 -8
  86. package/src/sourceRules.test.ts +19 -3
  87. package/src/tileStyles.ts +30 -1
@@ -16,6 +16,7 @@ import {
16
16
  RailNavigation,
17
17
  } from "../shell.storyHelpers.tsx"
18
18
  import { Main } from "./Main.tsx"
19
+ import { ScrollMemoryProvider } from "./ScrollMemoryProvider.tsx"
19
20
 
20
21
  const CONTENT_WIDTH_OPTIONS = [
21
22
  "sm",
@@ -173,17 +174,42 @@ const EPISODE_TITLES = Array.from(
173
174
 
174
175
  type HistoryEntry = {
175
176
  episode: string | null
177
+ isExpanded: boolean
176
178
  key: string
177
179
  }
178
180
 
181
+ const SPECIAL_TITLES = Array.from(
182
+ { length: 8 },
183
+ (_unused, index) => `Special ${index + 1}`,
184
+ )
185
+
186
+ const toPath = (entry: HistoryEntry) =>
187
+ entry.episode === null
188
+ ? "/library"
189
+ : `/library/${entry.episode}`
190
+
179
191
  /**
180
- * A history stack, small enough to read and honest about the one
181
- * thing that matters: **Back returns to the entry it left, with
182
- * the key it had.** That key is what the offset is filed under,
183
- * and reusing it is the whole mechanism.
192
+ * A history stack, small enough to read and honest about the two
193
+ * things that matter.
194
+ *
195
+ * **Back returns to the entry it left, with the key it had.** That
196
+ * key is what the offset is filed under, and reusing it is the
197
+ * whole mechanism.
198
+ *
199
+ * **Expanding the specials is a NEW entry on the SAME path.** That
200
+ * is what `setSearchParams` does for a filter chip or an expanded
201
+ * group, and it is the case that has to leave the scrollport alone.
202
+ * The key is one the memory has never seen, so a memory that read
203
+ * "unseen ⇒ top" would throw the reader to the top of the list
204
+ * every time they opened a group.
205
+ *
206
+ * The toggle sits in the **header**, not in the list, and that is
207
+ * not decoration: a control inside the scrollport is scrolled into
208
+ * view before it is pressed, which moves the very offset the demo
209
+ * is about. It cost an hour of chasing a bug that was the driver's.
184
210
  *
185
- * `contentDelayMs` is the second half of the problem. A real list
186
- * is fetched, so the commit that changes the route draws an empty
211
+ * `contentDelayMs` is the other half of the problem. A real list is
212
+ * fetched, so the commit that changes the route draws an empty
187
213
  * scrollport and the rows land later — an offset applied against
188
214
  * that clamps to `0`. Set it and the restore has to wait for the
189
215
  * content, which is the case the `ResizeObserver` in
@@ -195,7 +221,7 @@ const ScrollMemoryDemo = ({
195
221
  contentDelayMs: number
196
222
  }) => {
197
223
  const [entries, setEntries] = useState<HistoryEntry[]>([
198
- { episode: null, key: "entry-1" },
224
+ { episode: null, isExpanded: false, key: "entry-1" },
199
225
  ])
200
226
  const [entryIndex, setEntryIndex] = useState(0)
201
227
  const [isContentReady, setIsContentReady] = useState(
@@ -220,70 +246,103 @@ const ScrollMemoryDemo = ({
220
246
  }
221
247
  }, [contentDelayMs])
222
248
 
223
- const open = (episode: string) => {
224
- const opened = [
249
+ const push = (pushed: Omit<HistoryEntry, "key">) => {
250
+ const stack = [
225
251
  ...entries.slice(0, entryIndex + 1),
226
- { episode, key: `entry-${entries.length + 1}` },
252
+ { ...pushed, key: `entry-${entries.length + 1}` },
227
253
  ]
228
254
 
229
- setEntries(opened)
230
- setEntryIndex(opened.length - 1)
255
+ setEntries(stack)
256
+ setEntryIndex(stack.length - 1)
231
257
  }
232
258
 
233
- return (
234
- <Shell>
235
- <Header
236
- actions={
237
- <Button
238
- appearance="outline"
239
- isDisabled={entryIndex === 0}
240
- onClick={() => {
241
- setEntryIndex(entryIndex - 1)
242
- }}
243
- size="sm"
244
- >
245
- Back
246
- </Button>
247
- }
248
- heading="Library"
249
- />
259
+ const titles = entry?.isExpanded
260
+ ? [...EPISODE_TITLES, ...SPECIAL_TITLES]
261
+ : EPISODE_TITLES
250
262
 
251
- <Main scrollKey={entry?.key}>
252
- {!isContentReady && <Spinner label="Loading" />}
263
+ return (
264
+ <ScrollMemoryProvider
265
+ entry={{
266
+ key: entry?.key ?? "entry-1",
267
+ path: entry ? toPath(entry) : "/library",
268
+ }}
269
+ >
270
+ <Shell>
271
+ <Header
272
+ actions={
273
+ <>
274
+ <Button
275
+ appearance="outline"
276
+ isDisabled={entryIndex === 0}
277
+ onClick={() => {
278
+ setEntryIndex(entryIndex - 1)
279
+ }}
280
+ size="sm"
281
+ >
282
+ Back
283
+ </Button>
253
284
 
254
- {isContentReady && entry?.episode == null && (
255
- <div className="flex flex-col gap-3">
256
- {EPISODE_TITLES.map((title) => (
257
- <Card
258
- heading={title}
259
- key={title}
260
- padding="sm"
285
+ <Button
286
+ appearance="outline"
287
+ isDisabled={entry?.episode != null}
288
+ onClick={() => {
289
+ push({
290
+ episode: null,
291
+ isExpanded: !entry?.isExpanded,
292
+ })
293
+ }}
294
+ size="sm"
261
295
  >
262
- <Button
263
- appearance="ghost"
264
- onClick={() => {
265
- open(title)
266
- }}
267
- size="sm"
296
+ {entry?.isExpanded
297
+ ? "Collapse the specials"
298
+ : "Expand the specials"}
299
+ </Button>
300
+ </>
301
+ }
302
+ heading="Library"
303
+ />
304
+
305
+ <Main>
306
+ {!isContentReady && <Spinner label="Loading" />}
307
+
308
+ {isContentReady && entry?.episode == null && (
309
+ <div className="flex flex-col gap-3">
310
+ {titles.map((title) => (
311
+ <Card
312
+ heading={title}
313
+ key={title}
314
+ padding="sm"
268
315
  >
269
- Open {title}
270
- </Button>
271
- </Card>
272
- ))}
273
- </div>
274
- )}
275
-
276
- {isContentReady && entry?.episode != null && (
277
- <Card heading={entry.episode}>
278
- <p className="text-content-secondary text-sm">
279
- A page short enough that the scrollport
280
- collapses behind it. Press Back: the list
281
- returns to where it was, not to the top.
282
- </p>
283
- </Card>
284
- )}
285
- </Main>
286
- </Shell>
316
+ <Button
317
+ appearance="ghost"
318
+ onClick={() => {
319
+ push({
320
+ episode: title,
321
+ isExpanded:
322
+ entry?.isExpanded ?? false,
323
+ })
324
+ }}
325
+ size="sm"
326
+ >
327
+ Open {title}
328
+ </Button>
329
+ </Card>
330
+ ))}
331
+ </div>
332
+ )}
333
+
334
+ {isContentReady && entry?.episode != null && (
335
+ <Card heading={entry.episode}>
336
+ <p className="text-content-secondary text-sm">
337
+ A page short enough that the scrollport
338
+ collapses behind it. Press Back: the list
339
+ returns to where it was, not to the top.
340
+ </p>
341
+ </Card>
342
+ )}
343
+ </Main>
344
+ </Shell>
345
+ </ScrollMemoryProvider>
287
346
  )
288
347
  }
289
348
 
@@ -292,10 +351,16 @@ const ScrollMemoryDemo = ({
292
351
  *
293
352
  * `Shell` makes `<main>` the page's only vertical scrollport, and
294
353
  * a browser restores the **document** scroller and nothing else —
295
- * so without `scrollKey` this list comes back at the top every
296
- * time, in every browser, with no setting that changes it.
354
+ * so without a `ScrollMemoryProvider` this list comes back at the
355
+ * top every time, in every browser, with no setting that changes
356
+ * it.
297
357
  *
298
358
  * Scroll the list, open an episode, then press Back.
359
+ *
360
+ * Then scroll again and press **Expand the specials**. That is a
361
+ * new history entry on the same page, and the scrollport does not
362
+ * move — a filter is not a navigation, however the router files
363
+ * it.
299
364
  */
300
365
  export const ScrollMemory: Story = {
301
366
  render: () => <ScrollMemoryDemo contentDelayMs={0} />,
@@ -305,6 +305,64 @@ test("a restore waits for content that arrives after the navigation", async () =
305
305
  })
306
306
  })
307
307
 
308
+ /**
309
+ * The regression the first release of this shipped, and the reason
310
+ * an entry remembers its path.
311
+ *
312
+ * A filter chip, an expanded group and a selected tab each write a
313
+ * search param, and `setSearchParams` **navigates** — so pressing
314
+ * one mints a history entry the memory has never seen, on the page
315
+ * the reader is already looking at. Read as "unseen ⇒ start at the
316
+ * top", the list jumped to the top every time the reader opened a
317
+ * group. Measured live on Docket: scrolled to 1200, pressed a group
318
+ * toggle, landed at 0.
319
+ *
320
+ * The toggle is in the header rather than in the list on purpose.
321
+ * A control inside the scrollport is scrolled into view before it
322
+ * is pressed, so the driver moves the offset and the test proves
323
+ * nothing.
324
+ *
325
+ * Leaving the scrollport alone is also exactly what the page did
326
+ * before any of this existed, so this answer cannot itself be a
327
+ * regression.
328
+ */
329
+ test("expanding a group is a new history entry that does not move the scrollport", async () => {
330
+ await setViewport(DESKTOP)
331
+
332
+ const { canvas } = await mountStory(ScrollMemory)
333
+
334
+ const main = canvas.getByRole("main")
335
+
336
+ await scrollTo(main, 900)
337
+
338
+ await userEvent.click(
339
+ expectAgentDrivable(canvas, {
340
+ name: "Expand the specials",
341
+ role: "button",
342
+ }),
343
+ )
344
+
345
+ // The expansion happened, so this is a real new entry rather than
346
+ // a press that did nothing.
347
+ await expect(
348
+ expectAgentDrivable(canvas, {
349
+ name: "Collapse the specials",
350
+ role: "button",
351
+ }),
352
+ ).toBeVisible()
353
+
354
+ // Still where the reader put it. `waitFor` would hide a jump that
355
+ // corrects itself, so this is read directly after two rendering
356
+ // steps.
357
+ await new Promise((resolve) => {
358
+ globalThis.requestAnimationFrame(() => {
359
+ globalThis.requestAnimationFrame(resolve)
360
+ })
361
+ })
362
+
363
+ await expect(main.scrollTop).toBe(900)
364
+ })
365
+
308
366
  /**
309
367
  * A restore that outlives the reader's first scroll is a page that
310
368
  * fights them. Anything the reader does to the scrollport ends it.
package/src/Main/Main.tsx CHANGED
@@ -17,20 +17,11 @@ import {
17
17
  } from "../Shell/contentWidth.ts"
18
18
  import { ShellContext } from "../Shell/shellContext.ts"
19
19
  import { toClassName } from "../toClassName.ts"
20
+ import { ScrollMemoryContext } from "./scrollMemoryContext.ts"
20
21
  import { useScrollMemory } from "./useScrollMemory.ts"
21
22
 
22
23
  export type MainProps = ComponentPropsWithRef<"main"> & {
23
24
  contentWidth?: ContentWidth
24
- /**
25
- * The history entry this page belongs to — react-router's
26
- * `useLocation().key`, and the same value under a different
27
- * router. Given it, `<main>` remembers where each entry was
28
- * scrolled to and puts the offset back on Back and Forward.
29
- *
30
- * Omit it and nothing is remembered, which is the right default
31
- * for a page with no history to remember.
32
- */
33
- scrollKey?: string
34
25
  }
35
26
 
36
27
  /**
@@ -122,18 +113,27 @@ export type MainProps = ComponentPropsWithRef<"main"> & {
122
113
  * therefore lands at the top of a list the reader had scrolled
123
114
  * half-way down, and no browser setting changes that.
124
115
  *
125
- * Pass `scrollKey` — react-router's `useLocation().key` — and the
126
- * offset comes back. `useScrollMemory.ts` holds the mechanism and
127
- * `scrollMemory.ts` the reasoning.
116
+ * So this element remembers it, and **there is no prop for it**.
117
+ * The history entry arrives on `ScrollMemoryContext`, which
118
+ * `ReactRouterAdapter` puts at the app root:
128
119
  *
129
120
  * ```tsx
130
- * const location = useLocation()
131
- *
132
- * <Main scrollKey={location.key}>
133
- * <Outlet />
134
- * </Main>
121
+ * <BrowserRouter>
122
+ * <ReactRouterAdapter>
123
+ * <Routes>…</Routes>
124
+ * </ReactRouterAdapter>
125
+ * </BrowserRouter>
135
126
  * ```
136
127
  *
128
+ * It was a prop for one release — `scrollKey` — and that release is
129
+ * the argument against a prop. One app wired it; the other three
130
+ * carried on losing the reader's place with the library sitting
131
+ * right there supporting it. Render no provider and nothing is
132
+ * remembered, which is what an app with no router should get.
133
+ *
134
+ * `useScrollMemory.ts` holds the mechanism and `scrollMemory.ts`
135
+ * the reasoning.
136
+ *
137
137
  * ## Two more things it does, both one line
138
138
  *
139
139
  * `tabIndex={-1}` so `Shell`'s skip link moves **focus** and not
@@ -169,11 +169,11 @@ export const Main = ({
169
169
  contentWidth,
170
170
  id,
171
171
  ref,
172
- scrollKey,
173
172
  ...mainProps
174
173
  }: MainProps): ReactNode => {
175
174
  const shell = useContext(ShellContext)
176
- const setScrollport = useScrollMemory(scrollKey)
175
+ const scrollEntry = useContext(ScrollMemoryContext)
176
+ const setScrollport = useScrollMemory(scrollEntry)
177
177
 
178
178
  // Always called, never conditionally: a `Main` outside a `Shell`
179
179
  // still needs an id an app can point its own skip link at.
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ import type { ScrollEntry } from "./scrollMemory.ts"
4
+ import { ScrollMemoryContext } from "./scrollMemoryContext.ts"
5
+
6
+ export type ScrollMemoryProviderProps = {
7
+ children?: ReactNode
8
+ /**
9
+ * The history entry the page is showing, and the path it belongs
10
+ * to. Under react-router both come straight off `useLocation()`.
11
+ */
12
+ entry: ScrollEntry
13
+ }
14
+
15
+ /**
16
+ * Tell every `Main` below this which history entry it is showing,
17
+ * so Back and Forward return the reader to where they were.
18
+ *
19
+ * An app that uses react-router should not render this directly —
20
+ * `ReactRouterAdapter` in the `./react-router` subpath wires it,
21
+ * along with the link seam, in one component at the root. This is
22
+ * the router-free half, for an app on a different router:
23
+ *
24
+ * ```tsx
25
+ * const [location] = useLocation() // wouter, say
26
+ *
27
+ * <ScrollMemoryProvider
28
+ * entry={{ key: historyKey, path: location }}
29
+ * >
30
+ * <App />
31
+ * </ScrollMemoryProvider>
32
+ * ```
33
+ *
34
+ * **`key` must identify the history entry, not the URL.** Two visits
35
+ * to the same list are two entries and the reader may leave them at
36
+ * different offsets; a key derived from the path would give the
37
+ * second visit the first one's position and could not tell Back from
38
+ * Forward. A router that exposes no such key can pass a counter it
39
+ * increments on every push and restores on every pop.
40
+ */
41
+ export const ScrollMemoryProvider = ({
42
+ children,
43
+ entry,
44
+ }: ScrollMemoryProviderProps): ReactNode => (
45
+ <ScrollMemoryContext.Provider value={entry}>
46
+ {children}
47
+ </ScrollMemoryContext.Provider>
48
+ )
@@ -1,24 +1,66 @@
1
1
  import { afterEach, expect, test } from "vitest"
2
2
 
3
3
  import {
4
+ enterScrollEntry,
4
5
  forgetScrollOffsets,
5
- recallScrollOffset,
6
6
  rememberScrollOffset,
7
+ resolveScrollOffset,
7
8
  } from "./scrollMemory.ts"
8
9
 
9
10
  afterEach(() => {
10
11
  forgetScrollOffsets()
11
12
  })
12
13
 
13
- test("an entry nobody has scrolled recalls the top", () => {
14
- expect(recallScrollOffset("entry-never-seen")).toBe(0)
14
+ const toEntry = (key: string, path = "/backlog") => ({
15
+ key,
16
+ path,
17
+ })
18
+
19
+ test("the first page a reader opens starts at the top", () => {
20
+ expect(resolveScrollOffset(toEntry("entry-1"))).toBe(0)
15
21
  })
16
22
 
17
23
  test("an entry recalls the offset it was last left at", () => {
18
- rememberScrollOffset("entry-1", 900)
19
- rememberScrollOffset("entry-1", 1240)
24
+ rememberScrollOffset(toEntry("entry-1"), 900)
25
+ rememberScrollOffset(toEntry("entry-1"), 1240)
26
+
27
+ expect(resolveScrollOffset(toEntry("entry-1"))).toBe(1240)
28
+ })
29
+
30
+ test("a page the reader has not seen starts at the top", () => {
31
+ enterScrollEntry(toEntry("entry-1", "/backlog"))
32
+
33
+ expect(
34
+ resolveScrollOffset(toEntry("entry-2", "/tasks/9")),
35
+ ).toBe(0)
36
+ })
37
+
38
+ /**
39
+ * The defect this whole field exists for, and it shipped: a filter
40
+ * chip, an expanded group and a selected tab all write a search
41
+ * param, `setSearchParams` navigates, and the new entry is one the
42
+ * memory has never seen. Read as "unseen ⇒ top", opening a group
43
+ * threw the reader to the top of the list they were reading.
44
+ *
45
+ * `null` is a third answer — **leave the scrollport alone** — and
46
+ * it is what the page did before any of this existed, so it cannot
47
+ * be a regression.
48
+ */
49
+ test("a new entry on the same page leaves the scrollport alone", () => {
50
+ enterScrollEntry(toEntry("entry-1", "/backlog"))
51
+
52
+ expect(
53
+ resolveScrollOffset(toEntry("entry-2", "/backlog")),
54
+ ).toBeNull()
55
+ })
56
+
57
+ test("a remembered entry beats the page comparison", () => {
58
+ rememberScrollOffset(toEntry("entry-1", "/backlog"), 1240)
59
+ enterScrollEntry(toEntry("entry-2", "/backlog"))
20
60
 
21
- expect(recallScrollOffset("entry-1")).toBe(1240)
61
+ expect(
62
+ resolveScrollOffset(toEntry("entry-1", "/backlog")),
63
+ ).toBe(1240)
22
64
  })
23
65
 
24
66
  /**
@@ -31,29 +73,41 @@ test("an entry recalls the offset it was last left at", () => {
31
73
  */
32
74
  test("a long session evicts the least recently written entry", () => {
33
75
  for (let index = 0; index < 60; index += 1) {
34
- rememberScrollOffset(`entry-${index}`, index * 10)
76
+ rememberScrollOffset(
77
+ toEntry(`entry-${index}`),
78
+ index * 10,
79
+ )
35
80
  }
36
81
 
37
- // Written first, never re-written: gone.
38
- expect(recallScrollOffset("entry-0")).toBe(0)
82
+ // Written first, never re-written: gone, so it falls through to
83
+ // the page comparison and starts at the top.
84
+ expect(resolveScrollOffset(toEntry("entry-0"))).toBe(0)
39
85
 
40
86
  // Inside the last fifty: kept.
41
- expect(recallScrollOffset("entry-59")).toBe(590)
42
- expect(recallScrollOffset("entry-10")).toBe(100)
87
+ expect(resolveScrollOffset(toEntry("entry-59"))).toBe(590)
88
+ expect(resolveScrollOffset(toEntry("entry-10"))).toBe(100)
43
89
  })
44
90
 
45
91
  test("re-writing an entry saves it from eviction", () => {
46
- rememberScrollOffset("entry-old", 500)
92
+ rememberScrollOffset(toEntry("entry-old"), 500)
47
93
 
48
94
  for (let index = 0; index < 49; index += 1) {
49
- rememberScrollOffset(`entry-${index}`, index * 10)
95
+ rememberScrollOffset(
96
+ toEntry(`entry-${index}`),
97
+ index * 10,
98
+ )
50
99
  }
51
100
 
52
- rememberScrollOffset("entry-old", 640)
101
+ rememberScrollOffset(toEntry("entry-old"), 640)
53
102
 
54
103
  for (let index = 49; index < 60; index += 1) {
55
- rememberScrollOffset(`entry-${index}`, index * 10)
104
+ rememberScrollOffset(
105
+ toEntry(`entry-${index}`),
106
+ index * 10,
107
+ )
56
108
  }
57
109
 
58
- expect(recallScrollOffset("entry-old")).toBe(640)
110
+ expect(resolveScrollOffset(toEntry("entry-old"))).toBe(
111
+ 640,
112
+ )
59
113
  })