-
Notifications
You must be signed in to change notification settings - Fork 27.2k
Expand file tree
/
Copy pathviewport_scroller.ts
More file actions
244 lines (216 loc) Β· 7.56 KB
/
viewport_scroller.ts
File metadata and controls
244 lines (216 loc) Β· 7.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
inject,
Ι΅Ι΅defineInjectable,
DOCUMENT,
Ι΅formatRuntimeError as formatRuntimeError,
} from '@angular/core';
import {RuntimeErrorCode} from './errors';
/**
* Defines a scroll position manager. Implemented by `BrowserViewportScroller`.
*
* @publicApi
*/
export abstract class ViewportScroller {
// De-sugared tree-shakable injection
// See #23917
/** @nocollapse */
static Ι΅prov = /** @pureOrBreakMyCode */ /* @__PURE__ */ Ι΅Ι΅defineInjectable({
token: ViewportScroller,
providedIn: 'root',
factory: () =>
typeof ngServerMode !== 'undefined' && ngServerMode
? new NullViewportScroller()
: new BrowserViewportScroller(inject(DOCUMENT), window),
});
/**
* Configures the top offset used when scrolling to an anchor.
* @param offset A position in screen coordinates (a tuple with x and y values)
* or a function that returns the top offset position.
*
*/
abstract setOffset(offset: [number, number] | (() => [number, number])): void;
/**
* Retrieves the current scroll position.
* @returns A position in screen coordinates (a tuple with x and y values).
*/
abstract getScrollPosition(): [number, number];
/**
* Scrolls to a specified position.
* @param position A position in screen coordinates (a tuple with x and y values).
*/
abstract scrollToPosition(position: [number, number], options?: ScrollOptions): void;
/**
* Scrolls to an anchor element.
* @param anchor The ID of the anchor element.
*/
abstract scrollToAnchor(anchor: string, options?: ScrollOptions): void;
/**
* Disables automatic scroll restoration provided by the browser.
* See also [window.history.scrollRestoration
* info](https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration).
*/
abstract setHistoryScrollRestoration(scrollRestoration: 'auto' | 'manual'): void;
}
/**
* Manages the scroll position for a browser window.
*/
export class BrowserViewportScroller implements ViewportScroller {
private offset: () => [number, number] = () => [0, 0];
constructor(
private document: Document,
private window: Window,
) {}
/**
* Configures the top offset used when scrolling to an anchor.
* @param offset A position in screen coordinates (a tuple with x and y values)
* or a function that returns the top offset position.
*
*/
setOffset(offset: [number, number] | (() => [number, number])): void {
if (Array.isArray(offset)) {
this.offset = () => offset;
} else {
this.offset = offset;
}
}
/**
* Retrieves the current scroll position.
* @returns The position in screen coordinates.
*/
getScrollPosition(): [number, number] {
return [this.window.scrollX, this.window.scrollY];
}
/**
* Sets the scroll position.
* @param position The new position in screen coordinates.
*/
scrollToPosition(position: [number, number], options?: ScrollOptions): void {
this.window.scrollTo({...options, left: position[0], top: position[1]});
}
/**
* Scrolls to an element and attempts to focus the element.
*
* Note that the function name here is misleading in that the target string may be an ID for a
* non-anchor element.
*
* @param target The ID of an element or name of the anchor.
*
* @see https://html.spec.whatwg.org/#the-indicated-part-of-the-document
* @see https://html.spec.whatwg.org/#scroll-to-fragid
*/
scrollToAnchor(target: string, options?: ScrollOptions): void {
const elSelected = findAnchorFromDocument(this.document, target);
if (elSelected) {
this.scrollToElement(elSelected, options);
// After scrolling to the element, the spec dictates that we follow the focus steps for the
// target. Rather than following the robust steps, simply attempt focus.
//
// @see https://html.spec.whatwg.org/#get-the-focusable-area
// @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus
// @see https://html.spec.whatwg.org/#focusable-area
elSelected.focus();
}
}
/**
* Disables automatic scroll restoration provided by the browser.
*/
setHistoryScrollRestoration(scrollRestoration: 'auto' | 'manual'): void {
try {
this.window.history.scrollRestoration = scrollRestoration;
} catch {
console.warn(
formatRuntimeError(
RuntimeErrorCode.SCROLL_RESTORATION_UNSUPPORTED,
ngDevMode &&
'Failed to set `window.history.scrollRestoration`. ' +
'This may occur when:\n' +
'β’ The script is running inside a sandboxed iframe\n' +
'β’ The window is partially navigated or inactive\n' +
'β’ The script is executed in an untrusted or special context (e.g., test runners, browser extensions, or content previews)\n' +
'Scroll position may not be preserved across navigation.',
),
);
}
}
/**
* Scrolls to an element using the native offset and the specified offset set on this scroller.
*
* The offset can be used when we know that there is a floating header and scrolling naively to an
* element (ex: `scrollIntoView`) leaves the element hidden behind the floating header.
*/
private scrollToElement(el: HTMLElement, options?: ScrollOptions): void {
const rect = el.getBoundingClientRect();
const left = rect.left + this.window.pageXOffset;
const top = rect.top + this.window.pageYOffset;
const offset = this.offset();
this.window.scrollTo({
...options,
left: left - offset[0],
top: top - offset[1],
});
}
}
function findAnchorFromDocument(document: Document, target: string): HTMLElement | null {
const documentResult = document.getElementById(target) || document.getElementsByName(target)[0];
if (documentResult) {
return documentResult;
}
// `getElementById` and `getElementsByName` won't pierce through the shadow DOM so we
// have to traverse the DOM manually and do the lookup through the shadow roots.
if (
typeof document.createTreeWalker === 'function' &&
document.body &&
typeof document.body.attachShadow === 'function'
) {
const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
let currentNode = treeWalker.currentNode as HTMLElement | null;
while (currentNode) {
const shadowRoot = currentNode.shadowRoot;
if (shadowRoot) {
// Note that `ShadowRoot` doesn't support `getElementsByName`
// so we have to fall back to `querySelector`.
const result =
shadowRoot.getElementById(target) || shadowRoot.querySelector(`[name="${target}"]`);
if (result) {
return result;
}
}
currentNode = treeWalker.nextNode() as HTMLElement | null;
}
}
return null;
}
/**
* Provides an empty implementation of the viewport scroller.
*/
export class NullViewportScroller implements ViewportScroller {
/**
* Empty implementation
*/
setOffset(offset: [number, number] | (() => [number, number])): void {}
/**
* Empty implementation
*/
getScrollPosition(): [number, number] {
return [0, 0];
}
/**
* Empty implementation
*/
scrollToPosition(position: [number, number]): void {}
/**
* Empty implementation
*/
scrollToAnchor(anchor: string): void {}
/**
* Empty implementation
*/
setHistoryScrollRestoration(scrollRestoration: 'auto' | 'manual'): void {}
}