@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.
Files changed (51) hide show
  1. package/build/components/AnchorNavigation/style.css +1 -1
  2. package/build/components/AnchorNavigation/style.css.map +1 -1
  3. package/build/components/BlockAction/style.css +1 -1
  4. package/build/components/BlockAction/style.css.map +1 -1
  5. package/build/components/Buttons/style.css +1 -1
  6. package/build/components/Buttons/style.css.map +1 -1
  7. package/build/components/Carousel/style.css +1 -1
  8. package/build/components/Carousel/style.css.map +1 -1
  9. package/build/components/Grid/style.css +1 -1
  10. package/build/components/Grid/style.css.map +1 -1
  11. package/build/components/Link/style.css +1 -1
  12. package/build/components/Link/style.css.map +1 -1
  13. package/build/components/Megamenu/style.css +1 -1
  14. package/build/components/Megamenu/style.css.map +1 -1
  15. package/build/components/Modal/style.css +1 -1
  16. package/build/components/Modal/style.css.map +1 -1
  17. package/build/components/index.js +1 -1
  18. package/build/components/index.js.map +1 -1
  19. package/build/components/tsconfig.tsbuildinfo +1 -1
  20. package/build/components/types/src/components/AnchorNavigation/AnchorNavigation.static.d.ts +40 -0
  21. package/build/components/types/src/components/Carousel/Carousel.static.d.ts +0 -5
  22. package/build/components/types/src/components/Megamenu/Megamenu.static.d.ts +3 -0
  23. package/build/components/types/src/components/Modal/Modal.static.d.ts +1 -0
  24. package/build/lib/components.css +1 -1
  25. package/build/lib/components.css.map +1 -1
  26. package/build/lib/megamenu.css +1 -1
  27. package/build/lib/megamenu.css.map +1 -1
  28. package/build/lib/megamenu.js +1 -1
  29. package/build/lib/megamenu.js.map +1 -1
  30. package/build/lib/scripts.js +1 -1
  31. package/build/lib/scripts.js.map +1 -1
  32. package/build/lib/style.css +1 -1
  33. package/build/lib/style.css.map +1 -1
  34. package/build/lib/tsconfig.tsbuildinfo +1 -1
  35. package/build/sprite.svg +1 -1
  36. package/package.json +23 -23
  37. package/src/assets/icons/youtube.svg +3 -1
  38. package/src/components/AnchorNavigation/AnchorNavigation.static.ts +385 -77
  39. package/src/components/AnchorNavigation/styles/mixins.scss +18 -2
  40. package/src/components/AnchorNavigation/tests/AnchorNavigation.unit.test.jsx +266 -8
  41. package/src/components/BlockAction/styles/mixins.scss +1 -1
  42. package/src/components/Buttons/styles/mixins.scss +8 -13
  43. package/src/components/Carousel/Carousel.static.ts +5 -26
  44. package/src/components/Carousel/styles/mixins.scss +1 -1
  45. package/src/components/Carousel/tests/Carousel.static.test.jsx +117 -0
  46. package/src/components/Grid/styles/mixins.scss +14 -6
  47. package/src/components/Link/styles/mixins.scss +0 -1
  48. package/src/components/Megamenu/Megamenu.static.ts +27 -1
  49. package/src/components/Megamenu/styles/mixins.scss +4 -0
  50. package/src/components/Modal/Modal.static.ts +29 -7
  51. 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.41",
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.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.3.3",
67
- "html-react-parser": "^5.2.17",
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.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.4",
76
- "react-dom": "^19.2.4",
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.4",
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.1",
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.5.0",
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.2",
113
- "@vitest/browser-playwright": "^4.1.2",
114
- "@vitest/coverage-v8": "^4.1.2",
115
- "@vitest/ui": "^4.1.2",
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.11.2",
119
+ "html-validate": "10.13.1",
120
120
  "husky": "^9.1.7",
121
121
  "identity-obj-proxy": "^3.0.0",
122
- "jsdom": "29.0.1",
122
+ "jsdom": "29.0.2",
123
123
  "lint-staged": "16.4.0",
124
- "playwright": "^1.59.0",
125
- "prettier": "^3.8.1",
126
- "rollup": "^4.60.1",
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.98.0",
130
+ "sass": "^1.99.0",
131
131
  "svg-sprite": "^2.0.4",
132
- "typescript": "6.0.2",
133
- "vitest": "^4.1.2",
132
+ "typescript": "6.0.3",
133
+ "vitest": "^4.1.4",
134
134
  "vitest-axe": "^0.1.0",
135
- "vitest-browser-react": "^2.1.0"
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 width="80" height="80" viewbox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"><path 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" /></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
- this.element.style.top = `${this.megamenuElement.offsetHeight}px`;
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.isAutoScrolling = true;
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 additionalOffset = this.element.offsetHeight;
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
- scrollOffset -
204
- additionalOffset;
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: Math.max(0, targetTop),
208
- behavior: "smooth",
348
+ top: this.getTargetTop(targetElement),
349
+ behavior,
209
350
  });
351
+ }
210
352
 
211
- const nextUrl = `${window.location.pathname}${window.location.search}#${targetId}`;
212
- window.history.pushState(null, "", nextUrl);
213
- this.initScrollSpy(targetId);
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
- // Calculate scroll offset from megamenu height and anchor nav height
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 navLinks = this.navLinks;
327
- let firstNavLink: HTMLAnchorElement | null = null;
328
- let lastNavLink: HTMLAnchorElement | null = null;
329
-
330
- if (navLinks && navLinks.length > 0) {
331
- firstNavLink = navLinks[0];
332
- lastNavLink = navLinks[navLinks.length - 1];
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.isAutoScrolling = false;
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
- this.isAutoScrolling = false;
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
- if (this.scrollTimeout) {
602
- clearTimeout(this.scrollTimeout);
603
- this.scrollTimeout = null;
604
- }
924
+ this.clearAutoScrollTimers();
605
925
 
606
- window.removeEventListener("scroll", this.scrollHandler);
926
+ this.teardownMegamenuObserver();
607
927
  this.teardownScrollSpyListeners();
608
928
  this.teardownDragScroll();
609
929
 
610
- if (this.resizeObserver) {
611
- this.resizeObserver.disconnect();
612
- this.resizeObserver = null;
613
- }
930
+ this.teardownModalObserver();
931
+
932
+ this.restoreStickyPosition(false);
614
933
 
615
934
  this.element.style.top = "";
616
- this.megamenuElement = null;
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
- if (this.scrollTimeout) {
625
- clearTimeout(this.scrollTimeout);
626
- this.scrollTimeout = null;
627
- }
940
+ this.clearAutoScrollTimers();
628
941
 
629
- window.removeEventListener("scroll", this.scrollHandler);
942
+ this.teardownMegamenuObserver();
630
943
  this.teardownScrollSpyListeners();
631
944
  this.teardownDragScroll();
632
945
 
633
- if (this.resizeObserver) {
634
- this.resizeObserver.disconnect();
635
- this.resizeObserver = null;
636
- }
946
+ this.teardownModalObserver();
637
947
 
638
- this.megamenuElement = null;
639
- this.navLinks = null;
640
- this.sections = [];
641
- this.lastActiveIndex = 0;
948
+ this.restoreStickyPosition(false);
949
+ this.resetNavigationState();
642
950
 
643
951
  this.init();
644
952
  }