@charcuterie/ui 3.34.0 → 4.0.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/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/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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/package.json +1 -1
- 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/RouterLink/RouterLink.mdx +53 -16
- package/src/index.ts +3 -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 +4 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { composeStories } from "@storybook/react"
|
|
2
|
+
import { expect, userEvent, waitFor } from "storybook/test"
|
|
3
|
+
import { afterEach, test } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { forgetScrollOffsets } from "../Main/scrollMemory.ts"
|
|
6
|
+
import { mountStory } from "../mountStory.testHelpers.ts"
|
|
7
|
+
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
|
+
import {
|
|
9
|
+
DESKTOP,
|
|
10
|
+
setViewport,
|
|
11
|
+
} from "../viewport.testHelpers.ts"
|
|
12
|
+
import * as stories from "./ReactRouterAdapter.stories.tsx"
|
|
13
|
+
|
|
14
|
+
const { Default } = composeStories(stories)
|
|
15
|
+
|
|
16
|
+
// The offsets are module state, so they outlive a mount. A test
|
|
17
|
+
// must not.
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
forgetScrollOffsets()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The wiring, against a real router.
|
|
24
|
+
*
|
|
25
|
+
* `Main.test.tsx` proves the scroll mechanism against a hand-rolled
|
|
26
|
+
* history stack — the right subject for a `ResizeObserver` and a
|
|
27
|
+
* clamped offset. What it cannot prove is that the entry reaches
|
|
28
|
+
* `Main` at all when the value comes from `useLocation()` rather
|
|
29
|
+
* than from a story's own `useState`. That is the seam every app
|
|
30
|
+
* now depends on, and it is the kind that fails quietly: the links
|
|
31
|
+
* still navigate and the list still scrolls.
|
|
32
|
+
*/
|
|
33
|
+
test("an app renders one component and gets both seams", async () => {
|
|
34
|
+
await setViewport(DESKTOP)
|
|
35
|
+
|
|
36
|
+
const { canvas } = await mountStory(Default)
|
|
37
|
+
|
|
38
|
+
const main = canvas.getByRole("main")
|
|
39
|
+
|
|
40
|
+
// The link seam: react-router's `<Link>` renders an `<a href>`,
|
|
41
|
+
// so this is also what an unwired app looks like — the assertion
|
|
42
|
+
// that separates them is the absence of a page load below.
|
|
43
|
+
const link = expectAgentDrivable(canvas, {
|
|
44
|
+
name: "Open Episode 12",
|
|
45
|
+
role: "link",
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await expect(link).toHaveAttribute(
|
|
49
|
+
"href",
|
|
50
|
+
"/library/Episode 12",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
main.scrollTop = 900
|
|
54
|
+
|
|
55
|
+
await waitFor(async () => {
|
|
56
|
+
await expect(main.scrollTop).toBe(900)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await new Promise((resolve) => {
|
|
60
|
+
globalThis.requestAnimationFrame(() => {
|
|
61
|
+
globalThis.requestAnimationFrame(resolve)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
await userEvent.click(link)
|
|
66
|
+
|
|
67
|
+
// A soft navigation: the route changed inside the same document.
|
|
68
|
+
// A plain `<a>` would have left the story mounted and unchanged.
|
|
69
|
+
//
|
|
70
|
+
// `waitFor`, because the route commit is a state update and the
|
|
71
|
+
// click resolves before it. It passed locally and failed on CI
|
|
72
|
+
// for exactly that reason.
|
|
73
|
+
await waitFor(async () => {
|
|
74
|
+
await expect(
|
|
75
|
+
canvas.getByRole("heading", { name: "Episode" }),
|
|
76
|
+
).toBeVisible()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await userEvent.click(
|
|
80
|
+
expectAgentDrivable(canvas, {
|
|
81
|
+
name: "Back",
|
|
82
|
+
role: "button",
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// The scroll seam, from the same one component.
|
|
87
|
+
await waitFor(async () => {
|
|
88
|
+
await expect(canvas.getByRole("main").scrollTop).toBe(
|
|
89
|
+
900,
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { useLocation } from "react-router"
|
|
3
|
+
|
|
4
|
+
import { ScrollMemoryProvider } from "../Main/ScrollMemoryProvider.tsx"
|
|
5
|
+
import { RouterLinkProvider } from "../RouterLink/RouterLinkProvider.tsx"
|
|
6
|
+
import { ReactRouterLink } from "./ReactRouterLink.tsx"
|
|
7
|
+
|
|
8
|
+
export type ReactRouterAdapterProps = {
|
|
9
|
+
children?: ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Every seam this library has, wired to react-router, in one
|
|
14
|
+
* component at the app root.
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <BrowserRouter>
|
|
18
|
+
* <ReactRouterAdapter>
|
|
19
|
+
* <Routes>…</Routes>
|
|
20
|
+
* </ReactRouterAdapter>
|
|
21
|
+
* </BrowserRouter>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* It must sit **inside** the router, because it reads
|
|
25
|
+
* `useLocation()`, and **outside** the routes, because what it
|
|
26
|
+
* provides belongs to the whole app.
|
|
27
|
+
*
|
|
28
|
+
* ## Why one component and not two providers
|
|
29
|
+
*
|
|
30
|
+
* There are two seams today — the link seam and the scroll memory —
|
|
31
|
+
* and the fleet proved that a per-app opt-in does not reach the
|
|
32
|
+
* fleet. `Main`'s scroll memory shipped as a `scrollKey` prop, one
|
|
33
|
+
* app wired it, and the other three kept losing the reader's place
|
|
34
|
+
* with the library sitting right there supporting it. A seam an app
|
|
35
|
+
* has to know about is a seam most apps will not have.
|
|
36
|
+
*
|
|
37
|
+
* So the root wiring is one name to remember, and a seam added later
|
|
38
|
+
* arrives in every app that already renders this on its next
|
|
39
|
+
* release. `RouterLinkProvider`, `ReactRouterLink` and
|
|
40
|
+
* `ScrollMemoryProvider` all stay exported for an app that wants one
|
|
41
|
+
* half, or that renders its own root for another reason.
|
|
42
|
+
*
|
|
43
|
+
* ## What it does NOT do
|
|
44
|
+
*
|
|
45
|
+
* It does not render a router, and it takes no route table. This
|
|
46
|
+
* library has no opinion about either, and an app that wants
|
|
47
|
+
* `createBrowserRouter` should keep it.
|
|
48
|
+
*/
|
|
49
|
+
export const ReactRouterAdapter = ({
|
|
50
|
+
children,
|
|
51
|
+
}: ReactRouterAdapterProps): ReactNode => {
|
|
52
|
+
const location = useLocation()
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<RouterLinkProvider link={ReactRouterLink}>
|
|
56
|
+
<ScrollMemoryProvider
|
|
57
|
+
entry={{
|
|
58
|
+
key: location.key,
|
|
59
|
+
path: location.pathname,
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</ScrollMemoryProvider>
|
|
64
|
+
</RouterLinkProvider>
|
|
65
|
+
)
|
|
66
|
+
}
|
package/src/reactRouter/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@charcuterie/ui/react-router` — the react-router adapter for
|
|
3
|
-
*
|
|
2
|
+
* `@charcuterie/ui/react-router` — the react-router adapter for this
|
|
3
|
+
* library's seams.
|
|
4
4
|
*
|
|
5
5
|
* **A subpath export with an optional peer dependency**, and both
|
|
6
6
|
* halves of that matter. The base package must stay router-free: the
|
|
@@ -13,17 +13,23 @@
|
|
|
13
13
|
* asserts exactly that — the main entry is measured for what it
|
|
14
14
|
* reaches, so this cannot leak into it by accident.
|
|
15
15
|
*
|
|
16
|
-
* The wiring is one
|
|
16
|
+
* The wiring is one component at the app root; `Guides/Routing` in
|
|
17
17
|
* Storybook is the copy-pasteable version.
|
|
18
18
|
*
|
|
19
19
|
* ```tsx
|
|
20
|
-
* import {
|
|
21
|
-
* import { ReactRouterLink } from "@charcuterie/ui/react-router"
|
|
20
|
+
* import { ReactRouterAdapter } from "@charcuterie/ui/react-router"
|
|
22
21
|
*
|
|
23
|
-
* <
|
|
24
|
-
* <
|
|
25
|
-
*
|
|
22
|
+
* <BrowserRouter>
|
|
23
|
+
* <ReactRouterAdapter>
|
|
24
|
+
* <Routes>…</Routes>
|
|
25
|
+
* </ReactRouterAdapter>
|
|
26
|
+
* </BrowserRouter>
|
|
26
27
|
* ```
|
|
28
|
+
*
|
|
29
|
+
* `ReactRouterLink` stays exported for an app that wires
|
|
30
|
+
* `RouterLinkProvider` itself.
|
|
27
31
|
*/
|
|
28
32
|
|
|
33
|
+
export type { ReactRouterAdapterProps } from "./ReactRouterAdapter.tsx"
|
|
34
|
+
export { ReactRouterAdapter } from "./ReactRouterAdapter.tsx"
|
|
29
35
|
export { ReactRouterLink } from "./ReactRouterLink.tsx"
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -375,8 +375,10 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
375
375
|
// and `ButtonLink`, both a real `<a href>`, differing in paint
|
|
376
376
|
// rather than semantics: +2 -> 38, on top of `AdaptiveGrid`'s 36.
|
|
377
377
|
// `AnchorLink`,
|
|
378
|
-
// `RouterLinkProvider`,
|
|
379
|
-
//
|
|
378
|
+
// `RouterLinkProvider`, `ReactRouterLink`, `ScrollMemoryProvider`
|
|
379
|
+
// and `ReactRouterAdapter` are the router seam rather
|
|
380
|
+
// than components anybody stories, and live in `RouterLink/`,
|
|
381
|
+
// `Main/` and
|
|
380
382
|
// `reactRouter/` — outside this count by the `<Name>/<Name>.tsx`
|
|
381
383
|
// rule, exactly as `Overlay/`'s parts are.
|
|
382
384
|
// The unified app shell adds four: `Shell`, `Header`, `Rail`,
|