@barefootjs/router 0.29.0 → 0.30.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/README.md CHANGED
@@ -64,6 +64,13 @@ setup step.
64
64
  state, media playback, scroll, and hydrated scope survive — matched between
65
65
  documents by the attribute value (or `id`). A no-op when no element is marked;
66
66
  pass `morph: false` for a plain swap.
67
+ - **Swap in flight** (`data-bf-navigating`): set on `<html>` for the duration of
68
+ a region swap. A swap commits the new markup *before* re-hydrating it, so
69
+ "present in the DOM" and "interactive" are two moments — until this clears, a
70
+ swapped-in island may still be server markup with no handlers, and a click on
71
+ it is lost. Style off it for a loading indicator, or wait for its absence
72
+ before driving new content. `NAVIGATING_ATTR` is exported so callers need not
73
+ hard-code the string. Query-only navigations swap nothing and never set it.
67
74
 
68
75
  ## `<head>`: metadata is reconciled, resources are not
69
76
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { startRouter, navigate } from './router.ts';
1
+ export { startRouter, navigate, NAVIGATING_ATTR } from './router.ts';
2
2
  export type { RouterOptions, NavigateOptions, Router } from './types.ts';
3
3
  export { BF_REGION } from '@barefootjs/shared';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAGxE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAGxE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA"}
