@pylonsync/react 0.4.11 → 0.4.13

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/Link.d.ts CHANGED
@@ -3,7 +3,8 @@ declare global {
3
3
  interface Window {
4
4
  __pylon?: {
5
5
  prefetch: (href: string, opts?: {
6
- /** Also fetch the page payload (costs a server render — hover only). */
6
+ /** Also fetch the page payload. Costs a server render, so only on an
7
+ * intent signal: hover, touch, or focus. */
7
8
  document?: boolean;
8
9
  }) => Promise<void>;
9
10
  navigate: (href: string, opts?: {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.11",
6
+ "version": "0.4.13",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -14,8 +14,8 @@
14
14
  "prepack": "bun run build"
15
15
  },
16
16
  "dependencies": {
17
- "@pylonsync/sdk": "0.4.11",
18
- "@pylonsync/sync": "0.4.11"
17
+ "@pylonsync/sdk": "0.4.13",
18
+ "@pylonsync/sync": "0.4.13"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "react": ">=19.0.0"
package/src/Link.tsx CHANGED
@@ -11,9 +11,11 @@
11
11
  // - Prefetches in two stages, because the two halves cost very
12
12
  // different things. On viewport entry (IntersectionObserver) the
13
13
  // runtime modulepreloads the destination's client chunks — static,
14
- // immutable, cacheable. On hover/touch it also fetches the page
15
- // itself, which costs a server render, so that waits for a real
16
- // signal of intent rather than firing for a screenful of links.
14
+ // immutable, cacheable. On hover, touch, or focus it also fetches
15
+ // the page itself, which costs a server render, so that waits for a
16
+ // real signal of intent rather than firing for a screenful of links.
17
+ // Focus is included so a keyboard user tabbing to a link gets the
18
+ // same warm path a mouse user gets.
17
19
  // - Modifier keys (cmd/ctrl/shift/alt) or middle-click fall
18
20
  // through to the browser's default behavior (open in new tab,
19
21
  // etc.) — matches Next.js semantics.
@@ -32,7 +34,8 @@ declare global {
32
34
  prefetch: (
33
35
  href: string,
34
36
  opts?: {
35
- /** Also fetch the page payload (costs a server render — hover only). */
37
+ /** Also fetch the page payload. Costs a server render, so only on an
38
+ * intent signal: hover, touch, or focus. */
36
39
  document?: boolean;
37
40
  },
38
41
  ) => Promise<void>;
@@ -153,11 +156,17 @@ export function Link({
153
156
 
154
157
  el.addEventListener("mouseenter", warmPage);
155
158
  el.addEventListener("touchstart", warmPage, { passive: true });
159
+ // Focus is the keyboard's version of hover, and a stronger signal: a
160
+ // cursor slides across links it isn't aiming at, but nobody tabs onto one
161
+ // idly. Without it, tabbing to a link and pressing Enter took the cold
162
+ // path while a mouse user got the warm one — the wrong way round.
163
+ el.addEventListener("focus", warmPage);
156
164
 
157
165
  return () => {
158
166
  io?.disconnect();
159
167
  el.removeEventListener("mouseenter", warmPage);
160
168
  el.removeEventListener("touchstart", warmPage);
169
+ el.removeEventListener("focus", warmPage);
161
170
  };
162
171
  }, [href, prefetch]);
163
172
 
@@ -48,10 +48,16 @@ describe("notFound() — branded for the SSR not-found boundary", () => {
48
48
  describe("redirect() — client-side replace navigation", () => {
49
49
  let calls: Array<{ href: string; opts?: unknown }>;
50
50
  const hadWindow = "window" in globalThis;
51
+ // The suite preloads happy-dom, so a real window usually EXISTS here. Put it
52
+ // back rather than leaving the stub in place: bun isolates globals per test
53
+ // file today, so nothing observable depends on this, but a test that
54
+ // replaces a global and never restores it is one isolation change away from
55
+ // breaking every file that mounts.
56
+ const originalWindow = (globalThis as any).window;
51
57
 
52
58
  beforeEach(() => {
53
59
  calls = [];
54
- // bun:test has no DOM — stand up a minimal window the module can see.
60
+ // Stand up a minimal window the module can see, without assuming a DOM.
55
61
  (globalThis as any).window = {
56
62
  __pylon: {
57
63
  prefetch: async () => {},
@@ -63,8 +69,11 @@ describe("redirect() — client-side replace navigation", () => {
63
69
  });
64
70
 
65
71
  afterEach(() => {
66
- if (hadWindow) return;
67
- delete (globalThis as any).window;
72
+ if (hadWindow) {
73
+ (globalThis as any).window = originalWindow;
74
+ } else {
75
+ delete (globalThis as any).window;
76
+ }
68
77
  });
69
78
 
70
79
  test("delegates to __pylon.navigate with replace:true (no history push)", () => {