@orangesk/orange-design-system 2.0.0-beta.41 → 2.0.0-beta.43
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/build/components/AnchorNavigation/style.css +1 -1
- package/build/components/AnchorNavigation/style.css.map +1 -1
- package/build/components/BlockAction/style.css +1 -1
- package/build/components/BlockAction/style.css.map +1 -1
- package/build/components/Buttons/style.css +1 -1
- package/build/components/Buttons/style.css.map +1 -1
- package/build/components/Carousel/style.css +1 -1
- package/build/components/Carousel/style.css.map +1 -1
- package/build/components/Grid/style.css +1 -1
- package/build/components/Grid/style.css.map +1 -1
- package/build/components/Link/style.css +1 -1
- package/build/components/Link/style.css.map +1 -1
- package/build/components/Megamenu/style.css +1 -1
- package/build/components/Megamenu/style.css.map +1 -1
- package/build/components/Modal/style.css +1 -1
- package/build/components/Modal/style.css.map +1 -1
- package/build/components/index.js +1 -1
- package/build/components/index.js.map +1 -1
- package/build/components/tsconfig.tsbuildinfo +1 -1
- package/build/components/types/src/components/AnchorNavigation/AnchorNavigation.static.d.ts +40 -0
- package/build/components/types/src/components/Carousel/Carousel.static.d.ts +0 -5
- package/build/components/types/src/components/Megamenu/Megamenu.static.d.ts +3 -0
- package/build/components/types/src/components/Modal/Modal.static.d.ts +1 -0
- package/build/lib/components.css +1 -1
- package/build/lib/components.css.map +1 -1
- package/build/lib/megamenu.css +1 -1
- package/build/lib/megamenu.css.map +1 -1
- package/build/lib/megamenu.js +1 -1
- package/build/lib/megamenu.js.map +1 -1
- package/build/lib/scripts.js +1 -1
- package/build/lib/scripts.js.map +1 -1
- package/build/lib/style.css +1 -1
- package/build/lib/style.css.map +1 -1
- package/build/lib/tsconfig.tsbuildinfo +1 -1
- package/build/sprite.svg +1 -1
- package/package.json +23 -23
- package/src/assets/icons/youtube.svg +3 -1
- package/src/components/AnchorNavigation/AnchorNavigation.static.ts +385 -77
- package/src/components/AnchorNavigation/styles/mixins.scss +18 -2
- package/src/components/AnchorNavigation/tests/AnchorNavigation.unit.test.jsx +266 -8
- package/src/components/BlockAction/styles/mixins.scss +1 -1
- package/src/components/Buttons/styles/mixins.scss +8 -13
- package/src/components/Carousel/Carousel.static.ts +5 -26
- package/src/components/Carousel/styles/mixins.scss +1 -1
- package/src/components/Carousel/tests/Carousel.static.test.jsx +117 -0
- package/src/components/Grid/styles/mixins.scss +14 -6
- package/src/components/Link/styles/mixins.scss +0 -1
- package/src/components/Megamenu/Megamenu.static.ts +27 -1
- package/src/components/Megamenu/styles/mixins.scss +4 -0
- package/src/components/Modal/Modal.static.ts +29 -7
- package/src/components/Modal/styles/mixins.scss +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangesk/orange-design-system",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.43",
|
|
4
4
|
"private": false,
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.x"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@cloudfour/transition-hidden-element": "^2.0.2",
|
|
56
56
|
"@mdx-js/loader": "^3.1.1",
|
|
57
57
|
"@mdx-js/react": "^3.1.1",
|
|
58
|
-
"@next/mdx": "16.2.
|
|
58
|
+
"@next/mdx": "16.2.4",
|
|
59
59
|
"@orangesk/accessible-autocomplete": "3.2.2",
|
|
60
60
|
"@popperjs/core": "^2.11.8",
|
|
61
61
|
"@types/mdx": "^2.0.13",
|
|
@@ -63,19 +63,19 @@
|
|
|
63
63
|
"classnames": "^2.5.1",
|
|
64
64
|
"daypickr": "^0.3.4",
|
|
65
65
|
"diff2html": "^3.4.56",
|
|
66
|
-
"dompurify": "^3.
|
|
67
|
-
"html-react-parser": "
|
|
66
|
+
"dompurify": "^3.4.0",
|
|
67
|
+
"html-react-parser": "6.0.1",
|
|
68
68
|
"lorem-ipsum": "^2.0.8",
|
|
69
69
|
"minisearch": "7.2.0",
|
|
70
|
-
"next": "16.2.
|
|
70
|
+
"next": "16.2.4",
|
|
71
71
|
"normalize.css": "^8.0.1",
|
|
72
72
|
"nouislider": "^15.8.1",
|
|
73
73
|
"prism-react-renderer": "^2.4.1",
|
|
74
74
|
"query-string": "^9.3.1",
|
|
75
|
-
"react": "^19.2.
|
|
76
|
-
"react-dom": "^19.2.
|
|
75
|
+
"react": "^19.2.5",
|
|
76
|
+
"react-dom": "^19.2.5",
|
|
77
77
|
"react-element-to-jsx-string": "^17.0.1",
|
|
78
|
-
"react-is": "^19.2.
|
|
78
|
+
"react-is": "^19.2.5",
|
|
79
79
|
"rehype-autolink-headings": "^7.1.0",
|
|
80
80
|
"rehype-slug": "^6.0.0",
|
|
81
81
|
"remark-gemoji": "^8.0.0",
|
|
@@ -99,40 +99,40 @@
|
|
|
99
99
|
"@rollup/plugin-terser": "1.0.0",
|
|
100
100
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
101
101
|
"@rollup/plugin-url": "^8.0.2",
|
|
102
|
-
"@rollup/rollup-darwin-arm64": "^4.60.
|
|
102
|
+
"@rollup/rollup-darwin-arm64": "^4.60.2",
|
|
103
103
|
"@testing-library/dom": "^10.4.1",
|
|
104
104
|
"@testing-library/jest-dom": "^6.9.1",
|
|
105
105
|
"@testing-library/react": "^16.3.2",
|
|
106
106
|
"@testing-library/user-event": "^14.6.1",
|
|
107
|
-
"@types/node": "25.
|
|
107
|
+
"@types/node": "25.6.0",
|
|
108
108
|
"@types/react": "19.2.14",
|
|
109
109
|
"@types/react-dom": "19.2.3",
|
|
110
110
|
"@types/wnumb": "^1.2.3",
|
|
111
111
|
"@vitejs/plugin-react": "6.0.1",
|
|
112
|
-
"@vitest/browser": "^4.1.
|
|
113
|
-
"@vitest/browser-playwright": "^4.1.
|
|
114
|
-
"@vitest/coverage-v8": "^4.1.
|
|
115
|
-
"@vitest/ui": "^4.1.
|
|
112
|
+
"@vitest/browser": "^4.1.4",
|
|
113
|
+
"@vitest/browser-playwright": "^4.1.4",
|
|
114
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
115
|
+
"@vitest/ui": "^4.1.4",
|
|
116
116
|
"canvas": "^3.2.3",
|
|
117
117
|
"fs-extra": "^11.3.4",
|
|
118
118
|
"glob": "13.0.6",
|
|
119
|
-
"html-validate": "10.
|
|
119
|
+
"html-validate": "10.13.1",
|
|
120
120
|
"husky": "^9.1.7",
|
|
121
121
|
"identity-obj-proxy": "^3.0.0",
|
|
122
|
-
"jsdom": "29.0.
|
|
122
|
+
"jsdom": "29.0.2",
|
|
123
123
|
"lint-staged": "16.4.0",
|
|
124
|
-
"playwright": "^1.59.
|
|
125
|
-
"prettier": "^3.8.
|
|
126
|
-
"rollup": "^4.60.
|
|
124
|
+
"playwright": "^1.59.1",
|
|
125
|
+
"prettier": "^3.8.3",
|
|
126
|
+
"rollup": "^4.60.2",
|
|
127
127
|
"rollup-plugin-copy": "^3.5.0",
|
|
128
128
|
"rollup-plugin-dts": "^6.4.1",
|
|
129
129
|
"rollup-plugin-postcss": "^4.0.2",
|
|
130
|
-
"sass": "^1.
|
|
130
|
+
"sass": "^1.99.0",
|
|
131
131
|
"svg-sprite": "^2.0.4",
|
|
132
|
-
"typescript": "6.0.
|
|
133
|
-
"vitest": "^4.1.
|
|
132
|
+
"typescript": "6.0.3",
|
|
133
|
+
"vitest": "^4.1.4",
|
|
134
134
|
"vitest-axe": "^0.1.0",
|
|
135
|
-
"vitest-browser-react": "^2.
|
|
135
|
+
"vitest-browser-react": "^2.2.0"
|
|
136
136
|
},
|
|
137
137
|
"lint-staged": {
|
|
138
138
|
"*.{js,jsx,ts,tsx,json,css}": "biome check --write --no-errors-on-unmatched",
|
|
@@ -1 +1,3 @@
|
|
|
1
|
-
<svg
|
|
1
|
+
<svg viewBox="0 0 80 80" id="youtube">
|
|
2
|
+
<path xmlns="http://www.w3.org/2000/svg" d="M63.275 30.05S62.8 26.625 61.4 25.1c-1.825-1.975-3.85-2-4.75-2.1-6.7-.5-16.65-.5-16.65-.5s-10 0-16.625.5c-.925.125-2.95.125-4.75 2.1-1.425 1.5-1.9 4.95-1.9 4.95s-.475 4.025-.475 8.05v3.775c0 4.025.475 8.05.475 8.05s.475 3.425 1.875 4.95c1.825 2 4.2 1.925 5.25 2.125 3.8.375 16.15.5 16.15.5s10 0 16.625-.5c.925-.15 2.95-.15 4.75-2.125 1.425-1.5 1.9-4.95 1.9-4.95s.475-4 .475-8.05v-3.75c0-4.05-.475-8.075-.475-8.075ZM36.25 47.5v-15l12.5 7.525-12.5 7.475Z"></path>
|
|
3
|
+
</svg>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export default class AnchorNavigation {
|
|
2
|
+
private static readonly MODAL_OPEN_BODY_CLASS = "has-modal";
|
|
3
|
+
private static readonly STICKY_FLOAT_TOLERANCE = 3;
|
|
2
4
|
private static readonly SCROLLSPY_CENTER_BUFFER = 50;
|
|
3
5
|
private static readonly TOP_SECTION_THRESHOLD = 100;
|
|
4
6
|
private static readonly DRAG_START_THRESHOLD = 6;
|
|
5
7
|
private static readonly SCROLL_END_DEBOUNCE_MS = 150;
|
|
6
8
|
private static readonly SCROLL_EDGE_TOLERANCE = 1;
|
|
9
|
+
private static readonly SCROLL_ALIGNMENT_TOLERANCE = 1;
|
|
10
|
+
private static readonly MAX_SCROLL_CORRECTIONS = 2;
|
|
7
11
|
|
|
8
12
|
private element: HTMLElement;
|
|
9
13
|
private contentLeftElement: HTMLElement | null;
|
|
@@ -34,6 +38,23 @@ export default class AnchorNavigation {
|
|
|
34
38
|
private sections: HTMLElement[] = [];
|
|
35
39
|
private currentPath: string;
|
|
36
40
|
private lastActiveIndex: number = 0;
|
|
41
|
+
private autoScrollTargetId: string | null = null;
|
|
42
|
+
private autoScrollCorrectionCount: number = 0;
|
|
43
|
+
private lastStickyOffset: number = 0;
|
|
44
|
+
private modalClassObserver: MutationObserver | null = null;
|
|
45
|
+
private modalStateHandler: () => void;
|
|
46
|
+
private isForcedFixed: boolean = false;
|
|
47
|
+
private wasStickyFloating: boolean = false;
|
|
48
|
+
private forcedFixedTop: number = 0;
|
|
49
|
+
private forcedFixedLeft: number = 0;
|
|
50
|
+
private fixedFlowPlaceholder: HTMLDivElement | null = null;
|
|
51
|
+
private originalInlineStyles: {
|
|
52
|
+
position: string;
|
|
53
|
+
top: string;
|
|
54
|
+
left: string;
|
|
55
|
+
right: string;
|
|
56
|
+
width: string;
|
|
57
|
+
} | null = null;
|
|
37
58
|
|
|
38
59
|
constructor(element: HTMLElement) {
|
|
39
60
|
this.element = element;
|
|
@@ -58,6 +79,7 @@ export default class AnchorNavigation {
|
|
|
58
79
|
this.horizontalScrollHandler =
|
|
59
80
|
this.updateHorizontalOverflowState.bind(this);
|
|
60
81
|
this.nativeDragStartHandler = this.handleNativeDragStart.bind(this);
|
|
82
|
+
this.modalStateHandler = this.handleModalStateChange.bind(this);
|
|
61
83
|
|
|
62
84
|
(this.element as any).ODS_AnchorNavigation = this;
|
|
63
85
|
|
|
@@ -76,15 +98,44 @@ export default class AnchorNavigation {
|
|
|
76
98
|
return document.querySelector("[data-megamenu]") as HTMLElement | null;
|
|
77
99
|
}
|
|
78
100
|
|
|
101
|
+
private getMegamenuOffsetHeight(): number {
|
|
102
|
+
return this.megamenuElement?.offsetHeight || 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
private updateStickyPosition(): void {
|
|
106
|
+
if (this.isForcedFixed) return;
|
|
107
|
+
|
|
80
108
|
if (!this.megamenuElement) return;
|
|
81
|
-
|
|
109
|
+
|
|
110
|
+
const stickyOffset = this.getMegamenuOffsetHeight();
|
|
111
|
+
const hasStickyOffsetChanged = stickyOffset !== this.lastStickyOffset;
|
|
112
|
+
|
|
113
|
+
this.lastStickyOffset = stickyOffset;
|
|
114
|
+
this.element.style.top = `${stickyOffset}px`;
|
|
115
|
+
this.updateStickyFloatingState();
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
!hasStickyOffsetChanged ||
|
|
119
|
+
!this.isAutoScrolling ||
|
|
120
|
+
!this.autoScrollTargetId ||
|
|
121
|
+
this.autoScrollCorrectionCount >= AnchorNavigation.MAX_SCROLL_CORRECTIONS
|
|
122
|
+
) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const targetElement = document.getElementById(this.autoScrollTargetId);
|
|
127
|
+
if (!targetElement) return;
|
|
128
|
+
|
|
129
|
+
// Re-target smoothly as soon as sticky offset changes to avoid a visible snap at scroll end.
|
|
130
|
+
this.autoScrollCorrectionCount += 1;
|
|
131
|
+
this.scrollToSection(targetElement, "smooth");
|
|
82
132
|
}
|
|
83
133
|
|
|
84
134
|
private setupMegamenuObserver(): void {
|
|
85
135
|
this.megamenuElement = this.findMegamenuElement();
|
|
86
136
|
|
|
87
137
|
if (!this.megamenuElement) {
|
|
138
|
+
this.lastStickyOffset = 0;
|
|
88
139
|
this.element.style.top = "0px";
|
|
89
140
|
return;
|
|
90
141
|
}
|
|
@@ -132,6 +183,7 @@ export default class AnchorNavigation {
|
|
|
132
183
|
window.addEventListener("resize", this.resizeHandler);
|
|
133
184
|
|
|
134
185
|
this.initScrollSpy();
|
|
186
|
+
this.alignInitialHashIfNeeded();
|
|
135
187
|
}
|
|
136
188
|
|
|
137
189
|
private teardownScrollSpyListeners(): void {
|
|
@@ -190,27 +242,129 @@ export default class AnchorNavigation {
|
|
|
190
242
|
const targetElement = document.getElementById(targetId);
|
|
191
243
|
if (!targetElement) return;
|
|
192
244
|
|
|
193
|
-
this.
|
|
245
|
+
this.startAutoScroll(targetId);
|
|
194
246
|
anchor.blur();
|
|
195
247
|
|
|
248
|
+
this.scrollToSection(targetElement, "smooth");
|
|
249
|
+
|
|
250
|
+
const nextUrl = `${window.location.pathname}${window.location.search}#${targetId}`;
|
|
251
|
+
window.history.pushState(null, "", nextUrl);
|
|
252
|
+
this.initScrollSpy(targetId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private getHashSectionId(): string | null {
|
|
256
|
+
const hash = window.location.hash;
|
|
257
|
+
if (!hash || hash.length <= 1) return null;
|
|
258
|
+
|
|
259
|
+
let hashId = hash.slice(1);
|
|
260
|
+
try {
|
|
261
|
+
hashId = decodeURIComponent(hashId);
|
|
262
|
+
} catch {
|
|
263
|
+
// Keep raw hash when decoding fails.
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return hashId || null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private startAutoScroll(sectionId: string): void {
|
|
270
|
+
this.isAutoScrolling = true;
|
|
271
|
+
this.autoScrollTargetId = sectionId;
|
|
272
|
+
this.autoScrollCorrectionCount = 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private resetAutoScrollState(): void {
|
|
276
|
+
this.isAutoScrolling = false;
|
|
277
|
+
this.autoScrollTargetId = null;
|
|
278
|
+
this.autoScrollCorrectionCount = 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private tryCorrectAutoScrollAlignment(): boolean {
|
|
282
|
+
if (!this.autoScrollTargetId) return true;
|
|
283
|
+
|
|
284
|
+
const targetElement = document.getElementById(this.autoScrollTargetId);
|
|
285
|
+
if (!targetElement) return true;
|
|
286
|
+
|
|
287
|
+
const targetTop = this.getTargetTop(targetElement);
|
|
288
|
+
const distance = Math.abs(window.scrollY - targetTop);
|
|
289
|
+
if (distance <= AnchorNavigation.SCROLL_ALIGNMENT_TOLERANCE) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (
|
|
294
|
+
this.autoScrollCorrectionCount >= AnchorNavigation.MAX_SCROLL_CORRECTIONS
|
|
295
|
+
) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.autoScrollCorrectionCount += 1;
|
|
300
|
+
const correctionBehavior: ScrollBehavior =
|
|
301
|
+
this.autoScrollCorrectionCount === 1 ? "smooth" : "auto";
|
|
302
|
+
this.scrollToSection(targetElement, correctionBehavior);
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private getTotalStickyOffset(): number {
|
|
196
307
|
const scrollOffset = this.megamenuElement
|
|
197
308
|
? this.megamenuElement.offsetHeight
|
|
198
309
|
: 0;
|
|
199
|
-
const
|
|
310
|
+
const anchorNavOffset = this.element.offsetHeight;
|
|
311
|
+
return scrollOffset + anchorNavOffset;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private getConfiguredScrollPaddingTop(): number {
|
|
315
|
+
const rootStyles = window.getComputedStyle(document.documentElement);
|
|
316
|
+
const rawScrollPaddingTop = rootStyles.scrollPaddingTop;
|
|
317
|
+
const parsedScrollPaddingTop = Number.parseFloat(rawScrollPaddingTop);
|
|
318
|
+
|
|
319
|
+
if (Number.isFinite(parsedScrollPaddingTop) && parsedScrollPaddingTop > 0) {
|
|
320
|
+
return parsedScrollPaddingTop;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private getEffectiveScrollOffset(): number {
|
|
327
|
+
const configuredScrollPaddingTop = this.getConfiguredScrollPaddingTop();
|
|
328
|
+
if (configuredScrollPaddingTop > 0) {
|
|
329
|
+
return configuredScrollPaddingTop;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return this.getTotalStickyOffset();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private getTargetTop(targetElement: HTMLElement): number {
|
|
200
336
|
const targetTop =
|
|
201
337
|
targetElement.getBoundingClientRect().top +
|
|
202
338
|
window.scrollY -
|
|
203
|
-
|
|
204
|
-
|
|
339
|
+
this.getEffectiveScrollOffset();
|
|
340
|
+
return Math.max(0, targetTop);
|
|
341
|
+
}
|
|
205
342
|
|
|
343
|
+
private scrollToSection(
|
|
344
|
+
targetElement: HTMLElement,
|
|
345
|
+
behavior: ScrollBehavior,
|
|
346
|
+
): void {
|
|
206
347
|
window.scrollTo({
|
|
207
|
-
top:
|
|
208
|
-
behavior
|
|
348
|
+
top: this.getTargetTop(targetElement),
|
|
349
|
+
behavior,
|
|
209
350
|
});
|
|
351
|
+
}
|
|
210
352
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
353
|
+
private alignInitialHashIfNeeded(): void {
|
|
354
|
+
const hashId = this.getHashSectionId();
|
|
355
|
+
if (!hashId) return;
|
|
356
|
+
|
|
357
|
+
const targetElement = document.getElementById(hashId);
|
|
358
|
+
if (!targetElement) return;
|
|
359
|
+
|
|
360
|
+
this.startAutoScroll(hashId);
|
|
361
|
+
|
|
362
|
+
requestAnimationFrame(() => {
|
|
363
|
+
this.scrollToSection(targetElement, "auto");
|
|
364
|
+
requestAnimationFrame(() => {
|
|
365
|
+
this.handleScrollEnd();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
214
368
|
}
|
|
215
369
|
|
|
216
370
|
private initScrollSpy(forcedSectionId: string | null = null): void {
|
|
@@ -231,12 +385,7 @@ export default class AnchorNavigation {
|
|
|
231
385
|
);
|
|
232
386
|
}
|
|
233
387
|
} else {
|
|
234
|
-
|
|
235
|
-
const scrollOffset = this.megamenuElement
|
|
236
|
-
? this.megamenuElement.offsetHeight
|
|
237
|
-
: 0;
|
|
238
|
-
const anchorNavOffset = this.element.offsetHeight;
|
|
239
|
-
const totalOffset = scrollOffset + anchorNavOffset;
|
|
388
|
+
const totalOffset = this.getEffectiveScrollOffset();
|
|
240
389
|
const effectiveCenter =
|
|
241
390
|
window.scrollY + totalOffset + AnchorNavigation.SCROLLSPY_CENTER_BUFFER;
|
|
242
391
|
|
|
@@ -323,37 +472,15 @@ export default class AnchorNavigation {
|
|
|
323
472
|
0,
|
|
324
473
|
contentLeft.scrollWidth - contentLeft.clientWidth,
|
|
325
474
|
);
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const isFirstItem = activeLink === firstNavLink;
|
|
336
|
-
const isLastItem = activeLink === lastNavLink;
|
|
337
|
-
|
|
338
|
-
let targetScrollLeft: number | null = null;
|
|
339
|
-
if (isFirstItem) {
|
|
340
|
-
targetScrollLeft = 0;
|
|
341
|
-
} else if (isLastItem) {
|
|
342
|
-
targetScrollLeft = maxScrollLeft;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const itemLeft = activeLink.offsetLeft;
|
|
346
|
-
const itemRight = itemLeft + activeLink.clientWidth;
|
|
347
|
-
const viewportLeft = contentLeft.scrollLeft;
|
|
348
|
-
const viewportRight = viewportLeft + contentLeft.clientWidth;
|
|
349
|
-
const isVisible = itemLeft >= viewportLeft && itemRight <= viewportRight;
|
|
350
|
-
|
|
351
|
-
if (targetScrollLeft === null && !forceCenter && isVisible) return;
|
|
352
|
-
|
|
353
|
-
if (targetScrollLeft === null) {
|
|
354
|
-
targetScrollLeft =
|
|
355
|
-
itemLeft - contentLeft.clientWidth / 2 + activeLink.clientWidth / 2;
|
|
356
|
-
}
|
|
475
|
+
const contentRect = contentLeft.getBoundingClientRect();
|
|
476
|
+
const itemRect = activeLink.getBoundingClientRect();
|
|
477
|
+
const itemCenterWithinContent =
|
|
478
|
+
itemRect.left -
|
|
479
|
+
contentRect.left +
|
|
480
|
+
contentLeft.scrollLeft +
|
|
481
|
+
itemRect.width / 2;
|
|
482
|
+
const targetScrollLeft =
|
|
483
|
+
itemCenterWithinContent - contentLeft.clientWidth / 2;
|
|
357
484
|
|
|
358
485
|
const behavior = window.innerWidth < 768 ? "auto" : "smooth";
|
|
359
486
|
const nextScrollLeft = Math.min(
|
|
@@ -361,6 +488,11 @@ export default class AnchorNavigation {
|
|
|
361
488
|
Math.max(0, targetScrollLeft),
|
|
362
489
|
);
|
|
363
490
|
|
|
491
|
+
const isAlreadyAligned =
|
|
492
|
+
Math.abs(contentLeft.scrollLeft - nextScrollLeft) <=
|
|
493
|
+
AnchorNavigation.SCROLL_ALIGNMENT_TOLERANCE;
|
|
494
|
+
if (!forceCenter && isAlreadyAligned) return;
|
|
495
|
+
|
|
364
496
|
if (typeof contentLeft.scrollTo === "function") {
|
|
365
497
|
contentLeft.scrollTo({
|
|
366
498
|
left: nextScrollLeft,
|
|
@@ -559,11 +691,198 @@ export default class AnchorNavigation {
|
|
|
559
691
|
}
|
|
560
692
|
|
|
561
693
|
private handleResize(): void {
|
|
694
|
+
if (this.isForcedFixed) {
|
|
695
|
+
const parentRect = this.element.parentElement?.getBoundingClientRect();
|
|
696
|
+
const megamenuHeight = this.getMegamenuOffsetHeight();
|
|
697
|
+
|
|
698
|
+
if (parentRect) {
|
|
699
|
+
this.forcedFixedLeft = Math.round(parentRect.left);
|
|
700
|
+
this.element.style.width = `${Math.round(parentRect.width)}px`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (megamenuHeight > 0) {
|
|
704
|
+
this.forcedFixedTop = megamenuHeight;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
this.element.style.top = `${this.forcedFixedTop}px`;
|
|
708
|
+
this.element.style.left = `${this.forcedFixedLeft}px`;
|
|
709
|
+
|
|
710
|
+
if (this.fixedFlowPlaceholder) {
|
|
711
|
+
this.fixedFlowPlaceholder.style.height = `${this.element.offsetHeight}px`;
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
this.updateStickyFloatingState();
|
|
715
|
+
}
|
|
716
|
+
|
|
562
717
|
this.initScrollSpy();
|
|
563
718
|
this.updateDragState();
|
|
564
719
|
this.updateHorizontalOverflowState();
|
|
565
720
|
}
|
|
566
721
|
|
|
722
|
+
private isAnyModalOpen(): boolean {
|
|
723
|
+
return document.body.classList.contains(
|
|
724
|
+
AnchorNavigation.MODAL_OPEN_BODY_CLASS,
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private isCurrentlyStickyFloating(): boolean {
|
|
729
|
+
if (!this.megamenuElement) return false;
|
|
730
|
+
|
|
731
|
+
const stickyTop = this.getMegamenuOffsetHeight();
|
|
732
|
+
const currentTop = this.element.getBoundingClientRect().top;
|
|
733
|
+
|
|
734
|
+
return currentTop <= stickyTop + AnchorNavigation.STICKY_FLOAT_TOLERANCE;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private updateStickyFloatingState(): void {
|
|
738
|
+
if (this.isAnyModalOpen() || this.isForcedFixed) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
this.wasStickyFloating = this.isCurrentlyStickyFloating();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private ensureFixedFlowPlaceholder(): void {
|
|
746
|
+
if (!this.fixedFlowPlaceholder) {
|
|
747
|
+
this.fixedFlowPlaceholder = document.createElement("div");
|
|
748
|
+
this.fixedFlowPlaceholder.setAttribute("aria-hidden", "true");
|
|
749
|
+
this.fixedFlowPlaceholder.style.width = "100%";
|
|
750
|
+
this.fixedFlowPlaceholder.style.pointerEvents = "none";
|
|
751
|
+
this.fixedFlowPlaceholder.style.visibility = "hidden";
|
|
752
|
+
this.element.insertAdjacentElement("afterend", this.fixedFlowPlaceholder);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
this.fixedFlowPlaceholder.style.height = `${this.element.offsetHeight}px`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private removeFixedFlowPlaceholder(): void {
|
|
759
|
+
if (!this.fixedFlowPlaceholder) return;
|
|
760
|
+
|
|
761
|
+
this.fixedFlowPlaceholder.remove();
|
|
762
|
+
this.fixedFlowPlaceholder = null;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private applyFixedPosition(): void {
|
|
766
|
+
if (this.isForcedFixed) return;
|
|
767
|
+
|
|
768
|
+
const rect = this.element.getBoundingClientRect();
|
|
769
|
+
const parentRect = this.element.parentElement?.getBoundingClientRect();
|
|
770
|
+
const megamenuHeight = this.getMegamenuOffsetHeight();
|
|
771
|
+
|
|
772
|
+
this.originalInlineStyles = {
|
|
773
|
+
position: this.element.style.position,
|
|
774
|
+
top: this.element.style.top,
|
|
775
|
+
left: this.element.style.left,
|
|
776
|
+
right: this.element.style.right,
|
|
777
|
+
width: this.element.style.width,
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
this.forcedFixedTop =
|
|
781
|
+
megamenuHeight > 0 ? megamenuHeight : Math.max(0, Math.round(rect.top));
|
|
782
|
+
this.forcedFixedLeft = Math.max(
|
|
783
|
+
0,
|
|
784
|
+
Math.round(parentRect?.left ?? rect.left),
|
|
785
|
+
);
|
|
786
|
+
const fixedWidth = Math.round(parentRect?.width ?? rect.width);
|
|
787
|
+
|
|
788
|
+
this.element.style.position = "fixed";
|
|
789
|
+
this.element.style.top = `${this.forcedFixedTop}px`;
|
|
790
|
+
this.element.style.left = `${this.forcedFixedLeft}px`;
|
|
791
|
+
this.element.style.right = "";
|
|
792
|
+
this.element.style.width = `${fixedWidth}px`;
|
|
793
|
+
this.ensureFixedFlowPlaceholder();
|
|
794
|
+
|
|
795
|
+
this.isForcedFixed = true;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private restoreStickyPosition(shouldRecalculate: boolean = true): void {
|
|
799
|
+
if (!this.isForcedFixed) return;
|
|
800
|
+
|
|
801
|
+
const originalStyles = this.originalInlineStyles;
|
|
802
|
+
this.element.style.position = originalStyles?.position || "";
|
|
803
|
+
this.element.style.top = originalStyles?.top || "";
|
|
804
|
+
this.element.style.left = originalStyles?.left || "";
|
|
805
|
+
this.element.style.right = originalStyles?.right || "";
|
|
806
|
+
this.element.style.width = originalStyles?.width || "";
|
|
807
|
+
this.resetFixedPositionState();
|
|
808
|
+
|
|
809
|
+
if (shouldRecalculate) {
|
|
810
|
+
this.updateStickyPosition();
|
|
811
|
+
this.initScrollSpy();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private handleModalStateChange(): void {
|
|
816
|
+
if (this.isAnyModalOpen()) {
|
|
817
|
+
// Keep sticky in normal flow until it is actually pinned.
|
|
818
|
+
if (this.isForcedFixed || this.wasStickyFloating) {
|
|
819
|
+
this.applyFixedPosition();
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
this.restoreStickyPosition(false);
|
|
825
|
+
this.updateStickyFloatingState();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
private setupModalObserver(): void {
|
|
829
|
+
this.updateStickyFloatingState();
|
|
830
|
+
this.handleModalStateChange();
|
|
831
|
+
|
|
832
|
+
if (!document.body || typeof MutationObserver === "undefined") {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
this.modalClassObserver = new MutationObserver(this.modalStateHandler);
|
|
837
|
+
this.modalClassObserver.observe(document.body, {
|
|
838
|
+
attributes: true,
|
|
839
|
+
attributeFilter: ["class"],
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private teardownModalObserver(): void {
|
|
844
|
+
if (!this.modalClassObserver) return;
|
|
845
|
+
|
|
846
|
+
this.modalClassObserver.disconnect();
|
|
847
|
+
this.modalClassObserver = null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private teardownMegamenuObserver(): void {
|
|
851
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
|
852
|
+
|
|
853
|
+
if (this.resizeObserver) {
|
|
854
|
+
this.resizeObserver.disconnect();
|
|
855
|
+
this.resizeObserver = null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private clearAutoScrollTimers(): void {
|
|
860
|
+
if (this.scrollTimeout) {
|
|
861
|
+
clearTimeout(this.scrollTimeout);
|
|
862
|
+
this.scrollTimeout = null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private resetFixedPositionState(): void {
|
|
867
|
+
this.originalInlineStyles = null;
|
|
868
|
+
this.isForcedFixed = false;
|
|
869
|
+
this.wasStickyFloating = false;
|
|
870
|
+
this.forcedFixedTop = 0;
|
|
871
|
+
this.forcedFixedLeft = 0;
|
|
872
|
+
this.removeFixedFlowPlaceholder();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private resetNavigationState(): void {
|
|
876
|
+
this.megamenuElement = null;
|
|
877
|
+
this.lastStickyOffset = 0;
|
|
878
|
+
this.navLinks = null;
|
|
879
|
+
this.sections = [];
|
|
880
|
+
this.lastActiveIndex = 0;
|
|
881
|
+
this.autoScrollTargetId = null;
|
|
882
|
+
this.autoScrollCorrectionCount = 0;
|
|
883
|
+
this.resetFixedPositionState();
|
|
884
|
+
}
|
|
885
|
+
|
|
567
886
|
private handleScrollSpy(): void {
|
|
568
887
|
if (this.isAutoScrolling) {
|
|
569
888
|
// Clear existing timeout and set a new one
|
|
@@ -573,9 +892,7 @@ export default class AnchorNavigation {
|
|
|
573
892
|
|
|
574
893
|
// Set a timeout to detect when scrolling has ended
|
|
575
894
|
this.scrollTimeout = setTimeout(() => {
|
|
576
|
-
this.
|
|
577
|
-
this.scrollTimeout = null;
|
|
578
|
-
this.initScrollSpy();
|
|
895
|
+
this.handleScrollEnd();
|
|
579
896
|
}, AnchorNavigation.SCROLL_END_DEBOUNCE_MS);
|
|
580
897
|
} else {
|
|
581
898
|
this.initScrollSpy();
|
|
@@ -587,7 +904,12 @@ export default class AnchorNavigation {
|
|
|
587
904
|
clearTimeout(this.scrollTimeout);
|
|
588
905
|
this.scrollTimeout = null;
|
|
589
906
|
}
|
|
590
|
-
|
|
907
|
+
|
|
908
|
+
if (!this.tryCorrectAutoScrollAlignment()) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
this.resetAutoScrollState();
|
|
591
913
|
this.initScrollSpy();
|
|
592
914
|
}
|
|
593
915
|
|
|
@@ -595,50 +917,36 @@ export default class AnchorNavigation {
|
|
|
595
917
|
this.setupMegamenuObserver();
|
|
596
918
|
this.setupScrollSpy();
|
|
597
919
|
this.setupDragScroll();
|
|
920
|
+
this.setupModalObserver();
|
|
598
921
|
}
|
|
599
922
|
|
|
600
923
|
destroy(): void {
|
|
601
|
-
|
|
602
|
-
clearTimeout(this.scrollTimeout);
|
|
603
|
-
this.scrollTimeout = null;
|
|
604
|
-
}
|
|
924
|
+
this.clearAutoScrollTimers();
|
|
605
925
|
|
|
606
|
-
|
|
926
|
+
this.teardownMegamenuObserver();
|
|
607
927
|
this.teardownScrollSpyListeners();
|
|
608
928
|
this.teardownDragScroll();
|
|
609
929
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
930
|
+
this.teardownModalObserver();
|
|
931
|
+
|
|
932
|
+
this.restoreStickyPosition(false);
|
|
614
933
|
|
|
615
934
|
this.element.style.top = "";
|
|
616
|
-
this.
|
|
617
|
-
this.navLinks = null;
|
|
618
|
-
this.sections = [];
|
|
619
|
-
this.lastActiveIndex = 0;
|
|
935
|
+
this.resetNavigationState();
|
|
620
936
|
(this.element as any).ODS_AnchorNavigation = null;
|
|
621
937
|
}
|
|
622
938
|
|
|
623
939
|
update(): void {
|
|
624
|
-
|
|
625
|
-
clearTimeout(this.scrollTimeout);
|
|
626
|
-
this.scrollTimeout = null;
|
|
627
|
-
}
|
|
940
|
+
this.clearAutoScrollTimers();
|
|
628
941
|
|
|
629
|
-
|
|
942
|
+
this.teardownMegamenuObserver();
|
|
630
943
|
this.teardownScrollSpyListeners();
|
|
631
944
|
this.teardownDragScroll();
|
|
632
945
|
|
|
633
|
-
|
|
634
|
-
this.resizeObserver.disconnect();
|
|
635
|
-
this.resizeObserver = null;
|
|
636
|
-
}
|
|
946
|
+
this.teardownModalObserver();
|
|
637
947
|
|
|
638
|
-
this.
|
|
639
|
-
this.
|
|
640
|
-
this.sections = [];
|
|
641
|
-
this.lastActiveIndex = 0;
|
|
948
|
+
this.restoreStickyPosition(false);
|
|
949
|
+
this.resetNavigationState();
|
|
642
950
|
|
|
643
951
|
this.init();
|
|
644
952
|
}
|