@real-router/angular 0.2.2 → 0.4.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.
@@ -1,11 +1,365 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, inject, DestroyRef, InjectionToken, makeEnvironmentProviders, input, TemplateRef, Directive, contentChildren, computed, Component, output, effect, ElementRef } from '@angular/core';
2
+ import { signal, inject, DestroyRef, InjectionToken, provideEnvironmentInitializer, makeEnvironmentProviders, input, TemplateRef, Directive, contentChildren, computed, Component, output, effect, ElementRef } from '@angular/core';
3
3
  import { getNavigator, UNKNOWN_ROUTE } from '@real-router/core';
4
4
  import { createRouteSource, createRouteNodeSource, getTransitionSource, createActiveRouteSource, createDismissableError } from '@real-router/sources';
5
5
  import { getPluginApi } from '@real-router/core/api';
6
6
  import { getRouteUtils, startsWithSegment } from '@real-router/route-utils';
7
7
  import { NgTemplateOutlet } from '@angular/common';
8
8
 
9
+ const CLEAR_DELAY = 7000;
10
+ const SAFARI_READY_DELAY = 100;
11
+ const ANNOUNCER_ATTR = "data-real-router-announcer";
12
+ const INTERNAL_ROUTE_PREFIX = "@@";
13
+ const VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0";
14
+ function createRouteAnnouncer(router, options) {
15
+ const prefix = options?.prefix ?? "Navigated to ";
16
+ const getCustomText = options?.getAnnouncementText;
17
+ let isInitialNavigation = true;
18
+ let isReady = false;
19
+ let isDestroyed = false;
20
+ let lastAnnouncedText = "";
21
+ let pendingText = null;
22
+ let clearTimeoutId;
23
+ const announcer = getOrCreateAnnouncer();
24
+ const doAnnounce = (text, h1) => {
25
+ lastAnnouncedText = text;
26
+ clearTimeout(clearTimeoutId);
27
+ announcer.textContent = text;
28
+ clearTimeoutId = setTimeout(() => {
29
+ announcer.textContent = "";
30
+ lastAnnouncedText = "";
31
+ }, CLEAR_DELAY);
32
+ manageFocus(h1);
33
+ };
34
+ // Safari-ready delay: announcing before VoiceOver wires up the aria-live region
35
+ // causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms
36
+ // before marking the announcer "ready" — any navigation during that window is
37
+ // buffered in pendingText and flushed once the delay expires.
38
+ const safariTimeoutId = setTimeout(() => {
39
+ isReady = true;
40
+ if (pendingText !== null && !isDestroyed) {
41
+ const text = pendingText;
42
+ pendingText = null;
43
+ doAnnounce(text, document.querySelector("h1"));
44
+ }
45
+ }, SAFARI_READY_DELAY);
46
+ const unsubscribe = router.subscribe(({ route }) => {
47
+ if (isInitialNavigation) {
48
+ isInitialNavigation = false;
49
+ return;
50
+ }
51
+ // Double rAF: waits for two paint frames so the incoming route's DOM
52
+ // (including the new <h1>) is fully rendered before resolveText reads it.
53
+ // Single rAF fires before the new route's template has been attached,
54
+ // which would cause resolveText to pick up the OLD h1 or fall back to
55
+ // document.title / route.name prematurely.
56
+ requestAnimationFrame(() => {
57
+ requestAnimationFrame(() => {
58
+ if (isDestroyed) {
59
+ return;
60
+ }
61
+ const h1 = document.querySelector("h1");
62
+ const text = resolveText(route, prefix, getCustomText, h1);
63
+ if (!text || text === lastAnnouncedText) {
64
+ return;
65
+ }
66
+ if (!isReady) {
67
+ // Defer announcement until Safari-ready window elapses (see safariTimeoutId).
68
+ pendingText = text;
69
+ return;
70
+ }
71
+ doAnnounce(text, h1);
72
+ });
73
+ });
74
+ });
75
+ return {
76
+ destroy() {
77
+ isDestroyed = true;
78
+ unsubscribe();
79
+ clearTimeout(clearTimeoutId);
80
+ clearTimeout(safariTimeoutId);
81
+ removeAnnouncer();
82
+ },
83
+ };
84
+ }
85
+ function getOrCreateAnnouncer() {
86
+ const existing = document.querySelector(`[${ANNOUNCER_ATTR}]`);
87
+ if (existing) {
88
+ return existing;
89
+ }
90
+ const element = document.createElement("div");
91
+ element.setAttribute("style", VISUALLY_HIDDEN);
92
+ element.setAttribute("aria-live", "assertive");
93
+ element.setAttribute("aria-atomic", "true");
94
+ element.setAttribute(ANNOUNCER_ATTR, "");
95
+ document.body.prepend(element);
96
+ return element;
97
+ }
98
+ function removeAnnouncer() {
99
+ document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();
100
+ }
101
+ function resolveText(route, prefix, getCustomText, h1) {
102
+ if (getCustomText) {
103
+ return getCustomText(route);
104
+ }
105
+ const h1Text = h1?.textContent.trim() ?? "";
106
+ const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)
107
+ ? ""
108
+ : route.name;
109
+ const rawText = h1Text || document.title || routeName || globalThis.location.pathname;
110
+ return `${prefix}${rawText}`;
111
+ }
112
+ function manageFocus(h1) {
113
+ if (!h1) {
114
+ return;
115
+ }
116
+ if (!h1.hasAttribute("tabindex")) {
117
+ h1.setAttribute("tabindex", "-1");
118
+ }
119
+ h1.focus({ preventScroll: true });
120
+ }
121
+
122
+ const STORAGE_KEY = "real-router:scroll";
123
+ const NOOP_INSTANCE = Object.freeze({
124
+ destroy: () => {
125
+ /* no-op */
126
+ },
127
+ });
128
+ function createScrollRestoration(router, options) {
129
+ if (typeof globalThis.window === "undefined") {
130
+ return NOOP_INSTANCE;
131
+ }
132
+ const mode = options?.mode ?? "restore";
133
+ // mode "manual" = utility does nothing. Don't flip history.scrollRestoration,
134
+ // don't subscribe, don't register pagehide — leave the browser's native
135
+ // auto-restore intact for the app to override if it wants to.
136
+ if (mode === "manual") {
137
+ return NOOP_INSTANCE;
138
+ }
139
+ const anchorEnabled = options?.anchorScrolling ?? true;
140
+ const getContainer = options?.scrollContainer;
141
+ const prevScrollRestoration = history.scrollRestoration;
142
+ try {
143
+ history.scrollRestoration = "manual";
144
+ }
145
+ catch {
146
+ // Ignore — some embedded contexts may reject the assignment.
147
+ }
148
+ // Resolve the container lazily on every event so containers mounted AFTER
149
+ // the provider still get correct scroll handling. Falls back to window when
150
+ // the getter is absent or returns null (pre-mount).
151
+ const readPos = () => {
152
+ const element = getContainer?.();
153
+ return element ? element.scrollTop : globalThis.scrollY;
154
+ };
155
+ const writePos = (top) => {
156
+ const element = getContainer?.();
157
+ if (element) {
158
+ element.scrollTop = top;
159
+ }
160
+ else {
161
+ globalThis.scrollTo(0, top);
162
+ }
163
+ };
164
+ const scrollToHashOrTop = () => {
165
+ const hash = globalThis.location.hash;
166
+ if (anchorEnabled && hash.length > 1) {
167
+ // location.hash is percent-encoded; ids in the DOM are the raw string.
168
+ // Decode for the match. Fall back to the raw slice if the hash contains
169
+ // a malformed escape sequence (decodeURIComponent throws on those).
170
+ let id;
171
+ try {
172
+ id = decodeURIComponent(hash.slice(1));
173
+ }
174
+ catch {
175
+ id = hash.slice(1);
176
+ }
177
+ // eslint-disable-next-line unicorn/prefer-query-selector -- ids may contain CSS-unsafe chars
178
+ const element = document.getElementById(id);
179
+ if (element) {
180
+ element.scrollIntoView();
181
+ return;
182
+ }
183
+ }
184
+ writePos(0);
185
+ };
186
+ let destroyed = false;
187
+ const unsubscribe = router.subscribe(({ route, previousRoute }) => {
188
+ const nav = route.context
189
+ .navigation;
190
+ // Browsers dispatch reload as the initial navigation after refresh, so
191
+ // previousRoute is undefined and capture is naturally skipped. The
192
+ // pre-refresh position was already persisted via pagehide.
193
+ if (previousRoute) {
194
+ putPos(keyOf(previousRoute), readPos());
195
+ }
196
+ // Single rAF so DOM is committed before we read anchors / write scroll.
197
+ // Guard against destroy() racing with the callback.
198
+ requestAnimationFrame(() => {
199
+ if (destroyed) {
200
+ return;
201
+ }
202
+ if (mode === "top" || !nav) {
203
+ scrollToHashOrTop();
204
+ return;
205
+ }
206
+ if (nav.navigationType === "replace") {
207
+ return;
208
+ }
209
+ if (nav.direction === "back" ||
210
+ nav.navigationType === "traverse" ||
211
+ nav.navigationType === "reload") {
212
+ writePos(loadStore()[keyOf(route)] ?? 0);
213
+ return;
214
+ }
215
+ scrollToHashOrTop();
216
+ });
217
+ });
218
+ const onPageHide = () => {
219
+ const current = router.getState();
220
+ if (current) {
221
+ putPos(keyOf(current), readPos());
222
+ }
223
+ };
224
+ globalThis.addEventListener("pagehide", onPageHide);
225
+ return {
226
+ destroy: () => {
227
+ if (destroyed) {
228
+ return;
229
+ }
230
+ destroyed = true;
231
+ unsubscribe();
232
+ globalThis.removeEventListener("pagehide", onPageHide);
233
+ try {
234
+ history.scrollRestoration = prevScrollRestoration;
235
+ }
236
+ catch {
237
+ // Ignore.
238
+ }
239
+ },
240
+ };
241
+ }
242
+ function keyOf(state) {
243
+ return `${state.name}:${canonicalJson(state.params)}`;
244
+ }
245
+ function loadStore() {
246
+ try {
247
+ const raw = sessionStorage.getItem(STORAGE_KEY);
248
+ return raw ? JSON.parse(raw) : {};
249
+ }
250
+ catch {
251
+ return {};
252
+ }
253
+ }
254
+ function putPos(key, pos) {
255
+ try {
256
+ const store = loadStore();
257
+ store[key] = pos;
258
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(store));
259
+ }
260
+ catch {
261
+ // Ignore quota / security errors.
262
+ }
263
+ }
264
+ function canonicalJson(value) {
265
+ return JSON.stringify(value, canonicalReplacer);
266
+ }
267
+ function canonicalReplacer(_key, val) {
268
+ if (val !== null && typeof val === "object" && !Array.isArray(val)) {
269
+ const sorted = {};
270
+ // eslint-disable-next-line unicorn/no-array-sort -- ng-packagr uses pre-ES2023 lib; toSorted unavailable
271
+ const keys = Object.keys(val).sort((left, right) => left.localeCompare(right));
272
+ for (const key of keys) {
273
+ sorted[key] = val[key];
274
+ }
275
+ return sorted;
276
+ }
277
+ return val;
278
+ }
279
+
280
+ function shouldNavigate(evt) {
281
+ return (evt.button === 0 &&
282
+ !evt.metaKey &&
283
+ !evt.altKey &&
284
+ !evt.ctrlKey &&
285
+ !evt.shiftKey);
286
+ }
287
+ function buildHref(router, routeName, routeParams) {
288
+ try {
289
+ const buildUrl = router.buildUrl;
290
+ if (buildUrl) {
291
+ const url = buildUrl(routeName, routeParams);
292
+ if (url !== undefined) {
293
+ return url;
294
+ }
295
+ }
296
+ return router.buildPath(routeName, routeParams);
297
+ }
298
+ catch {
299
+ console.error(`[real-router] Route "${routeName}" is not defined. The element will render without an href attribute.`);
300
+ return undefined;
301
+ }
302
+ }
303
+ function parseTokens(value) {
304
+ return value ? (value.match(/\S+/g) ?? []) : [];
305
+ }
306
+ function buildActiveClassName(isActive, activeClassName, baseClassName) {
307
+ if (isActive && activeClassName) {
308
+ const activeTokens = parseTokens(activeClassName);
309
+ if (activeTokens.length === 0) {
310
+ return baseClassName ?? undefined;
311
+ }
312
+ if (!baseClassName) {
313
+ return activeTokens.join(" ");
314
+ }
315
+ const baseTokens = parseTokens(baseClassName);
316
+ const seen = new Set(baseTokens);
317
+ for (const token of activeTokens) {
318
+ if (!seen.has(token)) {
319
+ seen.add(token);
320
+ baseTokens.push(token);
321
+ }
322
+ }
323
+ return baseTokens.join(" ");
324
+ }
325
+ return baseClassName ?? undefined;
326
+ }
327
+ function shallowEqual(prev, next) {
328
+ if (Object.is(prev, next)) {
329
+ return true;
330
+ }
331
+ if (!prev || !next) {
332
+ return false;
333
+ }
334
+ const prevKeys = Object.keys(prev);
335
+ if (prevKeys.length !== Object.keys(next).length) {
336
+ return false;
337
+ }
338
+ const prevRecord = prev;
339
+ const nextRecord = next;
340
+ for (const key of prevKeys) {
341
+ if (!Object.is(prevRecord[key], nextRecord[key])) {
342
+ return false;
343
+ }
344
+ }
345
+ return true;
346
+ }
347
+ function applyLinkA11y(element) {
348
+ if (!element) {
349
+ return;
350
+ }
351
+ if (element instanceof HTMLAnchorElement ||
352
+ element instanceof HTMLButtonElement) {
353
+ return;
354
+ }
355
+ if (!element.hasAttribute("role")) {
356
+ element.setAttribute("role", "link");
357
+ }
358
+ if (!element.hasAttribute("tabindex")) {
359
+ element.setAttribute("tabindex", "0");
360
+ }
361
+ }
362
+
9
363
  /** Must be called within an injection context (constructor, field initializer, runInInjectionContext). */
