@orangesk/orange-design-system 3.12.0 → 4.0.0

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 (115) hide show
  1. package/build/components/Breadcrumbs/style.css +1 -1
  2. package/build/components/Breadcrumbs/style.css.map +1 -1
  3. package/build/components/Carousel/style.css +1 -1
  4. package/build/components/Carousel/style.css.map +1 -1
  5. package/build/components/Footer/style.css +1 -1
  6. package/build/components/Footer/style.css.map +1 -1
  7. package/build/components/Megamenu/style.css +1 -1
  8. package/build/components/Megamenu/style.css.map +1 -1
  9. package/build/components/index.js +1 -1
  10. package/build/components/index.js.map +1 -1
  11. package/build/lib/base.css +1 -1
  12. package/build/lib/base.css.map +1 -1
  13. package/build/lib/breadcrumbs.css +2 -2
  14. package/build/lib/breadcrumbs.css.map +1 -1
  15. package/build/lib/components.css +1 -1
  16. package/build/lib/components.css.map +1 -1
  17. package/build/lib/fonts.css +1 -1
  18. package/build/lib/fonts.css.map +1 -1
  19. package/build/lib/footer.css +1 -1
  20. package/build/lib/footer.css.map +1 -1
  21. package/build/lib/footer.iife.js +2 -0
  22. package/build/lib/footer.iife.js.map +1 -0
  23. package/build/lib/megamenu.css +1 -1
  24. package/build/lib/megamenu.css.map +1 -1
  25. package/build/lib/megamenu.iife.js +2 -0
  26. package/build/lib/megamenu.iife.js.map +1 -0
  27. package/build/lib/megamenu.js +1 -1
  28. package/build/lib/megamenu.js.map +1 -1
  29. package/build/lib/scripts.js +1 -1
  30. package/build/lib/scripts.js.map +1 -1
  31. package/build/lib/style.css +1 -1
  32. package/build/lib/style.css.map +1 -1
  33. package/package.json +10 -12
  34. package/src/components/AnchorNavigation/AnchorNavigation.static.ts +3 -4
  35. package/src/components/AnchorNavigation/tests/AnchorNavigation.megamenu.unit.test.ts +74 -0
  36. package/src/components/AnchorNavigation/tests/AnchorNavigation.unit.test.jsx +6 -0
  37. package/src/components/Breadcrumbs/styles/mixins.scss +6 -0
  38. package/src/components/Breadcrumbs/styles/rules.scss +30 -0
  39. package/src/components/Breadcrumbs/styles/style.scss +2 -26
  40. package/src/components/Carousel/Carousel.static.ts +0 -3
  41. package/src/components/Carousel/styles/mixins.scss +0 -4
  42. package/src/components/Carousel/tests/Carousel.interactions.test.ts +116 -85
  43. package/src/components/CarouselHero/CarouselHero.static.ts +0 -3
  44. package/src/components/CarouselPromotions/CarouselPromotions.static.ts +0 -3
  45. package/src/components/Footer/classic.ts +3 -0
  46. package/src/components/Footer/styles/mixins.scss +32 -1
  47. package/src/components/Footer/styles/style.scss +8 -5
  48. package/src/components/Footer/tests/Footer.styles.unit.test.jsx +7 -2
  49. package/src/components/Megamenu/Megamenu.static.ts +25 -6
  50. package/src/components/Megamenu/classic.ts +3 -0
  51. package/src/components/Megamenu/styles/mixins.scss +114 -52
  52. package/src/components/Megamenu/styles/style.scss +6 -7
  53. package/src/components/Megamenu/tests/Megamenu.scroll.unit.test.ts +185 -0
  54. package/src/styles/base/globals.scss +1 -37
  55. package/src/styles/base/header-height-values.scss +9 -0
  56. package/src/styles/base/header-height.scss +50 -0
  57. package/src/styles/base/styleguide.scss +0 -1
  58. package/build/files/tone-of-voice.pdf +0 -26572
  59. package/build/images/card-device-example.png +0 -0
  60. package/build/images/cta-device.png +0 -0
  61. package/build/images/cta-plans.png +0 -0
  62. package/build/images/cta-renewal.png +0 -0
  63. package/build/images/device-mobile.png +0 -0
  64. package/build/images/form-validation-format.png +0 -0
  65. package/build/images/form-validation-info-structure.png +0 -0
  66. package/build/images/form-validation-semantics.png +0 -0
  67. package/build/images/forms-labels-grouping.png +0 -0
  68. package/build/images/forms-labels-instruction.png +0 -0
  69. package/build/images/forms-labels-structure.png +0 -0
  70. package/build/images/hero/example-lg.jpg +0 -0
  71. package/build/images/hero/example-lg@2x.jpg +0 -0
  72. package/build/images/hero/example-xl.jpg +0 -0
  73. package/build/images/hero/example-xl@2x.jpg +0 -0
  74. package/build/images/hero/example-xxl.jpg +0 -0
  75. package/build/images/hero/example-xxl@2x.jpg +0 -0
  76. package/build/images/hero/example.jpg +0 -0
  77. package/build/images/hero/example@2x.jpg +0 -0
  78. package/build/images/hero/old-vendor-2x.jpg +0 -0
  79. package/build/images/hero/old-vendor-lg-2x.jpg +0 -0
  80. package/build/images/hero/old-vendor-lg.jpg +0 -0
  81. package/build/images/hero/old-vendor-xl-2x.jpg +0 -0
  82. package/build/images/hero/old-vendor-xl.jpg +0 -0
  83. package/build/images/hero/old-vendor-xxl-2x.jpg +0 -0
  84. package/build/images/hero/old-vendor-xxl.jpg +0 -0
  85. package/build/images/hero/old-vendor.jpg +0 -0
  86. package/build/images/hero/photo-2x.jpg +0 -0
  87. package/build/images/hero/photo-lg-2x.jpg +0 -0
  88. package/build/images/hero/photo-lg.jpg +0 -0
  89. package/build/images/hero/photo-xl-2x.jpg +0 -0
  90. package/build/images/hero/photo-xl.jpg +0 -0
  91. package/build/images/hero/photo-xxl-2x.jpg +0 -0
  92. package/build/images/hero/photo-xxl.jpg +0 -0
  93. package/build/images/hero/photo.jpg +0 -0
  94. package/build/images/hero/single.png +0 -0
  95. package/build/images/hero/single@2x.png +0 -0
  96. package/build/images/hero/vendor-lg.png +0 -0
  97. package/build/images/hero/vendor-lg@2x.png +0 -0
  98. package/build/images/hero/vendor-xl.png +0 -0
  99. package/build/images/hero/vendor-xl@2x.png +0 -0
  100. package/build/images/hero/vendor-xxl.png +0 -0
  101. package/build/images/hero/vendor-xxl@2x.png +0 -0
  102. package/build/images/hero/vendor.png +0 -0
  103. package/build/images/hero/vendor@2x.png +0 -0
  104. package/build/images/phones.png +0 -0
  105. package/build/images/product-1.svg +0 -10
  106. package/build/images/product-2.svg +0 -14
  107. package/build/images/product-3.svg +0 -9
  108. package/build/images/tone-of-voice.jpg +0 -0
  109. package/build/lib/base.js +0 -3
  110. package/build/lib/breadcrumbs.js +0 -3
  111. package/build/lib/components.js +0 -3
  112. package/build/lib/fonts.js +0 -3
  113. package/build/lib/reset.js +0 -3
  114. package/build/lib/style.js +0 -3
  115. package/build/lib/utilities.js +0 -3
