@orangesk/orange-design-system 2.0.0-beta.41 → 2.0.0-beta.42

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 (30) 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/Buttons/style.css +1 -1
  4. package/build/components/Buttons/style.css.map +1 -1
  5. package/build/components/Grid/style.css +1 -1
  6. package/build/components/Grid/style.css.map +1 -1
  7. package/build/components/Link/style.css +1 -1
  8. package/build/components/Link/style.css.map +1 -1
  9. package/build/components/Modal/style.css +1 -1
  10. package/build/components/Modal/style.css.map +1 -1
  11. package/build/components/index.js +1 -1
  12. package/build/components/index.js.map +1 -1
  13. package/build/components/tsconfig.tsbuildinfo +1 -1
  14. package/build/components/types/src/components/AnchorNavigation/AnchorNavigation.static.d.ts +13 -0
  15. package/build/lib/components.css +1 -1
  16. package/build/lib/components.css.map +1 -1
  17. package/build/lib/scripts.js +1 -1
  18. package/build/lib/scripts.js.map +1 -1
  19. package/build/lib/style.css +1 -1
  20. package/build/lib/style.css.map +1 -1
  21. package/build/sprite.svg +1 -1
  22. package/package.json +20 -20
  23. package/src/assets/icons/youtube.svg +3 -1
  24. package/src/components/AnchorNavigation/AnchorNavigation.static.ts +146 -45
  25. package/src/components/AnchorNavigation/styles/mixins.scss +10 -2
  26. package/src/components/AnchorNavigation/tests/AnchorNavigation.unit.test.jsx +194 -8
  27. package/src/components/Buttons/styles/mixins.scss +8 -13
  28. package/src/components/Grid/styles/mixins.scss +14 -6
  29. package/src/components/Link/styles/mixins.scss +0 -1
  30. 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.42",
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.3",
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.3",
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",
@@ -104,35 +104,35 @@
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.12.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",
124
+ "playwright": "^1.59.1",
125
+ "prettier": "^3.8.2",
126
126
  "rollup": "^4.60.1",
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
132
  "typescript": "6.0.2",
