@orangesk/orange-design-system 2.0.0-beta.30 → 2.0.0-beta.32

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 (82) hide show
  1. package/README.md +7 -7
  2. package/build/appstore.svg +31 -0
  3. package/build/components/Breadcrumbs/style.css +1 -1
  4. package/build/components/Breadcrumbs/style.css.map +1 -1
  5. package/build/components/Carousel/style.css +1 -1
  6. package/build/components/Carousel/style.css.map +1 -1
  7. package/build/components/Footer/style.css +1 -1
  8. package/build/components/Footer/style.css.map +1 -1
  9. package/build/components/Megamenu/style.css +1 -1
  10. package/build/components/Megamenu/style.css.map +1 -1
  11. package/build/components/Preview/style.css +1 -1
  12. package/build/components/Preview/style.css.map +1 -1
  13. package/build/components/PromoBanner/style.css +1 -1
  14. package/build/components/PromoBanner/style.css.map +1 -1
  15. package/build/components/index.js +1 -1
  16. package/build/components/index.js.map +1 -1
  17. package/build/components/tsconfig.tsbuildinfo +1 -1
  18. package/build/components/types/index.d.ts +17 -2
  19. package/build/components/types/src/components/Carousel/Carousel.static.d.ts +1 -11
  20. package/build/components/types/src/components/Footer/Footer.static.d.ts +21 -0
  21. package/build/components/types/src/components/Footer/constants.d.ts +12 -2
  22. package/build/components/types/src/components/Footer/data.d.ts +5 -0
  23. package/build/components/types/src/components/Footer/static.d.ts +21 -0
  24. package/build/components/types/src/components/Megamenu/Megamenu.d.ts +4 -2
  25. package/build/components/types/src/components/Megamenu/Megamenu.static.d.ts +16 -5
  26. package/build/components/types/src/components/Megamenu/MegamenuBlog.d.ts +1 -1
  27. package/build/components/types/src/components/Megamenu/MegamenuSearchContent.d.ts +10 -0
  28. package/build/components/types/src/components/Megamenu/MyOrangeMobilePanel.d.ts +6 -0
  29. package/build/components/types/src/components/Megamenu/constants.d.ts +15 -0
  30. package/build/components/types/src/components/Megamenu/data.d.ts +21 -0
  31. package/build/components/types/src/components/Megamenu/ids.d.ts +11 -0
  32. package/build/components/types/src/components/PromoBanner/PromoBanner.d.ts +5 -1
  33. package/build/components/types/src/components/index.d.ts +2 -1
  34. package/build/googleplay.svg +52 -0
  35. package/build/lib/base.css.map +1 -1
  36. package/build/lib/components.css +1 -1
  37. package/build/lib/components.css.map +1 -1
  38. package/build/lib/footer.css +1 -1
  39. package/build/lib/footer.css.map +1 -1
  40. package/build/lib/footer.js +2 -0
  41. package/build/lib/footer.js.map +1 -0
  42. package/build/lib/megamenu.css +1 -1
  43. package/build/lib/megamenu.css.map +1 -1
  44. package/build/lib/megamenu.js +1 -1
  45. package/build/lib/megamenu.js.map +1 -1
  46. package/build/lib/scripts.js +1 -1
  47. package/build/lib/scripts.js.map +1 -1
  48. package/build/lib/style.css +1 -1
  49. package/build/lib/style.css.map +1 -1
  50. package/build/lib/tsconfig.tsbuildinfo +1 -1
  51. package/build/lib/utilities.css +1 -1
  52. package/build/lib/utilities.css.map +1 -1
  53. package/build/search-index.json +8 -4
  54. package/package.json +12 -12
  55. package/src/components/Breadcrumbs/styles/mixins.scss +6 -1
  56. package/src/components/Carousel/Carousel.static.ts +60 -89
  57. package/src/components/Carousel/styles/mixins.scss +4 -12
  58. package/src/components/Footer/Footer.static.ts +130 -0
  59. package/src/components/Footer/Footer.tsx +142 -62
  60. package/src/components/Footer/constants.ts +12 -2
  61. package/src/components/Footer/data.ts +13 -0
  62. package/src/components/Footer/static.ts +59 -0
  63. package/src/components/Footer/styles/mixins.scss +122 -18
  64. package/src/components/Footer/styles/style.scss +63 -4
  65. package/src/components/Footer/tests/Footer.unit.test.js +2 -2
  66. package/src/components/Megamenu/Megamenu.static.ts +200 -90
  67. package/src/components/Megamenu/Megamenu.tsx +363 -615
  68. package/src/components/Megamenu/MegamenuBlog.tsx +192 -73
  69. package/src/components/Megamenu/MegamenuSearchContent.tsx +74 -0
  70. package/src/components/Megamenu/MyOrangeMobilePanel.tsx +127 -0
  71. package/src/components/Megamenu/constants.ts +15 -0
  72. package/src/components/Megamenu/data.ts +231 -0
  73. package/src/components/Megamenu/ids.ts +35 -0
  74. package/src/components/Megamenu/styles/mixins.scss +223 -16
  75. package/src/components/Megamenu/styles/style.scss +64 -0
  76. package/src/components/Preview/styles/style.scss +2 -1
  77. package/src/components/PromoBanner/PromoBanner.tsx +12 -1
  78. package/src/components/PromoBanner/styles/mixins.scss +31 -7
  79. package/src/components/PromoBanner/styles/style.scss +41 -0
  80. package/src/components/PromoBanner/tests/PromoBanner.unit.test.js +44 -0
  81. package/src/components/index.ts +3 -0
  82. package/src/styles/utilities/horizontal-scroll.scss +28 -11