@@ -21,7 +21,10 @@ describe("Footer standalone styles", () => {
21
21
  afterAll(() => stylesheet.remove());
22
22
 
23
23
  const styleFor = (selector) =>
24
- rules.find((rule) => rule.selectorText === selector)?.style;
24
+ rules.find(
25
+ (rule) =>
26
+ rule.selectorText.replaceAll(":is(:root, :host) ", "") === selector,
27
+ )?.style;
25
28
 
26
29
  it("sizes social buttons and icons without global ODS styles", () => {
27
30
  expect(styleFor(".footer .social-button")?.display).toBe("inline-flex");
@@ -54,7 +57,9 @@ describe("Footer standalone styles", () => {
54
57
  );
55
58
  expect(componentRules.length).toBeGreaterThan(0);
56
59
  for (const rule of componentRules) {
57
- for (const selector of rule.selectorText.split(",")) {
60
+ for (const selector of rule.selectorText
61
+ .replaceAll(":is(:root, :host) ", "")
62
+ .split(",")) {
58
63
  expect(selector.trim()).toMatch(/^\.footer /);
59
64
  }
60
65
  }
@@ -47,6 +47,7 @@ export default class Megamenu {
47
47
  mousedownHandler!: () => void;
48
48
  touchstartHandler!: () => void;
49
49
  instanceName!: string;
50
+ private scrollFrameId: number | null = null;
50
51
 
51
52
  private isModalOpen(): boolean {
52
53
  return document.body.classList.contains(MODAL_OPEN_CLASS);
@@ -80,6 +81,7 @@ export default class Megamenu {
80
81
  this.handleToggleClick = this.handleToggleClick.bind(this);
81
82
  this.handleKeyPress = this.handleKeyPress.bind(this);
82
83
  this.handleScroll = this.handleScroll.bind(this);
84
+ this.scheduleScrollUpdate = this.scheduleScrollUpdate.bind(this);
83
85
  this.handleAccordionClick = this.handleAccordionClick.bind(this);
84
86
  this.handleArrowNavigation = this.handleArrowNavigation.bind(this);
85
87
  this.trapFocus = this.trapFocus.bind(this);
@@ -182,7 +184,9 @@ export default class Megamenu {
182
184
 
183
185
  // Add scoped event listeners to this element
184
186
  this.element.addEventListener("keydown", this.handleKeyPress);
185
- window.addEventListener("scroll", this.handleScroll);
187
+ window.addEventListener("scroll", this.scheduleScrollUpdate, {
188
+ passive: true,
189
+ });
186
190
 
187
191
  // Track keyboard usage for proper focus management
188
192
  this.isKeyboardUser = false;
@@ -429,13 +433,21 @@ export default class Megamenu {
429
433
  }
430
434
  }
431
435
 
436
+ private scheduleScrollUpdate() {
437
+ if (this.scrollFrameId !== null) {
438
+ return;
439
+ }
440
+
441
+ this.scrollFrameId = window.requestAnimationFrame(() => {
442
+ this.scrollFrameId = null;
443
+ this.handleScroll();
444
+ });
445
+ }
446
+
432
447
  handleScroll() {
433
448
  const topElement = this.element.querySelector(
434
449
  '[data-hide-when-sticky="true"]',
435
450
  ) as HTMLElement;
436
- const hasBrandMark = Boolean(
437
- this.element.querySelector(BRAND_MARK_SELECTOR),
438
- );
439
451
  const effectiveScrollY = this.getEffectiveScrollY();
440
452
  const hideThreshold = topElement?.offsetHeight || 15;
441
453
  const shouldHideTop = effectiveScrollY > hideThreshold;
@@ -449,7 +461,10 @@ export default class Megamenu {
449
461
  topElement.classList.remove("is-hidden");
450
462
  this.element.classList.remove("top-hidden");
451
463
  }
452
- } else if (!this.element.classList.contains("non-sticky") && hasBrandMark) {
464
+ } else if (
465
+ !this.element.classList.contains("non-sticky") &&
466
+ this.element.querySelector(BRAND_MARK_SELECTOR)
467
+ ) {
453
468
  this.element.classList.toggle("top-hidden", shouldHideTop);
454
469
  }
455
470
  }
@@ -775,7 +790,11 @@ export default class Megamenu {
775
790
  });
776
791
 
777
792
  this.element.removeEventListener("keydown", this.handleKeyPress);
778
- window.removeEventListener("scroll", this.handleScroll);
793
+ window.removeEventListener("scroll", this.scheduleScrollUpdate);
794
+ if (this.scrollFrameId !== null) {
795
+ window.cancelAnimationFrame(this.scrollFrameId);
796
+ this.scrollFrameId = null;
797
+ }
779
798
 
780
799
  // Remove keyboard tracking event listeners
781
800
  this.element.removeEventListener("keydown", this.keydownHandler);
@@ -0,0 +1,3 @@
1
+ // The shared entry owns window.ODSMegamenu and auto-initialization.
2
+ // No exports: Rollup emits a classic script without creating another global.
3
+ import "./static";
@@ -18,6 +18,7 @@
18
18
  @use "../../Card/styles/mixins" as card;
19
19
  @use "../../../styles/typography/mixins" as typography;
20
20
  @use "../../../styles/tokens/overlay";
21
+ @use "../../../styles/base/header-height-values" as headerHeights;
21
22
 
22
23
  @use "./config";
23
24
 
@@ -45,6 +46,34 @@
45
46
  ul {
46
47
  @include typography.list-unstyled();
47
48
  margin-bottom: convert.to-rem(30px);
49
+ font-weight: 400;
50
+
51
+ // Match ODS body lists without depending on its global typography sheet.
52
+ @each $breakpoint, $props in map.get(typographyConfig.$body-text, "default")
53
+ {
54
+ @include breakpoint.get($breakpoint) {
55
+ font-size: map.get($props, font-size);
56
+ }
57
+ }
58
+ }
59
+
60
+ li {
61
+ font-size: inherit;
62
+ line-height: inherit;
63
+ font-weight: inherit;
64
+ }
65
+
66
+ :where(h3, strong, b) {
67
+ font-family: inherit;
68
+ }
69
+
70
+ :where(strong, b) {
71
+ font-weight: 700;
72
+ }
73
+
74
+ :where(button, input, select, textarea) {
75
+ font: inherit;
76
+ color: inherit;
48
77
  }
49
78
 
50
79
  h3 {
@@ -55,18 +84,24 @@
55
84
  color: var(--color-text-default);
56
85
  }
57
86
 
58
- a {
87
+ :where(a) {
59
88
  color: var(--color-text-default);
89
+ font-family: inherit;
60
90
  font-weight: 700;
61
91
  font-size: inherit;
62
92
  text-decoration: underline;
63
93
  text-underline-offset: convert.to-rem(2px);
64
94
  text-decoration-thickness: auto;
95
+ background-color: transparent;
96
+ box-shadow: none;
65
97
 
66
98
  &:hover,
67
99
  &:focus,
68
100
  &:active {
69
101
  color: var(--color-text-accent);
102
+ text-decoration: underline;
103
+ background-color: transparent;
104
+ box-shadow: none;
70
105
  }
71
106
 
72
107
  &:focus-visible {
@@ -86,6 +121,11 @@
86
121
  [tabindex]:not([tabindex="-1"]),
87
122
  [contentEditable="true"] {
88
123
  @include base.focusring;
124
+
125
+ &:focus-visible {
126
+ // Legacy ODS explicitly suppresses outlines with !important.
127
+ outline-width: base.$focusring-width !important;
128
+ }
89
129
  }
90
130
 
91
131
  hr {
@@ -141,13 +181,14 @@
141
181
  }
142
182
 
143
183
  @mixin base {
144
- --megamenu-main-height: #{convert.to-rem(80px)};
184
+ --megamenu-main-height: #{convert.to-rem(headerHeights.$collapsed)};
145
185
 
146
186
  top: 0;
147
187
  left: 0;
148
188
  width: 100%;
149
189
  z-index: 11;
150
190
  font-size: 1rem;
191
+ font-weight: 400;
151
192
  color: var(--color-text-default);
152
193
  background-color: var(--color-surface-primary);
153
194
  box-sizing: border-box;
@@ -155,7 +196,7 @@
155
196
 
156
197
  @include breakpoint.get("lg") {
157
198
  &:has(.megamenu__nav-logo > [data-megamenu-brand-mark]):not(.top-hidden) {
158
- --megamenu-main-height: #{convert.to-rem(107px)};
199
+ --megamenu-main-height: #{convert.to-rem(headerHeights.$brand-main)};
159
200
  }
160
201
  }
161
202
 
@@ -163,7 +204,7 @@
163
204
  &:not(.non-sticky) {
164
205
  position: fixed;
165
206
 
166
- body.has-modal & {
207
+ @at-root body.has-modal .megamenu:not(.non-sticky) {
167
208
  padding-right: var(--ods-modal-scrollbar-width, 0px);
168
209
  }
169
210
 
@@ -177,9 +218,9 @@
177
218
  @include breakpoint.get("lg") {
178
219
  #{config.$class-root}__top[data-hide-when-sticky="true"] {
179
220
  transition:
180
- height 0.2s ease-in-out,
181
- transform 0.2s ease-in-out,
182
- opacity 0.2s ease-in-out;
221
+ height var(--header-transition, #{headerHeights.$transition}),
222
+ transform var(--header-transition, #{headerHeights.$transition}),
223
+ opacity var(--header-transition, #{headerHeights.$transition});
183
224
 
184
225
  &.is-hidden {
185
226
  height: 0;
@@ -191,7 +232,8 @@
191
232
  }
192
233
 
193
234
  #{config.$class-root}__main {
194
- transition: min-height 0.2s ease-in-out;
235
+ transition: min-height
236
+ var(--header-transition, #{headerHeights.$transition});
195
237
  }
196
238
  }
197
239
  }
@@ -333,34 +375,34 @@
333
375
  }
334
376
 
335
377
  @include breakpoint.get("lg") {
336
- .megamenu:not(.top-hidden)
337
- &
338
- > [data-megamenu-brand-mark]
339
- > [data-megamenu-logo-full] {
378
+ @at-root .megamenu:not(.top-hidden)
379
+ .megamenu__nav-logo
380
+ > [data-megamenu-brand-mark]
381
+ > [data-megamenu-logo-full] {
340
382
  visibility: visible;
341
383
  pointer-events: auto;
342
384
  }
343
385
 
344
- .megamenu:not(.top-hidden)
345
- &
346
- > [data-megamenu-brand-mark]
347
- > [data-megamenu-logo-compact] {
386
+ @at-root .megamenu:not(.top-hidden)
387
+ .megamenu__nav-logo
388
+ > [data-megamenu-brand-mark]
389
+ > [data-megamenu-logo-compact] {
348
390
  visibility: hidden;
349
391
  pointer-events: none;
350
392
  }
351
393
 
352
- .megamenu.top-hidden
353
- &
354
- > [data-megamenu-brand-mark]
355
- > [data-megamenu-logo-full] {
394
+ @at-root .megamenu.top-hidden
395
+ .megamenu__nav-logo
396
+ > [data-megamenu-brand-mark]
397
+ > [data-megamenu-logo-full] {
356
398
  visibility: hidden;
357
399
  pointer-events: none;
358
400
  }
359
401
 
360
- .megamenu.top-hidden
361
- &
362
- > [data-megamenu-brand-mark]
363
- > [data-megamenu-logo-compact] {
402
+ @at-root .megamenu.top-hidden
403
+ .megamenu__nav-logo
404
+ > [data-megamenu-brand-mark]
405
+ > [data-megamenu-logo-compact] {
364
406
  visibility: visible;
365
407
  pointer-events: auto;
366
408
  }
@@ -409,6 +451,7 @@
409
451
  &:active,
410
452
  &.is-active {
411
453
  color: var(--color-text-default) !important;
454
+ text-decoration: none;
412
455
  box-shadow: config.$active-line;
413
456
  }
414
457
  }
@@ -456,6 +499,7 @@
456
499
  &:active,
457
500
  &.is-active {
458
501
  color: var(--color-text-default) !important;
502
+ background-color: var(--color-surface-accent);
459
503
  outline: solid 2px var(--color-border-accent);
460
504
  outline-offset: -2px;
461
505
  }
@@ -520,6 +564,7 @@
520
564
  align-items: center;
521
565
  color: var(--color-text-default);
522
566
  background: none;
567
+ text-decoration: none;
523
568
  border: none;
524
569
  cursor: pointer;
525
570
  padding: 0;
@@ -529,6 +574,7 @@
529
574
  &:active,
530
575
  &.is-active {
531
576
  color: var(--color-text-default) !important;
577
+ text-decoration: none;
532
578
  box-shadow: config.$active-line;
533
579
  }
534
580
 
@@ -687,8 +733,10 @@
687
733
 
688
734
  @mixin top {
689
735
  display: flex;
690
- border-bottom: 1px solid var(--color-border-subtle);
691
- height: convert.to-rem(40px);
736
+ // Draw the divider without a border that keeps a zero-height top bar 1px tall.
737
+ border-bottom: 0;
738
+ box-shadow: inset 0 -1px 0 var(--color-border-subtle);
739
+ height: convert.to-rem(headerHeights.$top);
692
740
  overflow: hidden;
693
741
  }
694
742
 
@@ -698,7 +746,7 @@
698
746
  min-height: var(--megamenu-main-height);
699
747
 
700
748
  @include breakpoint.get("md", "down") {
701
- min-height: convert.to-rem(60px);
749
+ min-height: convert.to-rem(headerHeights.$mobile-spacer);
702
750
  }
703
751
  }
704
752
 
@@ -1085,8 +1133,11 @@
1085
1133
  }
1086
1134
 
1087
1135
  &:hover,
1088
- &:focus-visible {
1136
+ &:focus,
1137
+ &:focus-visible,
1138
+ &:active {
1089
1139
  text-decoration: none;
1140
+ background-color: var(--color-fill-primary);
1090
1141
  }
1091
1142
  }
1092
1143
 
@@ -1146,30 +1197,38 @@
1146
1197
 
1147
1198
  @mixin spacer {
1148
1199
  height: 0;
1149
- transition: height 0.2s ease-in-out;
1200
+ transition: height var(--header-transition, #{headerHeights.$transition});
1150
1201
 
1151
- .megamenu:not(.non-sticky) + & {
1152
- height: var(--header-height, #{convert.to-rem(118px)});
1153
- }
1154
-
1155
- @include breakpoint.get("md", "down") {
1156
- .megamenu:not(.non-sticky) + & {
1157
- height: convert.to-rem(58px);
1202
+ // A spacer belongs to its adjacent menu, including inside a shadow root.
1203
+ @at-root :is(:root, :host) .megamenu:not(.non-sticky) {
1204
+ + :is(.megamenu-spacer, .megamenu-spacer--small) {
1205
+ height: convert.to-rem(headerHeights.$mobile-spacer);
1158
1206
  }
1159
- }
1160
- }
1161
1207
 
1162
- @mixin spacer-small {
1163
- height: 0;
1164
- transition: height 0.2s ease-in-out;
1208
+ @include breakpoint.get("lg") {
1209
+ + :is(.megamenu-spacer, .megamenu-spacer--small) {
1210
+ height: headerHeights.$desktop;
1211
+ }
1165
1212
 
1166
- .megamenu:not(.non-sticky) + & {
1167
- height: var(--header-height, #{convert.to-rem(78px)});
1168
- }
1213
+ &:not(.top-hidden):has(.megamenu__nav-logo > [data-megamenu-brand-mark])
1214
+ + :is(.megamenu-spacer, .megamenu-spacer--small) {
1215
+ height: headerHeights.$brand;
1216
+ }
1169
1217
 
1170
- @include breakpoint.get("md", "down") {
1171
- .megamenu:not(.non-sticky) + & {
1172
- height: convert.to-rem(58px);
1218
+ &:not(.top-hidden):has(
1219
+ .megamenu__nav-logo > [data-megamenu-brand-mark] + span
1220
+ )
1221
+ + :is(.megamenu-spacer, .megamenu-spacer--small) {
1222
+ height: headerHeights.$brand-with-title;
1223
+ }
1224
+
1225
+ &.top-hidden + :is(.megamenu-spacer, .megamenu-spacer--small) {
1226
+ height: headerHeights.$collapsed;
1227
+ }
1228
+
1229
+ + .megamenu-spacer--small {
1230
+ height: headerHeights.$collapsed;
1231
+ }
1173
1232
  }
1174
1233
  }
1175
1234
  }
@@ -1216,8 +1275,11 @@
1216
1275
  @include accordion.icon;
1217
1276
  transform: rotate(90deg);
1218
1277
 
1219
- [aria-expanded="true"] > &,
1220
- &.is-active {
1278
+ @at-root :is(:root, :host)
1279
+ .megamenu
1280
+ [aria-expanded="true"]
1281
+ > .megamenu__accordion-icon,
1282
+ #{&}.is-active {
1221
1283
  @include accordion.icon-active;
1222
1284
  color: var(--color-text-default) !important;
1223
1285
  transform: rotate(270deg);
@@ -1337,17 +1399,17 @@
1337
1399
  }
1338
1400
 
1339
1401
  // Interactive styles for button and anchor tags
1340
- &:not(&--transparent):where(button) {
1402
+ &:not(#{config.$class-root}__tag--transparent):where(button) {
1341
1403
  @include tag.button();
1342
1404
  z-index: 2;
1343
1405
  }
1344
1406
 
1345
- &:not(&--transparent):where(a) {
1407
+ &:not(#{config.$class-root}__tag--transparent):where(a) {
1346
1408
  @include tag.button();
1347
1409
  z-index: 2;
1348
1410
  }
1349
1411
 
1350
- &:not(&--transparent) {
1412
+ &:not(#{config.$class-root}__tag--transparent) {
1351
1413
  @include tag.buttton-disabled();
1352
1414
  }
1353
1415
  }
@@ -3,12 +3,14 @@
3
3
  @use "../../Tile/styles/mixins" as tile;
4
4
  @use "../../../styles/typography/mixins" as typography;
5
5
  @use "../../../styles/tools/convert";
6
+ @use "../../../styles/base/header-height";
6
7
 
7
8
  @use "./mixins";
8
9
  @use "./config";
9
10
 
10
11
  // Modifiers global states and resets
11
- #{config.$class-root} {
12
+ // Keep specificity equal in document and shadow trees, above legacy link rules.
13
+ :is(:root, :host) #{config.$class-root} {
12
14
  @include mixins.globals;
13
15
  }
14
16
 
@@ -24,7 +26,7 @@
24
26
  }
25
27
 
26
28
  // Component
27
- #{config.$class-root} {
29
+ :is(:root, :host) #{config.$class-root} {
28
30
  @include mixins.base;
29
31
 
30
32
  &__nav {
@@ -292,12 +294,9 @@
292
294
  @include mixins.overlay;
293
295
  }
294
296
 
295
- &-spacer {
297
+ &-spacer,
298
+ &-spacer--small {
296
299
  @include mixins.spacer;
297
-
298
- &--small {
299
- @include mixins.spacer-small;
300
- }
301
300
  }
302
301
 
303
302
  &__accordion {
@@ -0,0 +1,185 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import Megamenu from "../Megamenu.static";
4
+
5
+ describe("Megamenu scroll lifecycle", () => {
6
+ let frames: Map<number, FrameRequestCallback>;
7
+ let instances: Megamenu[];
8
+
9
+ const flushFrame = () => {
10
+ const callbacks = [...frames.values()];
11
+ frames.clear();
12
+ callbacks.forEach((callback) => callback(0));
13
+ };
14
+
15
+ const createMenu = (root: HTMLElement | ShadowRoot = document.body) => {
16
+ const element = document.createElement("nav");
17
+ element.setAttribute("data-megamenu", "");
18
+ element.innerHTML = '<div data-hide-when-sticky="true"></div>';
19
+ root.append(element);
20
+ const top = element.firstElementChild as HTMLElement;
21
+ Object.defineProperty(top, "offsetHeight", {
22
+ configurable: true,
23
+ value: 40,
24
+ });
25
+ const instance = new Megamenu(element);
26
+ instances.push(instance);
27
+ return { element, top, instance };
28
+ };
29
+
30
+ beforeEach(() => {
31
+ frames = new Map();
32
+ instances = [];
33
+ let nextId = 0;
34
+ vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
35
+ const id = nextId++;
36
+ frames.set(id, callback);
37
+ return id;
38
+ });
39
+ vi.stubGlobal("cancelAnimationFrame", (id: number) => frames.delete(id));
40
+ vi.stubGlobal("scrollY", 0);
41
+ });
42
+
43
+ afterEach(() => {
44
+ instances.forEach((instance) => instance.destroy());
45
+ document.body.replaceChildren();
46
+ document.body.classList.remove("has-modal");
47
+ document.body.removeAttribute("data-lock-scrolltop");
48
+ vi.restoreAllMocks();
49
+ vi.unstubAllGlobals();
50
+ });
51
+
52
+ it("applies the initial scroll state synchronously", () => {
53
+ vi.stubGlobal("scrollY", 100);
54
+ const { element, top } = createMenu();
55
+
56
+ expect(element.classList.contains("top-hidden")).toBe(true);
57
+ expect(top.classList.contains("is-hidden")).toBe(true);
58
+ expect(frames.size).toBe(0);
59
+ });
60
+
61
+ it("batches a burst of scroll events into one DOM update using the latest position", () => {
62
+ const { element, top } = createMenu();
63
+ const query = vi.spyOn(element, "querySelector");
64
+
65
+ vi.stubGlobal("scrollY", 10);
66
+ window.dispatchEvent(new Event("scroll"));
67
+ vi.stubGlobal("scrollY", 100);
68
+ window.dispatchEvent(new Event("scroll"));
69
+ window.dispatchEvent(new Event("scroll"));
70
+
71
+ expect(query).not.toHaveBeenCalled();
72
+ expect(frames.size).toBe(1);
73
+ flushFrame();
74
+ expect(element.classList.contains("top-hidden")).toBe(true);
75
+ expect(top.classList.contains("is-hidden")).toBe(true);
76
+ expect(query).toHaveBeenCalledTimes(1);
77
+
78
+ vi.stubGlobal("scrollY", 0);
79
+ window.dispatchEvent(new Event("scroll"));
80
+ flushFrame();
81
+ expect(element.classList.contains("top-hidden")).toBe(false);
82
+ expect(top.classList.contains("is-hidden")).toBe(false);
83
+ });
84
+
85
+ it("cancels pending work and removes the scroll listener on destroy", () => {
86
+ const { element, instance } = createMenu();
87
+ vi.stubGlobal("scrollY", 100);
88
+ window.dispatchEvent(new Event("scroll"));
89
+ expect(frames.size).toBe(1);
90
+
91
+ instance.destroy();
92
+
93
+ expect(frames.size).toBe(0);
94
+ flushFrame();
95
+ window.dispatchEvent(new Event("scroll"));
96
+ expect(frames.size).toBe(0);
97
+ expect(element.classList.contains("top-hidden")).toBe(false);
98
+
99
+ instance.init();
100
+ expect(element.classList.contains("top-hidden")).toBe(true);
101
+ vi.stubGlobal("scrollY", 0);
102
+ window.dispatchEvent(new Event("scroll"));
103
+ flushFrame();
104
+ expect(element.classList.contains("top-hidden")).toBe(false);
105
+ });
106
+
107
+ it("uses replacement markup and its current height without reinitialization", () => {
108
+ const { element, top } = createMenu();
109
+ vi.stubGlobal("scrollY", 100);
110
+ window.dispatchEvent(new Event("scroll"));
111
+ const replacement = top.cloneNode() as HTMLElement;
112
+ Object.defineProperty(replacement, "offsetHeight", {
113
+ configurable: true,
114
+ value: 120,
115
+ });
116
+ top.replaceWith(replacement);
117
+ flushFrame();
118
+ expect(element.classList.contains("top-hidden")).toBe(false);
119
+
120
+ Object.defineProperty(replacement, "offsetHeight", {
121
+ configurable: true,
122
+ value: 60,
123
+ });
124
+ window.dispatchEvent(new Event("scroll"));
125
+ flushFrame();
126
+ expect(replacement.classList.contains("is-hidden")).toBe(true);
127
+ });
128
+
129
+ it("uses the locked scroll position while a modal is open", () => {
130
+ const { element } = createMenu();
131
+ document.body.classList.add("has-modal");
132
+ document.body.setAttribute("data-lock-scrolltop", "100");
133
+ vi.stubGlobal("scrollY", 0);
134
+ window.dispatchEvent(new Event("scroll"));
135
+ flushFrame();
136
+ expect(element.classList.contains("top-hidden")).toBe(true);
137
+
138
+ document.body.removeAttribute("data-lock-scrolltop");
139
+ window.dispatchEvent(new Event("scroll"));
140
+ flushFrame();
141
+ expect(element.classList.contains("top-hidden")).toBe(false);
142
+ });
143
+
144
+ it("preserves the brand-mark variant and non-sticky behavior with dynamic markup", () => {
145
+ const { element, top } = createMenu();
146
+ top.remove();
147
+ vi.stubGlobal("scrollY", 100);
148
+ window.dispatchEvent(new Event("scroll"));
149
+ flushFrame();
150
+ expect(element.classList.contains("top-hidden")).toBe(false);
151
+
152
+ element.innerHTML = "<span data-megamenu-brand-mark></span>";
153
+ window.dispatchEvent(new Event("scroll"));
154
+ flushFrame();
155
+ expect(element.classList.contains("top-hidden")).toBe(true);
156
+
157
+ vi.stubGlobal("scrollY", 0);
158
+ window.dispatchEvent(new Event("scroll"));
159
+ flushFrame();
160
+ element.classList.add("non-sticky");
161
+ vi.stubGlobal("scrollY", 100);
162
+ window.dispatchEvent(new Event("scroll"));
163
+ flushFrame();
164
+ expect(element.classList.contains("top-hidden")).toBe(false);
165
+ });
166
+
167
+ it.each(["open", "closed"] as const)(
168
+ "supports explicit initialization in a %s shadow root",
169
+ (mode) => {
170
+ const host = document.createElement("vendor-menu");
171
+ document.body.append(host);
172
+ const { element, top, instance } = createMenu(
173
+ host.attachShadow({ mode }),
174
+ );
175
+
176
+ expect(Megamenu.getInstance(element)).toBe(instance);
177
+ expect(new Megamenu(element)).toBe(instance);
178
+ vi.stubGlobal("scrollY", 100);
179
+ window.dispatchEvent(new Event("scroll"));
180
+ flushFrame();
181
+ expect(element.classList.contains("top-hidden")).toBe(true);
182
+ expect(top.classList.contains("is-hidden")).toBe(true);
183
+ },
184
+ );
185
+ });