package/dist/index.js CHANGED
@@ -420,6 +420,7 @@ function ensureAnnouncer() {
420
420
 
421
421
  // src/router.ts
422
422
  var active = null;
423
+ var NAVIGATING_ATTR = "data-bf-navigating";
423
424
  function startRouter(options = {}) {
424
425
  if (typeof document === "undefined" || typeof window === "undefined") {
425
426
  return { stop() {}, navigate: async () => {}, prefetch() {} };
@@ -564,6 +565,7 @@ async function navigate(url, options = {}) {
564
565
  state.inflight?.abort();
565
566
  const controller = new AbortController;
566
567
  state.inflight = controller;
568
+ setNavigating(true);
567
569
  try {
568
570
  const snap = await loadPage(state, target.href);
569
571
  if (controller.signal.aborted)
@@ -633,10 +635,21 @@ async function navigate(url, options = {}) {
633
635
  announceNavigation(title);
634
636
  }
635
637
  } finally {
636
- if (state.inflight === controller)
638
+ if (state.inflight === controller) {
637
639
  state.inflight = null;
640
+ setNavigating(false);
641
+ }
638
642
  }
639
643
  }
644
+ function setNavigating(on) {
645
+ const root = typeof document !== "undefined" ? document.documentElement : null;
646
+ if (!root)
647
+ return;
648
+ if (on)
649
+ root.setAttribute(NAVIGATING_ATTR, "");
650
+ else
651
+ root.removeAttribute(NAVIGATING_ATTR);
652
+ }
640
653
  function prefetch(url) {
641
654
  const state = active;
642
655
  if (!state)
@@ -731,5 +744,6 @@ import { BF_REGION as BF_REGION3 } from "@barefootjs/shared";
731
744
  export {
732
745
  startRouter,
733
746
  navigate,
747
+ NAVIGATING_ATTR,
734
748
  BF_REGION3 as BF_REGION
735
749
  };
package/dist/router.d.ts CHANGED
@@ -8,6 +8,12 @@
8
8
  * the incoming ones, with last-wins semantics across overlapping navigations.
9
9
  */
10
10
  import type { NavigateOptions, Router, RouterOptions } from './types.ts';
11
+ /**
12
+ * Document-root attribute marking a region swap as in flight — see
13
+ * {@link setNavigating}. Public: a page may style off it (a loading bar), and a
14
+ * test may wait for its absence to know the swapped-in islands are live.
15
+ */
16
+ export declare const NAVIGATING_ATTR = "data-bf-navigating";
11
17
  export declare function startRouter(options?: RouterOptions): Router;
12
18
  export declare function navigate(url: string, options?: NavigateOptions): Promise<void>;
13
19
  //# sourceMappingURL=router.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwBH,OAAO,KAAK,EACV,eAAe,EAEf,MAAM,EACN,aAAa,EAEd,MAAM,YAAY,CAAA;AAInB,wBAAgB,WAAW,CAAC,OAAO,GAAE,aAAkB,GAAG,MAAM,CAkE/D;AA4FD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsKxF"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwBH,OAAO,KAAK,EACV,eAAe,EAEf,MAAM,EACN,aAAa,EAEd,MAAM,YAAY,CAAA;AAInB;;;;GAIG;AACH,eAAO,MAAM,eAAe,uBAAuB,CAAA;AAEnD,wBAAgB,WAAW,CAAC,OAAO,GAAE,aAAkB,GAAG,MAAM,CAkE/D;AA4FD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6KxF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/router",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Backend-agnostic partial-navigation client router for BarefootJS — swaps only the page region and re-hydrates the islands inside it",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -40,7 +40,7 @@
40
40
  "directory": "packages/router"
41
41
  },
42
42
  "dependencies": {
43
- "@barefootjs/shared": "0.29.0"
43
+ "@barefootjs/shared": "0.30.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@barefootjs/client": ">=0.14.0"
@@ -51,7 +51,7 @@
51
51
  }
52
52
  },
53
53
  "devDependencies": {
54
- "@barefootjs/client": "^0.29.0",
54
+ "@barefootjs/client": "^0.30.0",
55
55
  "@happy-dom/global-registrator": "^20.0.11",
56
56
  "typescript": "^5.0.0"
57
57
  }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { startRouter, navigate } from './router.ts'
1
+ export { startRouter, navigate, NAVIGATING_ATTR } from './router.ts'
2
2
  export type { RouterOptions, NavigateOptions, Router } from './types.ts'
3
3
 
4
4
  // Re-exported so server-side helpers can reference the swappable-region marker.
package/src/router.ts CHANGED
@@ -40,6 +40,13 @@ import type {
40
40
 
41
41
  let active: RouterState | null = null
42
42
 
43
+ /**
44
+ * Document-root attribute marking a region swap as in flight — see
45
+ * {@link setNavigating}. Public: a page may style off it (a loading bar), and a
46
+ * test may wait for its absence to know the swapped-in islands are live.
47
+ */
48
+ export const NAVIGATING_ATTR = 'data-bf-navigating'
49
+
43
50
  export function startRouter(options: RouterOptions = {}): Router {
44
51
  // SSR / non-DOM: a no-op handle, never throws.
45
52
  if (typeof document === 'undefined' || typeof window === 'undefined') {
@@ -238,6 +245,7 @@ export async function navigate(url: string, options: NavigateOptions = {}): Prom
238
245
  state.inflight?.abort()
239
246
  const controller = new AbortController()
240
247
  state.inflight = controller
248
+ setNavigating(true)
241
249
 
242
250
  try {
243
251
  const snap = await loadPage(state, target.href)
@@ -362,10 +370,42 @@ export async function navigate(url: string, options: NavigateOptions = {}): Prom
362
370
  announceNavigation(title)
363
371
  }
364
372
  } finally {
365
- if (state.inflight === controller) state.inflight = null
373
+ // Only the CURRENT navigation may clear the flag. A superseded one reaches
374
+ // this block while its successor is still mid-swap, and clearing there
375
+ // would announce "interactive" over content that is still being rebuilt.
376
+ if (state.inflight === controller) {
377
+ state.inflight = null
378
+ setNavigating(false)
379
+ }
366
380
  }
367
381
  }
368
382
 
383
+ /**
384
+ * Mark a region swap as in flight on the document root
385
+ * (`data-bf-navigating`), so a caller can tell "the new markup is in the DOM"
386
+ * from "the new markup is INTERACTIVE".
387
+ *
388
+ * Those are two different moments and the gap between them is real: the swap
389
+ * is committed, and only then does step 5 re-hydrate each region — which
390
+ * `defaultRehydrate` may reach through a dynamic import of
391
+ * `@barefootjs/client/runtime` (`seams.ts`). Until that resolves the
392
+ * swapped-in islands are server markup with no handlers attached, so a click
393
+ * lands on nothing and is silently lost. Nothing observable distinguished the two states before this,
394
+ * which made "wait for the element, then click it" look correct and fail only
395
+ * under load.
396
+ *
397
+ * Set where the swap sequence begins and cleared in its `finally`, so it spans
398
+ * dispose → module load → history → re-hydrate → focus. Query-only
399
+ * navigations return before that sequence and never set it: they re-render
400
+ * nothing and swap nothing, so there is no interactivity gap to describe.
401
+ */
402
+ function setNavigating(on: boolean): void {
403
+ const root = typeof document !== 'undefined' ? document.documentElement : null
404
+ if (!root) return
405
+ if (on) root.setAttribute(NAVIGATING_ATTR, '')
406
+ else root.removeAttribute(NAVIGATING_ATTR)
407
+ }
408
+
369
409
  // --- Prefetch -------------------------------------------------------------
370
410
 
371
411
  function prefetch(url: string): void {