@orangesk/orange-design-system 2.0.0-beta.18 → 2.0.0-beta.19

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 (45) hide show
  1. package/build/components/Accordion/style.css +1 -1
  2. package/build/components/Accordion/style.css.map +1 -1
  3. package/build/components/AnchorNavigation/style.css +1 -1
  4. package/build/components/AnchorNavigation/style.css.map +1 -1
  5. package/build/components/Megamenu/style.css +1 -1
  6. package/build/components/Megamenu/style.css.map +1 -1
  7. package/build/components/index.js +1 -23
  8. package/build/components/index.js.map +1 -1
  9. package/build/components/tsconfig.tsbuildinfo +1 -1
  10. package/build/components/types/index.d.ts +2 -2
  11. package/build/components/types/src/components/Accordion/Accordion.d.ts +2 -2
  12. package/build/components/types/src/components/Accordion/Accordion.static.d.ts +1 -0
  13. package/build/components/types/src/components/Carousel/Carousel.static.d.ts +38 -18
  14. package/build/lib/components.css +1 -1
  15. package/build/lib/components.css.map +1 -1
  16. package/build/lib/megamenu.css +1 -1
  17. package/build/lib/megamenu.css.map +1 -1
  18. package/build/lib/megamenu.js +1 -1
  19. package/build/lib/megamenu.js.map +1 -1
  20. package/build/lib/scripts.js +1 -9
  21. package/build/lib/scripts.js.map +1 -1
  22. package/build/lib/style.css +1 -1
  23. package/build/lib/style.css.map +1 -1
  24. package/build/lib/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +1 -1
  26. package/src/components/Accordion/Accordion.static.ts +36 -30
  27. package/src/components/Accordion/Accordion.tsx +4 -4
  28. package/src/components/Accordion/AccordionHeader.tsx +0 -10
  29. package/src/components/Accordion/styles/mixins.scss +0 -6
  30. package/src/components/Accordion/styles/style.scss +2 -2
  31. package/src/components/Accordion/tests/Accordion.unit.test.js +12 -26
  32. package/src/components/AnchorNavigation/AnchorNavigation.static.ts +0 -1
  33. package/src/components/AnchorNavigation/styles/mixins.scss +4 -0
  34. package/src/components/Carousel/Carousel.static.ts +177 -91
  35. package/src/components/Carousel/tests/Carousel.static.test.js +213 -0
  36. package/src/components/Carousel/tests/Carousel.unit.test.js +58 -0
  37. package/src/components/Expander/Expander.static.ts +11 -12
  38. package/src/components/Footer/Footer.tsx +1 -1
  39. package/src/components/Footer/tests/Footer.conformance.test.js +4 -4
  40. package/src/components/Footer/tests/Footer.unit.test.js +4 -4
  41. package/src/components/Megamenu/Megamenu.tsx +71 -55
  42. package/src/components/Megamenu/MegamenuBlog.tsx +3 -6
  43. package/src/components/Megamenu/static.ts +0 -9
  44. package/src/components/Megamenu/styles/mixins.scss +1 -1
  45. package/src/components/PromoBanner/PromoBanner.tsx +2 -0
@@ -7,6 +7,8 @@
7
7
  @use "../../Megamenu/styles/config" as megamenuConfig;
8
8
  @use "sass:map" as sass-map;
9
9
 