133
- "vitest": "^4.1.2",
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>
@@ -4,6 +4,8 @@ export default class AnchorNavigation {
4
4
  private static readonly DRAG_START_THRESHOLD = 6;
5
5
  private static readonly SCROLL_END_DEBOUNCE_MS = 150;
6
6
  private static readonly SCROLL_EDGE_TOLERANCE = 1;
7
+ private static readonly SCROLL_ALIGNMENT_TOLERANCE = 1;
8
+ private static readonly MAX_SCROLL_CORRECTIONS = 2;
7
9
 
8
10
  private element: HTMLElement;
9
11
  private contentLeftElement: HTMLElement | null;
@@ -34,6 +36,9 @@ export default class AnchorNavigation {
34
36
  private sections: HTMLElement[] = [];
35
37
  private currentPath: string;
36
38
  private lastActiveIndex: number = 0;
39
+ private autoScrollTargetId: string | null = null;
40
+ private autoScrollCorrectionCount: number = 0;
41
+ private lastStickyOffset: number = 0;
37
42
 
38
43
  constructor(element: HTMLElement) {
39
44
  this.element = element;
@@ -78,13 +83,35 @@ export default class AnchorNavigation {
78
83
 
79
84
  private updateStickyPosition(): void {
80
85
  if (!this.megamenuElement) return;
81
- this.element.style.top = `${this.megamenuElement.offsetHeight}px`;
86
+
87
+ const stickyOffset = this.megamenuElement.offsetHeight;
88
+ const hasStickyOffsetChanged = stickyOffset !== this.lastStickyOffset;
89
+
90
+ this.lastStickyOffset = stickyOffset;
91
+ this.element.style.top = `${stickyOffset}px`;
92
+
93
+ if (
94
+ !hasStickyOffsetChanged ||
95
+ !this.isAutoScrolling ||
96
+ !this.autoScrollTargetId ||
97
+ this.autoScrollCorrectionCount >= AnchorNavigation.MAX_SCROLL_CORRECTIONS
98
+ ) {
99
+ return;
100
+ }
101
+
102
+ const targetElement = document.getElementById(this.autoScrollTargetId);
103
+ if (!targetElement) return;
104
+
105
+ // Re-target smoothly as soon as sticky offset changes to avoid a visible snap at scroll end.
106
+ this.autoScrollCorrectionCount += 1;
107
+ this.scrollToSection(targetElement, "smooth");
82
108
  }
83
109
 
84
110
  private setupMegamenuObserver(): void {
85
111
  this.megamenuElement = this.findMegamenuElement();
86
112
 
87
113
  if (!this.megamenuElement) {
114
+ this.lastStickyOffset = 0;
88
115
  this.element.style.top = "0px";
89
116
  return;
90
117
  }
@@ -132,6 +159,7 @@ export default class AnchorNavigation {
132
159
  window.addEventListener("resize", this.resizeHandler);
133
160
 
134
161
  this.initScrollSpy();
162
+ this.alignInitialHashIfNeeded();
135
163
  }
136
164
 
137
165
  private teardownScrollSpyListeners(): void {
@@ -190,27 +218,108 @@ export default class AnchorNavigation {
190
218
  const targetElement = document.getElementById(targetId);
191
219
  if (!targetElement) return;
192
220
 
193
- this.isAutoScrolling = true;
221
+ this.startAutoScroll(targetId);
194
222
  anchor.blur();
195
223
 
224
+ this.scrollToSection(targetElement, "smooth");
225
+
226
+ const nextUrl = `${window.location.pathname}${window.location.search}#${targetId}`;
227
+ window.history.pushState(null, "", nextUrl);
228
+ this.initScrollSpy(targetId);
229
+ }
230
+
231
+ private getHashSectionId(): string | null {
232
+ const hash = window.location.hash;
233
+ if (!hash || hash.length <= 1) return null;
234
+
235
+ let hashId = hash.slice(1);
236
+ try {
237
+ hashId = decodeURIComponent(hashId);
238
+ } catch {
239
+ // Keep raw hash when decoding fails.
240
+ }
241
+
242
+ return hashId || null;
243
+ }
244
+
245
+ private startAutoScroll(sectionId: string): void {
246
+ this.isAutoScrolling = true;
247
+ this.autoScrollTargetId = sectionId;
248
+ this.autoScrollCorrectionCount = 0;
249
+ }
250
+
251
+ private resetAutoScrollState(): void {
252
+ this.isAutoScrolling = false;
253
+ this.autoScrollTargetId = null;
254
+ this.autoScrollCorrectionCount = 0;
255
+ }
256
+
257
+ private tryCorrectAutoScrollAlignment(): boolean {
258
+ if (!this.autoScrollTargetId) return true;
259
+
260
+ const targetElement = document.getElementById(this.autoScrollTargetId);
261
+ if (!targetElement) return true;
262
+
263
+ const targetTop = this.getTargetTop(targetElement);
264
+ const distance = Math.abs(window.scrollY - targetTop);
265
+ if (distance <= AnchorNavigation.SCROLL_ALIGNMENT_TOLERANCE) {
266
+ return true;
267
+ }
268
+
269
+ if (
270
+ this.autoScrollCorrectionCount >= AnchorNavigation.MAX_SCROLL_CORRECTIONS
271
+ ) {
272
+ return true;
273
+ }
274
+
275
+ this.autoScrollCorrectionCount += 1;
276
+ const correctionBehavior: ScrollBehavior =
277
+ this.autoScrollCorrectionCount === 1 ? "smooth" : "auto";
278
+ this.scrollToSection(targetElement, correctionBehavior);
279
+ return false;
280
+ }
281
+
282
+ private getTotalStickyOffset(): number {
196
283
  const scrollOffset = this.megamenuElement
197
284
  ? this.megamenuElement.offsetHeight
198
285
  : 0;
199
- const additionalOffset = this.element.offsetHeight;
286
+ const anchorNavOffset = this.element.offsetHeight;
287
+ return scrollOffset + anchorNavOffset;
288
+ }
289
+
290
+ private getTargetTop(targetElement: HTMLElement): number {
200
291
  const targetTop =
201
292
  targetElement.getBoundingClientRect().top +
202
293
  window.scrollY -
203
- scrollOffset -
204
- additionalOffset;
294
+ this.getTotalStickyOffset();
295
+ return Math.max(0, targetTop);
296
+ }
205
297
 
298
+ private scrollToSection(
299
+ targetElement: HTMLElement,
300
+ behavior: ScrollBehavior,
301
+ ): void {
206
302
  window.scrollTo({
207
- top: Math.max(0, targetTop),
208
- behavior: "smooth",
303
+ top: this.getTargetTop(targetElement),
304
+ behavior,
209
305
  });
306
+ }
210
307
 
211
- const nextUrl = `${window.location.pathname}${window.location.search}#${targetId}`;
212
- window.history.pushState(null, "", nextUrl);
213
- this.initScrollSpy(targetId);
308
+ private alignInitialHashIfNeeded(): void {
309
+ const hashId = this.getHashSectionId();
310
+ if (!hashId) return;
311
+
312
+ const targetElement = document.getElementById(hashId);
313
+ if (!targetElement) return;
314
+
315
+ this.startAutoScroll(hashId);
316
+
317
+ requestAnimationFrame(() => {
318
+ this.scrollToSection(targetElement, "auto");
319
+ requestAnimationFrame(() => {
320
+ this.handleScrollEnd();
321
+ });
322
+ });
214
323
  }
215
324
 
216
325
  private initScrollSpy(forcedSectionId: string | null = null): void {
@@ -323,37 +432,15 @@ export default class AnchorNavigation {
323
432
  0,
324
433
  contentLeft.scrollWidth - contentLeft.clientWidth,
325
434
  );
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
- }
435
+ const contentRect = contentLeft.getBoundingClientRect();
436
+ const itemRect = activeLink.getBoundingClientRect();
437
+ const itemCenterWithinContent =
438
+ itemRect.left -
439
+ contentRect.left +
440
+ contentLeft.scrollLeft +
441
+ itemRect.width / 2;
442
+ const targetScrollLeft =
443
+ itemCenterWithinContent - contentLeft.clientWidth / 2;
357
444
 
358
445
  const behavior = window.innerWidth < 768 ? "auto" : "smooth";
359
446
  const nextScrollLeft = Math.min(
@@ -361,6 +448,11 @@ export default class AnchorNavigation {
361
448
  Math.max(0, targetScrollLeft),
362
449
  );
363
450
 
451
+ const isAlreadyAligned =
452
+ Math.abs(contentLeft.scrollLeft - nextScrollLeft) <=
453
+ AnchorNavigation.SCROLL_ALIGNMENT_TOLERANCE;
454
+ if (!forceCenter && isAlreadyAligned) return;
455
+
364
456
  if (typeof contentLeft.scrollTo === "function") {
365
457
  contentLeft.scrollTo({
366
458
  left: nextScrollLeft,
@@ -573,9 +665,7 @@ export default class AnchorNavigation {
573
665
 
574
666
  // Set a timeout to detect when scrolling has ended
575
667
  this.scrollTimeout = setTimeout(() => {
576
- this.isAutoScrolling = false;
577
- this.scrollTimeout = null;
578
- this.initScrollSpy();
668
+ this.handleScrollEnd();
579
669
  }, AnchorNavigation.SCROLL_END_DEBOUNCE_MS);
580
670
  } else {
581
671
  this.initScrollSpy();
@@ -587,7 +677,12 @@ export default class AnchorNavigation {
587
677
  clearTimeout(this.scrollTimeout);
588
678
  this.scrollTimeout = null;
589
679
  }
590
- this.isAutoScrolling = false;
680
+
681
+ if (!this.tryCorrectAutoScrollAlignment()) {
682
+ return;
683
+ }
684
+
685
+ this.resetAutoScrollState();
591
686
  this.initScrollSpy();
592
687
  }
593
688
 
@@ -614,9 +709,12 @@ export default class AnchorNavigation {
614
709
 
615
710
  this.element.style.top = "";
616
711
  this.megamenuElement = null;
712
+ this.lastStickyOffset = 0;
617
713
  this.navLinks = null;
618
714
  this.sections = [];
619
715
  this.lastActiveIndex = 0;
716
+ this.autoScrollTargetId = null;
717
+ this.autoScrollCorrectionCount = 0;
620
718
  (this.element as any).ODS_AnchorNavigation = null;
621
719
  }
622
720
 
@@ -636,9 +734,12 @@ export default class AnchorNavigation {
636
734
  }
637
735
 
638
736
  this.megamenuElement = null;
737
+ this.lastStickyOffset = 0;
639
738
  this.navLinks = null;
640
739
  this.sections = [];
641
740
  this.lastActiveIndex = 0;
741
+ this.autoScrollTargetId = null;
742
+ this.autoScrollCorrectionCount = 0;
642
743
 
643
744
  this.init();
644
745
  }
@@ -48,15 +48,23 @@
48
48
  padding: convert.to-rem(15px) 0 !important;
49
49
  }
50
50
 
51
- &:hover,
52
51
  &:focus-visible,
53
- &:active,
54
52
  &.is-active {
55
53
  box-shadow: megamenuConfig.$active-line;
56
54
  color: inherit;
57
55
  text-decoration: none !important;
58
56
  outline: none;
59
57
  }
58
+
59
+ @media (hover: hover) and (pointer: fine) {
60
+ &:hover,
61
+ &:active {
62
+ box-shadow: megamenuConfig.$active-line;
63
+ color: inherit;
64
+ text-decoration: none !important;
65
+ outline: none;
66
+ }
67
+ }
60
68
  }
61
69
 
62
70
  @mixin anchor-navigation-content() {
@@ -362,13 +362,33 @@ describe("rendering AnchorNavigation", () => {
362
362
  value: 0,
363
363
  writable: true,
364
364
  });
365
- Object.defineProperty(activeLink, "offsetLeft", {
365
+ Object.defineProperty(contentLeft, "getBoundingClientRect", {
366
366
  configurable: true,
367
- value: 220,
367
+ value: () => ({
368
+ top: 0,
369
+ bottom: 0,
370
+ left: 0,
371
+ right: 180,
372
+ width: 180,
373
+ height: 0,
374
+ x: 0,
375
+ y: 0,
376
+ toJSON: () => ({}),
377
+ }),
368
378
  });
369
- Object.defineProperty(activeLink, "clientWidth", {
379
+ Object.defineProperty(activeLink, "getBoundingClientRect", {
370
380
  configurable: true,
371
- value: 80,
381
+ value: () => ({
382
+ top: 0,
383
+ bottom: 0,
384
+ left: 220,
385
+ right: 300,
386
+ width: 80,
387
+ height: 0,
388
+ x: 220,
389
+ y: 0,
390
+ toJSON: () => ({}),
391
+ }),
372
392
  });
373
393
  contentLeft.scrollTo = scrollToSpy;
374
394
 
@@ -412,13 +432,33 @@ describe("rendering AnchorNavigation", () => {
412
432
  value: 180,
413
433
  writable: true,
414
434
  });
415
- Object.defineProperty(activeLink, "offsetLeft", {
435
+ Object.defineProperty(contentLeft, "getBoundingClientRect", {
416
436
  configurable: true,
417
- value: 220,
437
+ value: () => ({
438
+ top: 0,
439
+ bottom: 0,
440
+ left: 0,
441
+ right: 200,
442
+ width: 200,
443
+ height: 0,
444
+ x: 0,
445
+ y: 0,
446
+ toJSON: () => ({}),
447
+ }),
418
448
  });
419
- Object.defineProperty(activeLink, "clientWidth", {
449
+ Object.defineProperty(activeLink, "getBoundingClientRect", {
420
450
  configurable: true,
421
- value: 80,
451
+ value: () => ({
452
+ top: 0,
453
+ bottom: 0,
454
+ left: 40,
455
+ right: 120,
456
+ width: 80,
457
+ height: 0,
458
+ x: 40,
459
+ y: 0,
460
+ toJSON: () => ({}),
461
+ }),
422
462
  });
423
463
  contentLeft.scrollTo = scrollToSpy;
424
464
 
@@ -433,6 +473,71 @@ describe("rendering AnchorNavigation", () => {
433
473
  section.remove();
434
474
  });
435
475
 
476
+ it("centers scroll-spy active item even when it is already visible", () => {
477
+ const { container } = render(<AnchorNavigation items={basicItems} />);
478
+ const anchorNavigationElement = initializeAnchorNavigation(container);
479
+ const anchorNavigation = AnchorNavigationStatic.getInstance(
480
+ anchorNavigationElement,
481
+ );
482
+ const contentLeft = container.querySelector(
483
+ ".anchor-navigation__content-left",
484
+ );
485
+ const activeLink = container.querySelector('a[href="#pricing"]');
486
+ const scrollToSpy = vi.fn();
487
+
488
+ Object.defineProperty(contentLeft, "clientWidth", {
489
+ configurable: true,
490
+ value: 200,
491
+ });
492
+ Object.defineProperty(contentLeft, "scrollWidth", {
493
+ configurable: true,
494
+ value: 600,
495
+ });
496
+ Object.defineProperty(contentLeft, "scrollLeft", {
497
+ configurable: true,
498
+ value: 180,
499
+ writable: true,
500
+ });
501
+ Object.defineProperty(contentLeft, "getBoundingClientRect", {
502
+ configurable: true,
503
+ value: () => ({
504
+ top: 0,
505
+ bottom: 0,
506
+ left: 0,
507
+ right: 200,
508
+ width: 200,
509
+ height: 0,
510
+ x: 0,
511
+ y: 0,
512
+ toJSON: () => ({}),
513
+ }),
514
+ });
515
+ Object.defineProperty(activeLink, "getBoundingClientRect", {
516
+ configurable: true,
517
+ value: () => ({
518
+ top: 0,
519
+ bottom: 0,
520
+ left: 40,
521
+ right: 120,
522
+ width: 80,
523
+ height: 0,
524
+ x: 40,
525
+ y: 0,
526
+ toJSON: () => ({}),
527
+ }),
528
+ });
529
+ contentLeft.scrollTo = scrollToSpy;
530
+
531
+ anchorNavigation.scrollActiveLinkIntoView(contentLeft, activeLink, false);
532
+
533
+ expect(scrollToSpy).toHaveBeenCalledWith({
534
+ left: 160,
535
+ behavior: "smooth",
536
+ });
537
+
538
+ anchorNavigation?.destroy();
539
+ });
540
+
436
541
  it("does not duplicate anchor click handling after update", () => {
437
542
  const section = document.createElement("section");
438
543
  section.id = "features";
@@ -498,6 +603,87 @@ describe("rendering AnchorNavigation", () => {
498
603
  section.remove();
499
604
  });
500
605
 
606
+ it("realigns anchor after sticky offset changes during smooth scroll", () => {
607
+ vi.useFakeTimers();
608
+
609
+ const section = document.createElement("section");
610
+ section.id = "pricing";
611
+ Object.defineProperty(section, "getBoundingClientRect", {
612
+ configurable: true,
613
+ value: () => ({
614
+ top: 600 - window.scrollY,
615
+ bottom: 700 - window.scrollY,
616
+ left: 0,
617
+ right: 0,
618
+ width: 0,
619
+ height: 100,
620
+ x: 0,
621
+ y: 600,
622
+ toJSON: () => ({}),
623
+ }),
624
+ });
625
+ document.body.appendChild(section);
626
+
627
+ const megamenu = document.createElement("div");
628
+ megamenu.setAttribute("data-megamenu", "");
629
+ let megamenuHeight = 120;
630
+ Object.defineProperty(megamenu, "offsetHeight", {
631
+ configurable: true,
632
+ get: () => megamenuHeight,
633
+ });
634
+ document.body.appendChild(megamenu);
635
+
636
+ Object.defineProperty(window, "scrollY", {
637
+ configurable: true,
638
+ value: 0,
639
+ writable: true,
640
+ });
641
+
642
+ const scrollToSpy = vi
643
+ .spyOn(window, "scrollTo")
644
+ .mockImplementation((options) => {
645
+ if (typeof options === "object" && typeof options.top === "number") {
646
+ window.scrollY = options.top;
647
+ }
648
+ });
649
+
650
+ const { container } = render(<AnchorNavigation items={basicItems} />);
651
+ const anchorNavigationElement = initializeAnchorNavigation(container);
652
+ const anchorNavigation = AnchorNavigationStatic.getInstance(
653
+ anchorNavigationElement,
654
+ );
655
+ const pricingLink = container.querySelector('a[href="#pricing"]');
656
+
657
+ Object.defineProperty(anchorNavigationElement, "offsetHeight", {
658
+ configurable: true,
659
+ value: 50,
660
+ });
661
+
662
+ fireEvent.click(pricingLink);
663
+
664
+ expect(scrollToSpy).toHaveBeenNthCalledWith(1, {
665
+ top: 430,
666
+ behavior: "smooth",
667
+ });
668
+
669
+ megamenuHeight = 80;
670
+ fireEvent.scroll(window);
671
+ vi.advanceTimersByTime(
672
+ AnchorNavigationStatic.SCROLL_END_DEBOUNCE_MS + 20,
673
+ );
674
+
675
+ expect(scrollToSpy).toHaveBeenNthCalledWith(2, {
676
+ top: 470,
677
+ behavior: "smooth",
678
+ });
679
+
680
+ anchorNavigation?.destroy();
681
+ scrollToSpy.mockRestore();
682
+ megamenu.remove();
683
+ section.remove();
684
+ vi.useRealTimers();
685
+ });
686
+
501
687
  it("toggles left and right overflow classes based on scroll position", () => {
502
688
  const { container } = render(<AnchorNavigation items={moreItems} />);
503
689
  const anchorNavigationElement = initializeAnchorNavigation(container);
@@ -9,31 +9,26 @@
9
9
  ) {
10
10
  display: flex;
11
11
  flex-flow: row wrap;
12
- max-width: calc(100% - #{$spacing});
13
- margin: ($spacing * -1) ($spacing * -1) space.get("large") 0;
12
+ gap: $spacing;
13
+ margin: 0 0 space.get("large") 0;
14
14
 
15
15
  #{$button-selector},
16
- #{$button-selector}:last-child,
17
16
  #{$link-selector},
18
- #{$link-selector}:last-child,
19
- button#{$link-selector},
20
- button#{$link-selector}:last-child {
21
- margin: $spacing $spacing 0 0;
17
+ button#{$link-selector} {
18
+ margin: 0;
22
19
  }
23
20
  }
24
21
 
25
22
  @mixin stack-on-xs($spacing: config.$spacing, $button-selector: ".btn") {
26
23
  @include breakpoint.get("xs", "down") {
27
24
  flex-flow: column;
25
+ gap: $spacing;
28
26
  max-width: 100%;
29
27
  margin: 0 0 $spacing 0;
30
28
 
31
- #{$button-selector}:not(:last-child),
32
- #{$button-selector}:last-child,
33
- button.link:not(:last-child),
34
- button.link:last-child {
35
- margin-right: 0;
36
- margin-bottom: 0;
29
+ #{$button-selector},
30
+ button.link {
31
+ margin: 0;
37
32
  }
38
33
  }
39
34
  }