10
364
  function sourceToSignal(source) {
11
365
  const sig = signal(source.getSnapshot(), ...(ngDevMode ? [{ debugName: "sig" }] : /* istanbul ignore next */ []));
@@ -23,9 +377,9 @@ function sourceToSignal(source) {
23
377
  const ROUTER = new InjectionToken("ROUTER");
24
378
  const NAVIGATOR = new InjectionToken("NAVIGATOR");
25
379
  const ROUTE = new InjectionToken("ROUTE");
26
- function provideRealRouter(router) {
380
+ function provideRealRouter(router, options) {
27
381
  const navigator = getNavigator(router);
28
- return makeEnvironmentProviders([
382
+ const providers = [
29
383
  { provide: ROUTER, useValue: router },
30
384
  { provide: NAVIGATOR, useValue: navigator },
31
385
  {
@@ -35,7 +389,17 @@ function provideRealRouter(router) {
35
389
  navigator,
36
390
  }),
37
391
  },
38
- ]);
392
+ ];
393
+ if (options?.scrollRestoration) {
394
+ const scrollOpts = options.scrollRestoration;
395
+ providers.push(provideEnvironmentInitializer(() => {
396
+ const sr = createScrollRestoration(router, scrollOpts);
397
+ inject(DestroyRef).onDestroy(() => {
398
+ sr.destroy();
399
+ });
400
+ }));
401
+ }
402
+ return makeEnvironmentProviders(providers);
39
403
  }
40
404
 
41
405
  function injectOrThrow(token, fnName) {
@@ -107,6 +471,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
107
471
  args: [{ selector: "ng-template[routeNotFound]" }]
108
472
  }] });