10
+ $body-text-small: sass-map.get(typography.$body-text, "small");
11
+
10
12
  @mixin anchor-navigation() {
11
13
  position: sticky;
12
14
  top: 0;
@@ -35,6 +37,8 @@
35
37
  text-decoration: none !important;
36
38
  display: inline-block;
37
39
  cursor: pointer;
40
+ font-size: sass-map.get($body-text-small, "font-size") !important;
41
+ line-height: sass-map.get($body-text-small, "line-height") !important;
38
42
  font-weight: 700 !important;
39
43
 
40
44
  &:last-child {
@@ -27,6 +27,7 @@ import {
27
27
  CLASS_SLIDE_NEXT,
28
28
  CLASS_SLIDE_PREV,
29
29
  CLASS_BLEED_RIGHT,
30
+ CLASS_VIEWPORT_WRAPPER,
30
31
  } from "./constants";
31
32
 
32
33
  export const defaultConfig: SwiperOptions = {
@@ -49,6 +50,7 @@ export const defaultConfig: SwiperOptions = {
49
50
  enabled: true,
50
51
  horizontalClass: CLASS_SCROLLBAR_HORIZONTAL,
51
52
  dragClass: CLASS_SCROLLBAR_DRAG,
53
+ hide: false,
52
54
  },
53
55
  slidesPerView: 1.2,
54
56
  a11y: {
@@ -105,7 +107,6 @@ export default class Carousel {
105
107
 
106
108
  (this.element as any).ODS_Carousel = this;
107
109
 
108
- // Defer initialization to ensure DOM is ready
109
110
  requestAnimationFrame(() => {
110
111
  this.init();
111
112
  });
@@ -121,30 +122,110 @@ export default class Carousel {
121
122
 
122
123
  this.instance = new Swiper(this.viewport, {
123
124
  ...this.config,
125
+ enabled: false,
124
126
  modules: [Navigation, Pagination, Scrollbar, A11y, Keyboard],
125
127
  on: {
126
128
  slideChange: this.handleSlideChange,
127
129
  slideChangeTransitionEnd: () => {
128
- // Update external controls after transition completes
129
130
  this.updateExternalControlsState();
130
131
  },
131
132
  },
132
133
  });
133
134
 
134
- // Check if this is a bleed-right carousel and adjust after initialization
135
+ this.updateCarouselEnabledState();
136
+
135
137
  if (this.element.classList.contains(CLASS_BLEED_RIGHT)) {
136
138
  this.adjustConfigForBleedRight();
139
+ this.fixBleedRightScrollbar();
140
+ }
141
+
142
+ if (this.instance && typeof this.instance.on === "function") {
143
+ this.instance.on("resize", () => {
144
+ this.updateCarouselEnabledState();
145
+ });
137
146
  }
138
147
 
139
- // Initialize external controls
140
148
  this.initExternalControls();
141
149
  }
142
150
 
151
+ /**
152
+ * Fix scrollbar drag size and position for bleed-right carousels.
153
+ * Overrides Swiper's default scrollbar calculations to work correctly with bleed-right layouts.
154
+ */
155
+ fixBleedRightScrollbar() {
156
+ const updateScrollbar = () => {
157
+ const viewportWrapper = this.element.querySelector(
158
+ `.${CLASS_VIEWPORT_WRAPPER}`,
159
+ ) as HTMLElement;
160
+ const scrollbar = this.instance?.scrollbar;
161
+
162
+ if (!viewportWrapper || !scrollbar || !scrollbar.dragEl) {
163
+ return;
164
+ }
165
+
166
+ const slidesCount = this.instance.slides.length;
167
+ const slidesPerView = Number(this.instance.params.slidesPerView) || 1;
168
+
169
+ // Only show scrollbar when there are more slides than visible at once
170
+ if (slidesCount > slidesPerView) {
171
+ if (scrollbar.el) {
172
+ scrollbar.el.style.display = "";
173
+ }
174
+
175
+ const scrollbarWidth = scrollbar.el.offsetWidth;
176
+
177
+ // Calculate drag handle size based on the ratio of visible slides to total slides
178
+ // Example: 3 visible out of 5 total = 60% of scrollbar width
179
+ const visibleRatio = slidesPerView / slidesCount;
180
+ const dragSize = visibleRatio * scrollbarWidth;
181
+ // Ensure minimum 30px for usability
182
+ const finalDragSize = Math.max(dragSize, 30);
183
+
184
+ scrollbar.dragEl.style.width = `${finalDragSize}px`;
185
+ scrollbar.dragEl.style.display = "";
186
+
187
+ // Calculate scrollbar position based on carousel's actual scroll position
188
+ const maxScrollableSlides = slidesCount - slidesPerView;
189
+ const currentTranslate = Math.abs(this.instance.translate || 0);
190
+ const slideSize = this.instance.slidesSizesGrid?.[0] || 0;
191
+
192
+ // Determine how far we've scrolled as a ratio (0 = start, 1 = end)
193
+ const maxTranslateValue = maxScrollableSlides * slideSize;
194
+ const scrollRatio =
195
+ maxTranslateValue > 0
196
+ ? Math.min(currentTranslate / maxTranslateValue, 1)
197
+ : 0;
198
+
199
+ // Position scrollbar drag handle proportionally
200
+ const maxDragTranslate = scrollbarWidth - finalDragSize;
201
+ const dragTranslateX = scrollRatio * maxDragTranslate;
202
+
203
+ scrollbar.dragEl.style.transform = `translate3d(${dragTranslateX}px, 0, 0)`;
204
+ } else {
205
+ // Hide scrollbar when all slides are visible (no scrolling needed)
206
+ if (scrollbar.el) {
207
+ scrollbar.el.style.display = "none";
208
+ }
209
+ }
210
+ };
211
+
212
+ // Initial update (double requestAnimationFrame ensures DOM is fully ready)
213
+ requestAnimationFrame(() => {
214
+ requestAnimationFrame(updateScrollbar);
215
+ });
216
+
217
+ // Keep scrollbar in sync with carousel state changes
218
+ this.instance.on("progress", updateScrollbar);
219
+ this.instance.on("slideChange", updateScrollbar);
220
+ this.instance.on("resize", updateScrollbar);
221
+ this.instance.on("update", updateScrollbar);
222
+ this.instance.on("setTranslate", updateScrollbar);
223
+ }
224
+
143
225
  getElements() {
144
226
  this.viewport = this.element.querySelector(SELECTOR_VIEWPORT)!;
145
227
  this.track = this.viewport.querySelector(SELECTOR_TRACK)!;
146
228
 
147
- // Ensure pagination container has proper role
148
229
  const paginationEl = this.element.querySelector(
149
230
  SELECTOR_DOTS,
150
231
  ) as HTMLElement | null;
@@ -186,71 +267,90 @@ export default class Carousel {
186
267
  };
187
268
  }
188
269
 
189
- adjustConfigForBleedRight() {
190
- // For bleed-right variant, calculate the exact bleed amount dynamically
191
- // This allows the last slide to scroll back to align with container edge
270
+ /**
271
+ * Enable or disable the carousel based on whether content requires scrolling.
272
+ * Carousel is enabled only when there are more slides than can fit in the current viewport.
273
+ */
274
+ private updateCarouselEnabledState(): void {
275
+ if (!this.instance || !this.instance.params) return;
192
276
 
193
- // Wait for next frame to ensure container has final dimensions
194
- requestAnimationFrame(() => {
195
- // Find the container element
196
- const container = this.element.closest(".container") as HTMLElement;
197
- if (!container || !this.instance) return;
277
+ const slidesCount = this.instance.slides.length;
278
+ const slidesPerView = Number(this.instance.params.slidesPerView) || 1;
198
279
 
199
- // Calculate the actual bleed amount with limits to match CSS
200
- const containerWidth = container.offsetWidth;
201
- const halfViewportWidth = window.innerWidth / 2;
202
- const halfContainerWidth = containerWidth / 2;
203
- let bleedAmount = halfViewportWidth - halfContainerWidth;
280
+ if (slidesCount > slidesPerView) {
281
+ this.instance.enable();
282
+ } else {
283
+ this.instance.disable();
284
+ }
285
+ }
204
286
 
205
- // On very large screens (above 2K), stop the bleeding effect entirely
206
- if (window.innerWidth >= 2560) {
207
- bleedAmount = 0;
208
- }
287
+ /**
288
+ * Calculate how much the carousel should extend beyond the container (bleed effect).
289
+ * On screens 2560px and wider, the bleed effect is disabled.
290
+ */
291
+ private calculateBleedAmount(container: HTMLElement): number {
292
+ const containerWidth = container.offsetWidth;
293
+ const halfViewportWidth = window.innerWidth / 2;
294
+ const halfContainerWidth = containerWidth / 2;
295
+ let bleedAmount = halfViewportWidth - halfContainerWidth;
296
+
297
+ if (window.innerWidth >= 2560) {
298
+ bleedAmount = 0;
299
+ }
209
300
 
210
- // Only add offset if there's actual bleeding (positive value)
211
- if (bleedAmount > 0) {
212
- // Update the Swiper configuration
213
- // Add offset only on the right side for right bleed effect
214
- this.instance.params.slidesOffsetAfter = bleedAmount;
215
- this.instance.update();
216
- } else {
217
- // Remove offset if no bleeding should occur
218
- this.instance.params.slidesOffsetAfter = 0;
219
- this.instance.update();
220
- }
301
+ return Math.max(bleedAmount, 0);
302
+ }
221
303
 
222
- // Also listen for window resize to recalculate
223
- const handleResize = () => {
224
- if (this.instance && container) {
225
- const newContainerWidth = container.offsetWidth;
226
- const newHalfViewportWidth = window.innerWidth / 2;
227
- const newHalfContainerWidth = newContainerWidth / 2;
228
- let newBleedAmount = newHalfViewportWidth - newHalfContainerWidth;
229
-
230
- // Apply same limits as above
231
- if (window.innerWidth >= 2560) {
232
- newBleedAmount = 0;
233
- }
304
+ /**
305
+ * Apply the bleed offset to extend the carousel beyond the container edge.
306
+ * Only applies when content exceeds the visible area.
307
+ */
308
+ private applyBleedOffset(
309
+ viewportWrapper: HTMLElement,
310
+ bleedAmount: number,
311
+ ): void {
312
+ const visibleWidth = viewportWrapper.clientWidth;
313
+ const totalWidth = this.track.scrollWidth;
314
+ const contentFitsWithoutScroll = totalWidth <= visibleWidth + 5;
315
+
316
+ if (bleedAmount > 0 && !contentFitsWithoutScroll) {
317
+ this.instance.params.slidesOffsetAfter = bleedAmount;
318
+ } else {
319
+ this.instance.params.slidesOffsetAfter = 0;
320
+ }
234
321
 
235
- if (newBleedAmount > 0) {
236
- this.instance.params.slidesOffsetAfter = newBleedAmount;
237
- } else {
238
- this.instance.params.slidesOffsetAfter = 0;
239
- }
240
- this.instance.update();
241
- }
242
- };
322
+ this.instance.update();
323
+ }
243
324
 
244
- // Remove any existing resize listener to avoid duplicates
245
- window.removeEventListener("resize", handleResize);
246
- window.addEventListener("resize", handleResize);
325
+ /**
326
+ * Configure bleed-right carousel to extend beyond the container edge.
327
+ * Calculates the exact offset needed for the carousel to reach the viewport edge
328
+ * while keeping the last slide aligned with the container edge when scrolled to the end.
329
+ */
330
+ adjustConfigForBleedRight() {
331
+ requestAnimationFrame(() => {
332
+ const container = this.element.closest(".container") as HTMLElement;
333
+ if (!container || !this.instance) return;
334
+
335
+ const viewportWrapper = this.element.querySelector(
336
+ `.${CLASS_VIEWPORT_WRAPPER}`,
337
+ ) as HTMLElement;
338
+ if (!viewportWrapper) return;
339
+
340
+ const bleedAmount = this.calculateBleedAmount(container);
341
+ this.applyBleedOffset(viewportWrapper, bleedAmount);
342
+
343
+ // Recalculate bleed on window resize
344
+ this.instance.on("resize", () => {
345
+ const newBleedAmount = this.calculateBleedAmount(container);
346
+ this.applyBleedOffset(viewportWrapper, newBleedAmount);
347
+ });
247
348
  });
248
349
  }
249
350
 
250
351
  /**
251
- * Handles the slide change event on the carousel.
252
- * Updates the tooltip position for the active slide and hides tooltips for non-active slides.
253
- * Also updates accessibility attributes for pagination buttons and external controls state.
352
+ * Handle carousel slide change events.
353
+ * Updates tooltip positions and accessibility states for the active slide.
254
354
  */
255
355
  handleSlideChange() {
256
356
  const activeSlide = this.track.querySelector(SELECTOR_ACTIVE);
@@ -266,13 +366,11 @@ export default class Carousel {
266
366
  this.hideAllTooltips(nonActiveSlides);
267
367
  }
268
368
 
269
- // Update external controls state based on current position
270
369
  this.updateExternalControlsState();
271
370
  }
272
371
 
273
372
  /**
274
- * Updates the position of tooltips for the given element.
275
- * @param {HTMLElement} element - The element containing tooltip triggers.
373
+ * Update tooltip positions for elements within a slide.
276
374
  */
277
375
  updateTooltipPosition(element: HTMLElement) {
278
376
  const tooltipTriggers = element.querySelectorAll(
@@ -295,8 +393,7 @@ export default class Carousel {
295
393
  }
296
394
 
297
395
  /**
298
- * Hides all tooltips for the given elements.
299
- * @param {NodeListOf<HTMLElement>} elements - The elements containing tooltip triggers.
396
+ * Hide all tooltips for non-active slides.
300
397
  */
301
398
  hideAllTooltips(elements: NodeListOf<Element>) {
302
399
  elements.forEach((element) => {
@@ -322,20 +419,18 @@ export default class Carousel {
322
419
  }
323
420
 
324
421
  /**
325
- * Initialize external controls that reference this carousel
422
+ * Initialize external navigation controls that reference this carousel via data attributes.
423
+ * Supports prev/next buttons placed anywhere in the DOM.
326
424
  */
327
425
  initExternalControls() {
328
- // Get carousel ID from data-carousel-id or element id
329
426
  const carouselId = this.element.dataset.carouselId || this.element.id;
330
427
 
331
428
  if (!carouselId) {
332
- return; // No ID to reference, skip external controls
429
+ return;
333
430
  }
334
431
 
335
- // Store carousel ID for later use
336
432
  this.carouselId = carouselId;
337
433
 
338
- // Find all elements with data-carousel-controls matching this carousel's ID
339
434
  const controlElements = document.querySelectorAll(
340
435
  `[data-carousel-controls="${carouselId}"]`,
341
436
  );
@@ -343,12 +438,10 @@ export default class Carousel {
343
438
  controlElements.forEach((control) => {
344
439
  const htmlControl = control as HTMLElement;
345
440
 
346
- // Skip if already initialized to avoid duplicate event listeners
347
441
  if (htmlControl.hasAttribute("data-carousel-initialized")) {
348
442
  return;
349
443
  }
350
444
 
351
- // Remove any existing event listener first (just in case)
352
445
  if ((htmlControl as any)._carouselClickHandler) {
353
446
  htmlControl.removeEventListener(
354
447
  "click",
@@ -358,7 +451,6 @@ export default class Carousel {
358
451
 
359
452
  const action = htmlControl.dataset.carouselAction;
360
453
 
361
- // Create bound event handler to avoid multiple listeners
362
454
  const clickHandler = (e: Event) => {
363
455
  e.preventDefault();
364
456
  if (htmlControl.hasAttribute("disabled")) {
@@ -374,7 +466,6 @@ export default class Carousel {
374
466
  if (action === "next" || action === "prev") {
375
467
  htmlControl.addEventListener("click", clickHandler);
376
468
 
377
- // Add ARIA attributes for accessibility
378
469
  htmlControl.setAttribute(
379
470
  "aria-label",
380
471
  action === "next"
@@ -383,20 +474,18 @@ export default class Carousel {
383
474
  );
384
475
  htmlControl.setAttribute("type", "button");
385
476
 
386
- // Store the handler reference for potential cleanup
387
477
  (htmlControl as any)._carouselClickHandler = clickHandler;
388
478
  }
389
479
 
390
- // Mark as initialized to avoid duplicate event listeners
391
480
  htmlControl.setAttribute("data-carousel-initialized", "true");
392
481
  });
393
482
 
394
- // Update initial state of external controls
395
483
  this.updateExternalControlsState();
396
484
  }
397
485
 
398
486
  /**
399
- * Update the disabled state of external controls based on current slide position
487
+ * Update the disabled state of external navigation controls.
488
+ * Controls are disabled at the start/end of the carousel based on slide position.
400
489
  */
401
490
  updateExternalControlsState() {
402
491
  if (!this.carouselId || !this.instance) {
@@ -404,19 +493,21 @@ export default class Carousel {
404
493
  }
405
494
 
406
495
  const isAtStart = this.instance.isBeginning;
407
- const isAtEnd = this.instance.isEnd;
496
+ const slidesCount = this.instance.slides.length;
497
+ const slidesPerView = this.instance.params.slidesPerView as number;
498
+ const activeIndex = this.instance.activeIndex;
499
+
500
+ // Check if we've reached the end (when there are no more slides to scroll to)
501
+ const isAtEnd = activeIndex + slidesPerView >= slidesCount;
408
502
 
409
- // Find all prev controls for this carousel
410
503
  const prevControls = document.querySelectorAll(
411
504
  `[data-carousel-controls="${this.carouselId}"][data-carousel-action="prev"]`,
412
505
  );
413
506
 
414
- // Find all next controls for this carousel
415
507
  const nextControls = document.querySelectorAll(
416
508
  `[data-carousel-controls="${this.carouselId}"][data-carousel-action="next"]`,
417
509
  );
418
510
 
419
- // Update prev controls
420
511
  prevControls.forEach((control) => {
421
512
  const htmlControl = control as HTMLElement;
422
513
  if (isAtStart) {
@@ -430,7 +521,6 @@ export default class Carousel {
430
521
  }
431
522
  });
432
523
 
433
- // Update next controls
434
524
  nextControls.forEach((control) => {
435
525
  const htmlControl = control as HTMLElement;
436
526
  if (isAtEnd) {
@@ -446,7 +536,7 @@ export default class Carousel {
446
536
  }
447
537
 
448
538
  /**
449
- * Go to the next slide
539
+ * Navigate to the next slide.
450
540
  */
451
541
  slideNext() {
452
542
  if (this.instance) {
@@ -455,7 +545,7 @@ export default class Carousel {
455
545
  }
456
546
 
457
547
  /**
458
- * Go to the previous slide
548
+ * Navigate to the previous slide.
459
549
  */
460
550
  slidePrev() {
461
551
  if (this.instance) {
@@ -464,16 +554,14 @@ export default class Carousel {
464
554
  }
465
555
 
466
556
  /**
467
- * Get the current active slide index
468
- * @returns {number} The current slide index
557
+ * Get the current active slide index.
469
558
  */
470
559
  getActiveIndex(): number {
471
560
  return this.instance ? this.instance.activeIndex : 0;
472
561
  }
473
562
 
474
563
  /**
475
- * Add event listener for slide changes
476
- * @param {Function} callback - Callback function to execute on slide change
564
+ * Register a callback for slide change events.
477
565
  */
478
566
  onSlideChange(callback: (activeIndex: number) => void) {
479
567
  if (this.instance) {
@@ -518,9 +606,7 @@ export default class Carousel {
518
606
  }
519
607
 
520
608
  /**
521
- * Find carousel instance by ID or data attribute
522
- * @param {string} carouselId - The ID or data attribute value of the carousel
523
- * @returns {Carousel | null} The carousel instance or null
609
+ * Find a carousel instance by its ID or data-carousel-id attribute.
524
610
  */
525
611
  static getInstanceById(carouselId: string): Carousel | null {
526
612
  const element =