@@ -26,18 +26,23 @@ export default class Megamenu {
26
26
  config!: Required<MegamenuConfig>;
27
27
  toggleButtons!: NodeListOf<HTMLButtonElement>;
28
28
  dropdowns!: NodeListOf<HTMLElement>;
29
- mobileMenuButton!: HTMLButtonElement | null;
30
- mobileMenu!: HTMLElement | null;
31
- mobileOverlay!: HTMLElement | null;
32
- mobileCloseButton!: HTMLButtonElement | null;
29
+ mobileMenuButtons!: HTMLButtonElement[];
30
+ mobilePanels!: HTMLElement[];
31
+ mobileCloseButtons!: HTMLButtonElement[];
32
+ activeMobilePanel!: HTMLElement | null;
33
+ activeMobileTarget!: string | null;
33
34
  desktopOverlay!: HTMLButtonElement | null;
34
35
  accordionButtons!: NodeListOf<HTMLButtonElement>;
36
+ desktopOverlayClickHandler!: (event: Event) => void;
37
+ mobileCloseHandler!: (event: Event) => void;
35
38
  focusTrapHandler!: (event: KeyboardEvent) => void;
39
+ focusTrapElement!: HTMLElement | null;
36
40
  lastFocusedElement!: HTMLElement | null;
37
41
  isKeyboardUser!: boolean;
38
42
  keydownHandler!: (e: KeyboardEvent) => void;
39
43
  mousedownHandler!: () => void;
40
44
  touchstartHandler!: () => void;
45
+ instanceName!: string;
41
46
 
42
47
  constructor(element: HTMLElement, config: MegamenuConfig = {}) {
43
48
  if ((element as any).ODS_Megamenu) {
@@ -53,6 +58,12 @@ export default class Megamenu {
53
58
  this.handleAccordionClick = this.handleAccordionClick.bind(this);
54
59
  this.handleArrowNavigation = this.handleArrowNavigation.bind(this);
55
60
  this.trapFocus = this.trapFocus.bind(this);
61
+ this.handleMobileMenuToggle = this.handleMobileMenuToggle.bind(this);
62
+ this.desktopOverlayClickHandler = () => this.closeAllDropdowns();
63
+ this.mobileCloseHandler = (event: Event) => {
64
+ event.preventDefault();
65
+ this.closeMobileMenu();
66
+ };
56
67
 
57
68
  (this.element as any).ODS_Megamenu = this;
58
69
 
@@ -71,15 +82,42 @@ export default class Megamenu {
71
82
  );
72
83
  this.dropdowns = this.element.querySelectorAll(`.${CLASS_NAV_DROPDOWN}`);
73
84
 
74
- // Find mobile menu elements
75
- this.mobileMenuButton = this.element.querySelector(
76
- `.${CLASS_HIDE_LG_UP} button`,
77
- ) as HTMLButtonElement;
78
- this.mobileMenu = this.element.querySelector(`.${CLASS_MOBILE}`);
79
- this.mobileOverlay = this.element.querySelector(`.${CLASS_MOBILE_OVERLAY}`);
80
- this.mobileCloseButton = this.element.querySelector(
81
- "[data-megamenu-close-button]",
82
- ) as HTMLButtonElement;
85
+ // Resolve an optional instance name so external triggers can target this menu.
86
+ this.instanceName =
87
+ this.element.getAttribute("data-megamenu-name") || this.element.id || "";
88
+
89
+ // Find mobile panel elements.
90
+ this.mobilePanels = Array.from(
91
+ this.element.querySelectorAll(`.${CLASS_MOBILE}`),
92
+ );
93
+ this.activeMobilePanel = null;
94
+ this.activeMobileTarget = null;
95
+ this.focusTrapElement = null;
96
+
97
+ // Find all mobile toggles both in menu and in document scope.
98
+ const localToggleButtons = Array.from(
99
+ this.element.querySelectorAll(
100
+ `[data-megamenu-mobile-toggle], .${CLASS_HIDE_LG_UP} button`,
101
+ ),
102
+ ) as HTMLButtonElement[];
103
+ const globalToggleButtons = Array.from(
104
+ document.querySelectorAll(`[data-megamenu-mobile-toggle]`),
105
+ ) as HTMLButtonElement[];
106
+
107
+ this.mobileMenuButtons = Array.from(
108
+ new Set([...localToggleButtons, ...globalToggleButtons]),
109
+ ).filter((button) => this.shouldUseMobileTrigger(button));
110
+
111
+ this.mobileCloseButtons = Array.from(
112
+ this.element.querySelectorAll(
113
+ `.${CLASS_MOBILE_OVERLAY}, [data-megamenu-close-button]`,
114
+ ),
115
+ ) as HTMLButtonElement[];
116
+
117
+ this.mobilePanels.forEach((panel) => {
118
+ panel.setAttribute("aria-hidden", "true");
119
+ });
120
+ this.syncMobileButtonState();
83
121
 
84
122
  // Find desktop overlay element
85
123
  this.desktopOverlay = this.element.querySelector(
@@ -97,30 +135,18 @@ export default class Megamenu {
97
135
  });
98
136
 
99
137
  // Add mobile menu event listeners
100
- if (this.mobileMenuButton) {
101
- this.mobileMenuButton.addEventListener(
102
- "click",
103
- this.handleMobileMenuToggle.bind(this),
104
- );
105
- }
106
- if (this.mobileOverlay) {
107
- this.mobileOverlay.addEventListener(
108
- "click",
109
- this.closeMobileMenu.bind(this),
110
- );
111
- }
112
- if (this.mobileCloseButton) {
113
- this.mobileCloseButton.addEventListener(
114
- "click",
115
- this.closeMobileMenu.bind(this),
116
- );
117
- }
138
+ this.mobileMenuButtons.forEach((button) => {
139
+ button.addEventListener("click", this.handleMobileMenuToggle);
140
+ });
141
+ this.mobileCloseButtons.forEach((button) => {
142
+ button.addEventListener("click", this.mobileCloseHandler);
143
+ });
118
144
 
119
145
  // Add desktop overlay event listener
120
146
  if (this.desktopOverlay) {
121
147
  this.desktopOverlay.addEventListener(
122
148
  "click",
123
- this.closeAllDropdowns.bind(this),
149
+ this.desktopOverlayClickHandler,
124
150
  );
125
151
  }
126
152
 
@@ -155,6 +181,70 @@ export default class Megamenu {
155
181
  this.updateTabIndices();
156
182
  }
157
183
 
184
+ shouldUseMobileTrigger(button: HTMLButtonElement) {
185
+ if (this.element.contains(button)) {
186
+ return true;
187
+ }
188
+
189
+ const targetName = button.getAttribute("data-megamenu-mobile-for");
190
+ if (!targetName) {
191
+ const allMegamenus = document.querySelectorAll("[data-megamenu]");
192
+ return allMegamenus.length === 1 && allMegamenus[0] === this.element;
193
+ }
194
+
195
+ if (!this.instanceName) {
196
+ return false;
197
+ }
198
+
199
+ return targetName === this.instanceName;
200
+ }
201
+
202
+ getDefaultMobileTarget() {
203
+ const firstPanel = this.mobilePanels[0];
204
+ return firstPanel?.getAttribute("data-megamenu-mobile-panel") || "main";
205
+ }
206
+
207
+ getTargetFromElement(control: HTMLElement | null) {
208
+ return (
209
+ control?.getAttribute("data-megamenu-mobile-target") ||
210
+ this.getDefaultMobileTarget()
211
+ );
212
+ }
213
+
214
+ getPanelByTarget(target: string) {
215
+ const matchingPanel = this.mobilePanels.find(
216
+ (panel) => panel.getAttribute("data-megamenu-mobile-panel") === target,
217
+ );
218
+
219
+ if (matchingPanel) {
220
+ return matchingPanel;
221
+ }
222
+
223
+ // Backward compatibility for existing markup with a single mobile panel.
224
+ if (
225
+ target === this.getDefaultMobileTarget() &&
226
+ this.mobilePanels.length === 1
227
+ ) {
228
+ return this.mobilePanels[0];
229
+ }
230
+
231
+ return null;
232
+ }
233
+
234
+ syncMobileButtonState() {
235
+ this.mobileMenuButtons.forEach((button) => {
236
+ const target = this.getTargetFromElement(button);
237
+ const isActive = this.activeMobileTarget === target;
238
+ button.setAttribute("aria-expanded", isActive ? "true" : "false");
239
+ button.classList.toggle(this.config.activeClass, isActive);
240
+ });
241
+ }
242
+
243
+ setMobilePanelState(panel: HTMLElement, isOpen: boolean) {
244
+ panel.classList.toggle(this.config.activeClass, isOpen);
245
+ panel.setAttribute("aria-hidden", isOpen ? "false" : "true");
246
+ }
247
+
158
248
  handleToggleClick(event: Event) {
159
249
  const button = event.currentTarget as HTMLButtonElement;
160
250
  const isActive = button.classList.contains(this.config.activeClass);
@@ -333,53 +423,79 @@ export default class Megamenu {
333
423
 
334
424
  handleMobileMenuToggle(event: Event) {
335
425
  event.preventDefault();
336
- if (this.mobileMenu?.classList.contains(this.config.activeClass)) {
426
+ const trigger = event.currentTarget as HTMLButtonElement;
427
+ const target = this.getTargetFromElement(trigger);
428
+ const panel = this.getPanelByTarget(target);
429
+
430
+ if (!panel) {
431
+ return;
432
+ }
433
+
434
+ if (
435
+ this.activeMobileTarget === target &&
436
+ panel.classList.contains(this.config.activeClass)
437
+ ) {
337
438
  this.closeMobileMenu();
338
439
  } else {
339
- this.openMobileMenu();
440
+ this.openMobileMenu(target, trigger);
340
441
  }
341
442
  }
342
443
 
343
- openMobileMenu() {
344
- if (this.mobileMenu) {
345
- // Store the element that opened the menu for focus restoration
346
- this.lastFocusedElement = document.activeElement as HTMLElement;
444
+ openMobileMenu(target = this.getDefaultMobileTarget(), source?: HTMLElement) {
445
+ const panel = this.getPanelByTarget(target);
347
446
 
348
- this.mobileMenu.classList.add(this.config.activeClass);
349
- document.body.style.overflow = "hidden"; // Prevent background scrolling
447
+ if (!panel) {
448
+ return;
449
+ }
350
450
 
351
- // Set up focus trap immediately to prevent focus escape
352
- this.trapFocus(this.mobileMenu);
451
+ // Keep one side panel active at a time.
452
+ this.mobilePanels.forEach((mobilePanel) => {
453
+ this.setMobilePanelState(mobilePanel, mobilePanel === panel);
454
+ });
353
455
 
354
- // Only manage focus if user is using keyboard navigation (not touch)
355
- if (this.isKeyboardUser) {
356
- // Focus first focusable element in mobile menu for keyboard users
357
- const firstFocusable = this.mobileMenu.querySelector(
358
- "button, a",
359
- ) as HTMLElement;
360
- if (firstFocusable) {
361
- // Reduced delay for better responsiveness
362
- setTimeout(() => firstFocusable.focus(), 50);
363
- }
456
+ // Store the element that opened the menu for focus restoration.
457
+ this.lastFocusedElement =
458
+ source || (document.activeElement as HTMLElement | null);
459
+ this.activeMobilePanel = panel;
460
+ this.activeMobileTarget = target;
461
+ this.syncMobileButtonState();
462
+
463
+ document.body.style.overflow = "hidden";
464
+ this.removeFocusTrap();
465
+ this.trapFocus(panel);
466
+
467
+ if (this.isKeyboardUser) {
468
+ const firstFocusable = panel.querySelector("button, a") as HTMLElement;
469
+ if (firstFocusable) {
470
+ setTimeout(() => firstFocusable.focus(), 50);
364
471
  }
365
472
  }
473
+
474
+ this.updateTabIndices();
366
475
  }
367
476
 
368
477
  closeMobileMenu() {
369
- if (this.mobileMenu) {
370
- this.mobileMenu.classList.remove(this.config.activeClass);
371
- document.body.style.overflow = ""; // Restore scrolling
372
- this.closeAllAccordions(); // Close any open accordions
478
+ if (!this.activeMobilePanel) {
479
+ return;
480
+ }
373
481
 
374
- // Remove focus trap (safe to call even if not active)
375
- this.removeFocusTrap();
482
+ this.mobilePanels.forEach((panel) => {
483
+ this.setMobilePanelState(panel, false);
484
+ });
376
485
 
377
- // Return focus to element that opened the menu only for keyboard users
378
- if (this.isKeyboardUser && this.lastFocusedElement) {
379
- this.lastFocusedElement.focus();
380
- }
381
- this.lastFocusedElement = null;
486
+ document.body.style.overflow = "";
487
+ this.closeAllAccordions();
488
+ this.removeFocusTrap();
489
+
490
+ if (this.isKeyboardUser && this.lastFocusedElement) {
491
+ this.lastFocusedElement.focus();
382
492
  }
493
+
494
+ this.activeMobilePanel = null;
495
+ this.activeMobileTarget = null;
496
+ this.lastFocusedElement = null;
497
+ this.syncMobileButtonState();
498
+ this.updateTabIndices();
383
499
  }
384
500
 
385
501
  showDesktopOverlay() {
@@ -497,12 +613,17 @@ export default class Megamenu {
497
613
  }
498
614
  };
499
615
 
616
+ this.focusTrapElement = element;
500
617
  element.addEventListener("keydown", this.focusTrapHandler);
501
618
  }
502
619
 
503
620
  removeFocusTrap() {
504
- if (this.mobileMenu && this.focusTrapHandler) {
505
- this.mobileMenu.removeEventListener("keydown", this.focusTrapHandler);
621
+ if (this.focusTrapElement && this.focusTrapHandler) {
622
+ this.focusTrapElement.removeEventListener(
623
+ "keydown",
624
+ this.focusTrapHandler,
625
+ );
626
+ this.focusTrapElement = null;
506
627
  }
507
628
  }
508
629
 
@@ -526,18 +647,18 @@ export default class Megamenu {
526
647
  });
527
648
 
528
649
  // Also update mobile menu elements when mobile menu is active
529
- if (this.mobileMenu) {
530
- const isMobileMenuActive = this.mobileMenu.classList.contains(
650
+ this.mobilePanels.forEach((panel) => {
651
+ const isMobilePanelActive = panel.classList.contains(
531
652
  this.config.activeClass,
532
653
  );
533
- const mobileMenuFocusableElements = this.mobileMenu.querySelectorAll(
654
+ const mobilePanelFocusableElements = panel.querySelectorAll(
534
655
  "a, button, [tabindex]:not([tabindex='-1'])",
535
656
  ) as NodeListOf<HTMLElement>;
536
657
 
537
- mobileMenuFocusableElements.forEach((element) => {
538
- element.tabIndex = isMobileMenuActive ? 0 : -1;
658
+ mobilePanelFocusableElements.forEach((element) => {
659
+ element.tabIndex = isMobilePanelActive ? 0 : -1;
539
660
  });
540
- }
661
+ });
541
662
  }
542
663
 
543
664
  destroy() {
@@ -547,30 +668,18 @@ export default class Megamenu {
547
668
  });
548
669
 
549
670
  // Remove mobile menu event listeners
550
- if (this.mobileMenuButton) {
551
- this.mobileMenuButton.removeEventListener(
552
- "click",
553
- this.handleMobileMenuToggle.bind(this),
554
- );
555
- }
556
- if (this.mobileOverlay) {
557
- this.mobileOverlay.removeEventListener(
558
- "click",
559
- this.closeMobileMenu.bind(this),
560
- );
561
- }
562
- if (this.mobileCloseButton) {
563
- this.mobileCloseButton.removeEventListener(
564
- "click",
565
- this.closeMobileMenu.bind(this),
566
- );
567
- }
671
+ this.mobileMenuButtons.forEach((button) => {
672
+ button.removeEventListener("click", this.handleMobileMenuToggle);
673
+ });
674
+ this.mobileCloseButtons.forEach((button) => {
675
+ button.removeEventListener("click", this.mobileCloseHandler);
676
+ });
568
677
 
569
678
  // Remove desktop overlay event listener
570
679
  if (this.desktopOverlay) {
571
680
  this.desktopOverlay.removeEventListener(
572
681
  "click",
573
- this.closeAllDropdowns.bind(this),
682
+ this.desktopOverlayClickHandler,
574
683
  );
575
684
  }
576
685
 
@@ -589,6 +698,7 @@ export default class Megamenu {
589
698
 
590
699
  // Restore body overflow if menu was open
591
700
  document.body.style.overflow = "";
701
+ this.removeFocusTrap();
592
702
 
593
703
  // Clean up instance reference
594
704
  (this.element as any).ODS_Megamenu = null;