109
473
 
474
+ class RouteSelf {
475
+ templateRef = inject(TemplateRef);
476
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: RouteSelf, deps: [], target: i0.ɵɵFactoryTarget.Directive });
477
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.8", type: RouteSelf, isStandalone: true, selector: "ng-template[routeSelf]", ngImport: i0 });
478
+ }
479
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: RouteSelf, decorators: [{
480
+ type: Directive,
481
+ args: [{ selector: "ng-template[routeSelf]" }]
482
+ }] });
483
+
110
484
  const EMPTY_SNAPSHOT = Object.freeze({
111
485
  route: undefined,
112
486
  previousRoute: undefined,
@@ -114,6 +488,7 @@ const EMPTY_SNAPSHOT = Object.freeze({
114
488
  class RouteView {
115
489
  nodeName = input("", { ...(ngDevMode ? { debugName: "nodeName" } : /* istanbul ignore next */ {}), alias: "routeNode" });
116
490
  matches = contentChildren(RouteMatch, { ...(ngDevMode ? { debugName: "matches" } : /* istanbul ignore next */ {}), descendants: true });
491
+ selfs = contentChildren(RouteSelf, { ...(ngDevMode ? { debugName: "selfs" } : /* istanbul ignore next */ {}), descendants: true });
117
492
  notFounds = contentChildren(RouteNotFound, { ...(ngDevMode ? { debugName: "notFounds" } : /* istanbul ignore next */ {}), descendants: true });
118
493
  activeTemplate = computed(() => {
119
494
  const snapshot = this.routeState();
@@ -128,6 +503,16 @@ class RouteView {
128
503
  return match.templateRef;
129
504
  }
130
505
  }
506
+ // Self has priority over NotFound. First-wins to mirror NotFound's
507
+ // last-wins inversion would be inconsistent with React/Preact/Solid/Vue
508
+ // adapters where Self is "first wins"; Angular's contentChildren returns
509
+ // declaration order, so picking [0] gives first-wins.
510
+ if (routeName === this.nodeName()) {
511
+ const first = this.selfs().at(0);
512
+ if (first) {
513
+ return first.templateRef;
514
+ }
515
+ }
131
516
  if (routeName === UNKNOWN_ROUTE) {
132
517
  const last = this.notFounds().at(-1);
133
518
  if (last) {
@@ -161,7 +546,7 @@ class RouteView {
161
546
  });
162
547
  }
163
548
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: RouteView, deps: [], target: i0.ɵɵFactoryTarget.Component });
164
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: RouteView, isStandalone: true, selector: "route-view", inputs: { nodeName: { classPropertyName: "nodeName", publicName: "routeNode", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "matches", predicate: RouteMatch, descendants: true, isSignal: true }, { propertyName: "notFounds", predicate: RouteNotFound, descendants: true, isSignal: true }], ngImport: i0, template: `
549
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: RouteView, isStandalone: true, selector: "route-view", inputs: { nodeName: { classPropertyName: "nodeName", publicName: "routeNode", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "matches", predicate: RouteMatch, descendants: true, isSignal: true }, { propertyName: "selfs", predicate: RouteSelf, descendants: true, isSignal: true }, { propertyName: "notFounds", predicate: RouteNotFound, descendants: true, isSignal: true }], ngImport: i0, template: `
165
550
  @if (activeTemplate()) {
166
551
  <ng-container [ngTemplateOutlet]="activeTemplate()!" />
167
552
  }
@@ -178,7 +563,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
178
563
  `,
179
564
  imports: [NgTemplateOutlet],
180
565
  }]
181
- }], propDecorators: { nodeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeNode", required: false }] }], matches: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteMatch), { ...{ descendants: true }, isSignal: true }] }], notFounds: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteNotFound), { ...{ descendants: true }, isSignal: true }] }] } });
566
+ }], propDecorators: { nodeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeNode", required: false }] }], matches: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteMatch), { ...{ descendants: true }, isSignal: true }] }], selfs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteSelf), { ...{ descendants: true }, isSignal: true }] }], notFounds: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteNotFound), { ...{ descendants: true }, isSignal: true }] }] } });
182
567
 
183
568
  class RouterErrorBoundary {
184
569
  errorTemplate = input(...(ngDevMode ? [undefined, { debugName: "errorTemplate" }] : /* istanbul ignore next */ []));
@@ -235,202 +620,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
235
620
  }]
236
621
  }], ctorParameters: () => [], propDecorators: { errorTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTemplate", required: false }] }], onError: [{ type: i0.Output, args: ["onError"] }] } });
237
622
 
238
- const CLEAR_DELAY = 7000;
239
- const SAFARI_READY_DELAY = 100;
240
- const ANNOUNCER_ATTR = "data-real-router-announcer";
241
- const INTERNAL_ROUTE_PREFIX = "@@";
242
- const VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0";
243
- function createRouteAnnouncer(router, options) {
244
- const prefix = options?.prefix ?? "Navigated to ";
245
- const getCustomText = options?.getAnnouncementText;
246
- let isInitialNavigation = true;
247
- let isReady = false;
248
- let isDestroyed = false;
249
- let lastAnnouncedText = "";
250
- let pendingText = null;
251
- let clearTimeoutId;
252
- const announcer = getOrCreateAnnouncer();
253
- const doAnnounce = (text, h1) => {
254
- lastAnnouncedText = text;
255
- clearTimeout(clearTimeoutId);
256
- announcer.textContent = text;
257
- clearTimeoutId = setTimeout(() => {
258
- announcer.textContent = "";
259
- lastAnnouncedText = "";
260
- }, CLEAR_DELAY);
261
- manageFocus(h1);
262
- };
263
- // Safari-ready delay: announcing before VoiceOver wires up the aria-live region
264
- // causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms
265
- // before marking the announcer "ready" — any navigation during that window is
266
- // buffered in pendingText and flushed once the delay expires.
267
- const safariTimeoutId = setTimeout(() => {
268
- isReady = true;
269
- if (pendingText !== null && !isDestroyed) {
270
- const text = pendingText;
271
- pendingText = null;
272
- doAnnounce(text, document.querySelector("h1"));
273
- }
274
- }, SAFARI_READY_DELAY);
275
- const unsubscribe = router.subscribe(({ route }) => {
276
- if (isInitialNavigation) {
277
- isInitialNavigation = false;
278
- return;
279
- }
280
- // Double rAF: waits for two paint frames so the incoming route's DOM
281
- // (including the new <h1>) is fully rendered before resolveText reads it.
282
- // Single rAF fires before the new route's template has been attached,
283
- // which would cause resolveText to pick up the OLD h1 or fall back to
284
- // document.title / route.name prematurely.
285
- requestAnimationFrame(() => {
286
- requestAnimationFrame(() => {
287
- if (isDestroyed) {
288
- return;
289
- }
290
- const h1 = document.querySelector("h1");
291
- const text = resolveText(route, prefix, getCustomText, h1);
292
- if (!text || text === lastAnnouncedText) {
293
- return;
294
- }
295
- if (!isReady) {
296
- // Defer announcement until Safari-ready window elapses (see safariTimeoutId).
297
- pendingText = text;
298
- return;
299
- }
300
- doAnnounce(text, h1);
301
- });
302
- });
303
- });
304
- return {
305
- destroy() {
306
- isDestroyed = true;
307
- unsubscribe();
308
- clearTimeout(clearTimeoutId);
309
- clearTimeout(safariTimeoutId);
310
- removeAnnouncer();
311
- },
312
- };
313
- }
314
- function getOrCreateAnnouncer() {
315
- const existing = document.querySelector(`[${ANNOUNCER_ATTR}]`);
316
- if (existing) {
317
- return existing;
318
- }
319
- const element = document.createElement("div");
320
- element.setAttribute("style", VISUALLY_HIDDEN);
321
- element.setAttribute("aria-live", "assertive");
322
- element.setAttribute("aria-atomic", "true");
323
- element.setAttribute(ANNOUNCER_ATTR, "");
324
- document.body.prepend(element);
325
- return element;
326
- }
327
- function removeAnnouncer() {
328
- document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();
329
- }
330
- function resolveText(route, prefix, getCustomText, h1) {
331
- if (getCustomText) {
332
- return getCustomText(route);
333
- }
334
- const h1Text = h1?.textContent.trim() ?? "";
335
- const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)
336
- ? ""
337
- : route.name;
338
- const rawText = h1Text || document.title || routeName || globalThis.location.pathname;
339
- return `${prefix}${rawText}`;
340
- }
341
- function manageFocus(h1) {
342
- if (!h1) {
343
- return;
344
- }
345
- if (!h1.hasAttribute("tabindex")) {
346
- h1.setAttribute("tabindex", "-1");
347
- }
348
- h1.focus({ preventScroll: true });
349
- }
350
-
351
- function shouldNavigate(evt) {
352
- return (evt.button === 0 &&
353
- !evt.metaKey &&
354
- !evt.altKey &&
355
- !evt.ctrlKey &&
356
- !evt.shiftKey);
357
- }
358
- function buildHref(router, routeName, routeParams) {
359
- try {
360
- const buildUrl = router.buildUrl;
361
- if (buildUrl) {
362
- const url = buildUrl(routeName, routeParams);
363
- if (url !== undefined) {
364
- return url;
365
- }
366
- }
367
- return router.buildPath(routeName, routeParams);
368
- }
369
- catch {
370
- console.error(`[real-router] Route "${routeName}" is not defined. The element will render without an href attribute.`);
371
- return undefined;
372
- }
373
- }
374
- function parseTokens(value) {
375
- return value ? (value.match(/\S+/g) ?? []) : [];
376
- }
377
- function buildActiveClassName(isActive, activeClassName, baseClassName) {
378
- if (isActive && activeClassName) {
379
- const activeTokens = parseTokens(activeClassName);
380
- if (activeTokens.length === 0) {
381
- return baseClassName ?? undefined;
382
- }
383
- if (!baseClassName) {
384
- return activeTokens.join(" ");
385
- }
386
- const baseTokens = parseTokens(baseClassName);
387
- const seen = new Set(baseTokens);
388
- for (const token of activeTokens) {
389
- if (!seen.has(token)) {
390
- seen.add(token);
391
- baseTokens.push(token);
392
- }
393
- }
394
- return baseTokens.join(" ");
395
- }
396
- return baseClassName ?? undefined;
397
- }
398
- function shallowEqual(prev, next) {
399
- if (Object.is(prev, next)) {
400
- return true;
401
- }
402
- if (!prev || !next) {
403
- return false;
404
- }
405
- const prevKeys = Object.keys(prev);
406
- if (prevKeys.length !== Object.keys(next).length) {
407
- return false;
408
- }
409
- const prevRecord = prev;
410
- const nextRecord = next;
411
- for (const key of prevKeys) {
412
- if (!Object.is(prevRecord[key], nextRecord[key])) {
413
- return false;
414
- }
415
- }
416
- return true;
417
- }
418
- function applyLinkA11y(element) {
419
- if (!element) {
420
- return;
421
- }
422
- if (element instanceof HTMLAnchorElement ||
423
- element instanceof HTMLButtonElement) {
424
- return;
425
- }
426
- if (!element.hasAttribute("role")) {
427
- element.setAttribute("role", "link");
428
- }
429
- if (!element.hasAttribute("tabindex")) {
430
- element.setAttribute("tabindex", "0");
431
- }
432
- }
433
-
434
623
  class NavigationAnnouncer {
435
624
  announcer = createRouteAnnouncer(injectRouter());
436
625
  constructor() {
@@ -563,5 +752,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
563
752
  * Generated bundle index. Do not edit.
564
753
  */
565
754
 
566
- export { NAVIGATOR, NavigationAnnouncer, ROUTE, ROUTER, RealLink, RealLinkActive, RouteMatch, RouteNotFound, RouteView, RouterErrorBoundary, injectIsActiveRoute, injectNavigator, injectRoute, injectRouteNode, injectRouteUtils, injectRouter, injectRouterTransition, provideRealRouter, sourceToSignal };
755
+ export { NAVIGATOR, NavigationAnnouncer, ROUTE, ROUTER, RealLink, RealLinkActive, RouteMatch, RouteNotFound, RouteSelf, RouteView, RouterErrorBoundary, injectIsActiveRoute, injectNavigator, injectRoute, injectRouteNode, injectRouteUtils, injectRouter, injectRouterTransition, provideRealRouter, sourceToSignal };
567
756
  //# sourceMappingURL=real-router-angular.mjs.map