@orangesk/orange-design-system 2.0.0-beta.40 → 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 +15 -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 +178 -29
  25. package/src/components/AnchorNavigation/styles/mixins.scss +12 -3
  26. package/src/components/AnchorNavigation/tests/AnchorNavigation.unit.test.jsx +262 -14
  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.40",
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;
@@ -16,6 +18,7 @@ export default class AnchorNavigation {
16
18
  private scrollEndHandler: () => void;
17
19
  private scrollEndFallbackHandler: () => void;
18
20
  private resizeHandler: () => void;
21
+ private mouseDownHandler: (event: MouseEvent) => void;
19
22
  private dragStartHandler: (event: MouseEvent) => void;
20
23
  private dragMoveHandler: (event: MouseEvent) => void;
21
24
  private dragEndHandler: () => void;
@@ -33,6 +36,9 @@ export default class AnchorNavigation {
33
36
  private sections: HTMLElement[] = [];
34
37
  private currentPath: string;
35
38
  private lastActiveIndex: number = 0;
39
+ private autoScrollTargetId: string | null = null;
40
+ private autoScrollCorrectionCount: number = 0;
41
+ private lastStickyOffset: number = 0;
36
42
 
37
43
  constructor(element: HTMLElement) {
38
44
  this.element = element;
@@ -50,6 +56,7 @@ export default class AnchorNavigation {
50
56
  this.scrollEndHandler = this.handleScrollEnd.bind(this);
51
57
  this.scrollEndFallbackHandler = this.handleScrollEndFallback.bind(this);
52
58
  this.resizeHandler = this.handleResize.bind(this);
59
+ this.mouseDownHandler = this.handleMouseDown.bind(this);
53
60
  this.dragStartHandler = this.handleDragStart.bind(this);
54
61
  this.dragMoveHandler = this.handleDragMove.bind(this);
55
62
  this.dragEndHandler = this.handleDragEnd.bind(this);
@@ -76,13 +83,35 @@ export default class AnchorNavigation {
76
83
 
77
84
  private updateStickyPosition(): void {
78
85
  if (!this.megamenuElement) return;
79
- 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");
80
108
  }
81
109
 
82
110
  private setupMegamenuObserver(): void {
83
111
  this.megamenuElement = this.findMegamenuElement();
84
112
 
85
113
  if (!this.megamenuElement) {
114
+ this.lastStickyOffset = 0;
86
115
  this.element.style.top = "0px";
87
116
  return;
88
117
  }
@@ -116,6 +145,7 @@ export default class AnchorNavigation {
116
145
  this.teardownScrollSpyListeners();
117
146
 
118
147
  this.element.addEventListener("click", this.anchorClickHandler);
148
+ this.element.addEventListener("mousedown", this.mouseDownHandler);
119
149
  window.addEventListener("scroll", this.scrollSpyHandler, { passive: true });
120
150
 
121
151
  if (this.supportsScrollEnd) {
@@ -129,10 +159,12 @@ export default class AnchorNavigation {
129
159
  window.addEventListener("resize", this.resizeHandler);
130
160
 
131
161
  this.initScrollSpy();
162
+ this.alignInitialHashIfNeeded();
132
163
  }
133
164
 
134
165
  private teardownScrollSpyListeners(): void {
135
166
  this.element.removeEventListener("click", this.anchorClickHandler);
167
+ this.element.removeEventListener("mousedown", this.mouseDownHandler);
136
168
  window.removeEventListener("scroll", this.scrollSpyHandler);
137
169
  window.removeEventListener("resize", this.resizeHandler);
138
170
 
@@ -148,20 +180,35 @@ export default class AnchorNavigation {
148
180
  }
149
181
  }
150
182
 
151
- private handleAnchorClick(event: Event): void {
183
+ private handleMouseDown(event: MouseEvent): void {
184
+ if (event.button !== 0) return;
185
+
152
186
  const target = event.target as HTMLElement | null;
153
- const anchor = target?.closest(
154
- ".anchor-navigation__item",
155
- ) as HTMLAnchorElement | null;
187
+ if (!target || !this.element.contains(target)) return;
156
188
 
157
- if (!anchor || !this.element.contains(anchor)) return;
189
+ const interactiveSelector =
190
+ "a, button, input, select, textarea, label, [role='button'], [contenteditable='true']";
191
+ if (target.closest(interactiveSelector)) return;
158
192
 
193
+ // Prevent native text-selection drag from triggering page autoscroll.
194
+ event.preventDefault();
195
+ }
196
+
197
+ private handleAnchorClick(event: Event): void {
159
198
  if (this.suppressClick) {
160
199
  event.preventDefault();
200
+ event.stopPropagation();
161
201
  this.suppressClick = false;
162
202
  return;
163
203
  }
164
204
 
205
+ const target = event.target as HTMLElement | null;
206
+ const anchor = target?.closest(
207
+ ".anchor-navigation__item",
208
+ ) as HTMLAnchorElement | null;
209
+
210
+ if (!anchor || !this.element.contains(anchor)) return;
211
+
165
212
  event.preventDefault();
166
213
 
167
214
  const href = anchor.getAttribute("href");
@@ -171,27 +218,108 @@ export default class AnchorNavigation {
171
218
  const targetElement = document.getElementById(targetId);
172
219
  if (!targetElement) return;
173
220
 
174
- this.isAutoScrolling = true;
221
+ this.startAutoScroll(targetId);
175
222
  anchor.blur();
176
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 {
177
283
  const scrollOffset = this.megamenuElement
178
284
  ? this.megamenuElement.offsetHeight
179
285
  : 0;
180
- const additionalOffset = this.element.offsetHeight;
286
+ const anchorNavOffset = this.element.offsetHeight;
287
+ return scrollOffset + anchorNavOffset;
288
+ }
289
+
290
+ private getTargetTop(targetElement: HTMLElement): number {
181
291
  const targetTop =
182
292
  targetElement.getBoundingClientRect().top +
183
293
  window.scrollY -
184
- scrollOffset -
185
- additionalOffset;
294
+ this.getTotalStickyOffset();
295
+ return Math.max(0, targetTop);
296
+ }
186
297
 
298
+ private scrollToSection(
299
+ targetElement: HTMLElement,
300
+ behavior: ScrollBehavior,
301
+ ): void {
187
302
  window.scrollTo({
188
- top: Math.max(0, targetTop),
189
- behavior: "smooth",
303
+ top: this.getTargetTop(targetElement),
304
+ behavior,
190
305
  });
306
+ }
191
307
 
192
- const nextUrl = `${window.location.pathname}${window.location.search}#${targetId}`;
193
- window.history.pushState(null, "", nextUrl);
194
- 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
+ });
195
323
  }
196
324
 
197
325
  private initScrollSpy(forcedSectionId: string | null = null): void {
@@ -300,18 +428,30 @@ export default class AnchorNavigation {
300
428
  activeLink: HTMLElement,
301
429
  forceCenter: boolean = false,
302
430
  ): void {
303
- const itemLeft = activeLink.offsetLeft;
304
- const itemRight = itemLeft + activeLink.clientWidth;
305
- const viewportLeft = contentLeft.scrollLeft;
306
- const viewportRight = viewportLeft + contentLeft.clientWidth;
307
- const isVisible = itemLeft >= viewportLeft && itemRight <= viewportRight;
308
-
309
- if (!forceCenter && isVisible) return;
310
-
431
+ const maxScrollLeft = Math.max(
432
+ 0,
433
+ contentLeft.scrollWidth - contentLeft.clientWidth,
434
+ );
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;
311
442
  const targetScrollLeft =
312
- itemLeft - contentLeft.clientWidth / 2 + activeLink.clientWidth / 2;
443
+ itemCenterWithinContent - contentLeft.clientWidth / 2;
444
+
313
445
  const behavior = window.innerWidth < 768 ? "auto" : "smooth";
314
- const nextScrollLeft = Math.max(0, targetScrollLeft);
446
+ const nextScrollLeft = Math.min(
447
+ maxScrollLeft,
448
+ Math.max(0, targetScrollLeft),
449
+ );
450
+
451
+ const isAlreadyAligned =
452
+ Math.abs(contentLeft.scrollLeft - nextScrollLeft) <=
453
+ AnchorNavigation.SCROLL_ALIGNMENT_TOLERANCE;
454
+ if (!forceCenter && isAlreadyAligned) return;
315
455
 
316
456
  if (typeof contentLeft.scrollTo === "function") {
317
457
  contentLeft.scrollTo({
@@ -525,9 +665,7 @@ export default class AnchorNavigation {
525
665
 
526
666
  // Set a timeout to detect when scrolling has ended
527
667
  this.scrollTimeout = setTimeout(() => {
528
- this.isAutoScrolling = false;
529
- this.scrollTimeout = null;
530
- this.initScrollSpy();
668
+ this.handleScrollEnd();
531
669
  }, AnchorNavigation.SCROLL_END_DEBOUNCE_MS);
532
670
  } else {
533
671
  this.initScrollSpy();
@@ -539,7 +677,12 @@ export default class AnchorNavigation {
539
677
  clearTimeout(this.scrollTimeout);
540
678
  this.scrollTimeout = null;
541
679
  }
542
- this.isAutoScrolling = false;
680
+
681
+ if (!this.tryCorrectAutoScrollAlignment()) {
682
+ return;
683
+ }
684
+
685
+ this.resetAutoScrollState();
543
686
  this.initScrollSpy();
544
687
  }
545
688
 
@@ -566,9 +709,12 @@ export default class AnchorNavigation {
566
709
 
567
710
  this.element.style.top = "";
568
711
  this.megamenuElement = null;
712
+ this.lastStickyOffset = 0;
569
713
  this.navLinks = null;
570
714
  this.sections = [];
571
715
  this.lastActiveIndex = 0;
716
+ this.autoScrollTargetId = null;
717
+ this.autoScrollCorrectionCount = 0;
572
718
  (this.element as any).ODS_AnchorNavigation = null;
573
719
  }
574
720
 
@@ -588,9 +734,12 @@ export default class AnchorNavigation {
588
734
  }
589
735
 
590
736
  this.megamenuElement = null;
737
+ this.lastStickyOffset = 0;
591
738
  this.navLinks = null;
592
739
  this.sections = [];
593
740
  this.lastActiveIndex = 0;
741
+ this.autoScrollTargetId = null;
742
+ this.autoScrollCorrectionCount = 0;
594
743
 
595
744
  this.init();
596
745
  }
@@ -44,19 +44,27 @@
44
44
  }
45
45
 
46
46
  @include breakpoint.get("sm", "down") {
47
- margin-right: space.get("medium") !important;
47
+ margin-right: convert.to-rem(10px) !important;
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() {
@@ -134,6 +142,7 @@
134
142
  display: flex;
135
143
  flex: 1 1 auto;
136
144
  min-width: 0;
145
+ scroll-snap-type: none !important;
137
146
 
138
147
  &.is-draggable {
139
148
  user-select: none;