@orangesk/orange-design-system 3.10.0 → 3.12.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 (52) hide show
  1. package/build/components/Carousel/style.css +1 -1
  2. package/build/components/Carousel/style.css.map +1 -1
  3. package/build/components/Megamenu/style.css +1 -1
  4. package/build/components/Megamenu/style.css.map +1 -1
  5. package/build/components/index.js +1 -1
  6. package/build/components/index.js.map +1 -1
  7. package/build/footer-sprite-dark.svg +1 -0
  8. package/build/footer-sprite-light.svg +1 -0
  9. package/build/lib/components.css +1 -1
  10. package/build/lib/components.css.map +1 -1
  11. package/build/lib/megamenu.css +1 -1
  12. package/build/lib/megamenu.css.map +1 -1
  13. package/build/lib/scripts.js +1 -1
  14. package/build/lib/scripts.js.map +1 -1
  15. package/build/lib/style.css +1 -1
  16. package/build/lib/style.css.map +1 -1
  17. package/package.json +26 -26
  18. package/public/footer-sprite-dark.svg +1 -0
  19. package/public/footer-sprite-light.svg +1 -0
  20. package/src/assets/footer/appstore.svg +31 -0
  21. package/src/assets/footer/googleplay.svg +52 -0
  22. package/src/assets/footer/logo.svg +33 -0
  23. package/src/{components/Footer/PlanObnovy.tsx → assets/footer/plan-obnovy.svg} +53 -211
  24. package/src/assets/icons/age-rating-3.svg +0 -0
  25. package/src/assets/icons/assistent.svg +0 -0
  26. package/src/assets/icons/boxes.svg +0 -0
  27. package/src/assets/icons/business-lounge.svg +0 -0
  28. package/src/assets/icons/calendar-30.svg +0 -0
  29. package/src/assets/icons/calendar-month.svg +0 -0
  30. package/src/assets/icons/communication-settings.svg +0 -0
  31. package/src/assets/icons/cookies.svg +0 -0
  32. package/src/assets/icons/currency-euro.svg +0 -0
  33. package/src/assets/icons/data-services.svg +0 -0
  34. package/src/assets/icons/device-mobile-package.svg +0 -0
  35. package/src/assets/icons/futher-education.svg +0 -0
  36. package/src/assets/icons/modifier-delete.svg +0 -0
  37. package/src/assets/icons/orange-experts-orange-business-consultant.svg +0 -0
  38. package/src/assets/icons/orange-messenger.svg +0 -0
  39. package/src/assets/icons/orange-reflect.svg +0 -0
  40. package/src/assets/icons/personal-smartphone-trainer.svg +0 -0
  41. package/src/assets/icons/unlimited-calls.svg +0 -0
  42. package/src/components/Carousel/Carousel.static.ts +67 -16
  43. package/src/components/Carousel/styles/mixins.scss +8 -7
  44. package/src/components/Carousel/tests/Carousel.interactions.test.ts +102 -0
  45. package/src/components/Carousel/tests/Carousel.lifecycle.test.ts +223 -0
  46. package/src/components/CarouselHero/CarouselHero.static.ts +3 -0
  47. package/src/components/CarouselPromotions/CarouselPromotions.static.ts +3 -0
  48. package/src/components/Footer/Footer.tsx +44 -54
  49. package/src/components/Footer/FooterIcon.tsx +12 -0
  50. package/src/components/Footer/data.ts +4 -2
  51. package/src/components/Footer/tests/Footer.unit.test.jsx +63 -0
  52. package/src/components/Megamenu/styles/mixins.scss +2 -0
@@ -40,6 +40,9 @@ interface ExternalControlsState {
40
40
  }
41
41
 
42
42
  export const defaultConfig: SwiperOptions = {
43
+ // Keep pointer selection and click handlers available to CMS editors and integrations.
44
+ touchStartPreventDefault: false,
45
+ preventClicksPropagation: false,
43
46
  navigation: {
44
47
  nextEl: SELECTOR_NEXT,
45
48
  prevEl: SELECTOR_PREV,
@@ -96,8 +99,8 @@ export const defaultConfig: SwiperOptions = {
96
99
  };
97
100
 
98
101
  export default class Carousel {
99
- element: HTMLElement;
100
- config: SwiperOptions;
102
+ element!: HTMLElement;
103
+ config!: SwiperOptions;
101
104
  viewport!: HTMLElement;
102
105
  track!: HTMLElement;
103
106
  instance!: Swiper;
@@ -106,8 +109,9 @@ export default class Carousel {
106
109
  private externalControlsState: ExternalControlsState | null = null;
107
110
  private resizeObserver?: ResizeObserver;
108
111
  private resizeRafId?: number;
109
- private boundWindowResizeHandler: () => void;
112
+ private boundWindowResizeHandler!: () => void;
110
113
  private bleedResizeHandler?: () => void;
114
+ private destroyed = false;
111
115
  private static readonly OVERFLOW_EPSILON_PX = 1;
112
116
 
113
117
  private getViewportWrapper(): HTMLElement | null {
@@ -141,6 +145,17 @@ export default class Carousel {
141
145
  return this.track?.scrollWidth || 0;
142
146
  }
143
147
 
148
+ const sizes = this.instance.slidesSizesGrid;
149
+ const positions = this.instance.slidesGrid;
150
+ if (sizes?.length && positions?.length === sizes.length) {
151
+ const last = sizes.length - 1;
152
+ // Swiper's grid preserves fractional widths and resolved percentage gaps.
153
+ const edgesWidth = this.instance.params.centeredSlides
154
+ ? (sizes[0] + sizes[last]) / 2
155
+ : sizes[last];
156
+ return positions[last] - positions[0] + edgesWidth;
157
+ }
158
+
144
159
  const slides = Array.from(this.instance.slides) as HTMLElement[];
145
160
  const widthsSum = slides.reduce((sum, slide) => sum + slide.offsetWidth, 0);
146
161
  const spaceBetween = Number(this.instance.params?.spaceBetween) || 0;
@@ -155,17 +170,18 @@ export default class Carousel {
155
170
  return measuredWidth;
156
171
  }
157
172
 
158
- private getOverflowWidth(viewportWrapper?: HTMLElement): number {
173
+ private getOverflowWidth(viewportWrapper?: HTMLElement): number | null {
159
174
  const isBleedRight = this.isBleedRight();
160
175
  const containerWidth = isBleedRight
161
176
  ? this.element.clientWidth || viewportWrapper?.clientWidth || 0
162
177
  : this.viewport?.clientWidth || 0;
163
178
 
164
179
  if (containerWidth <= 0) {
165
- return 0;
180
+ return null;
166
181
  }
167
182
 
168
183
  const contentWidth = this.getSlidesContentWidth();
184
+ if (contentWidth <= 0) return null;
169
185
 
170
186
  return contentWidth - containerWidth;
171
187
  }
@@ -176,7 +192,7 @@ export default class Carousel {
176
192
  }
177
193
 
178
194
  const overflowWidth = this.getOverflowWidth(viewportWrapper);
179
- if (overflowWidth !== 0) {
195
+ if (overflowWidth !== null) {
180
196
  return overflowWidth > Carousel.OVERFLOW_EPSILON_PX;
181
197
  }
182
198
 
@@ -187,6 +203,8 @@ export default class Carousel {
187
203
  }
188
204
 
189
205
  constructor(element: HTMLElement, config?: Partial<SwiperOptions>) {
206
+ const existing = Carousel.getInstance(element);
207
+ if (existing) return existing;
190
208
  const scrollbarEl = element.querySelector(
191
209
  SELECTOR_SCROLLBAR,
192
210
  ) as HTMLElement | null;
@@ -227,6 +245,7 @@ export default class Carousel {
227
245
  }
228
246
 
229
247
  init() {
248
+ if (this.destroyed || this.instance) return;
230
249
  this.getElements();
231
250
 
232
251
  if (this.element.hasAttribute("data-swiper-options")) {
@@ -282,7 +301,7 @@ export default class Carousel {
282
301
 
283
302
  // Trigger a final update to notify listeners (like SameHeight) that carousel is ready
284
303
  requestAnimationFrame(() => {
285
- if (this.instance) {
304
+ if (!this.destroyed && this.instance) {
286
305
  this.instance.update();
287
306
  }
288
307
  });
@@ -319,7 +338,7 @@ export default class Carousel {
319
338
  */
320
339
  syncScrollbar() {
321
340
  requestAnimationFrame(() => {
322
- if (this.instance?.scrollbar) {
341
+ if (!this.destroyed && this.instance?.scrollbar) {
323
342
  this.instance.scrollbar.updateSize();
324
343
  }
325
344
  });
@@ -331,8 +350,12 @@ export default class Carousel {
331
350
  }
332
351
 
333
352
  this.resizeObserver?.disconnect();
353
+ const firstSlide = this.instance?.slides?.[0] as HTMLElement | undefined;
354
+ let viewportWidth = this.viewport.clientWidth;
355
+ let rootWidth = this.element.clientWidth;
356
+ let slideWidth = firstSlide?.offsetWidth;
334
357
  this.resizeObserver = new ResizeObserver(() => {
335
- if (!this.instance || !this.viewport) {
358
+ if (this.destroyed || !this.instance || !this.viewport) {
336
359
  return;
337
360
  }
338
361
 
@@ -340,14 +363,24 @@ export default class Carousel {
340
363
  return;
341
364
  }
342
365
 
343
- this.instance.update();
344
- this.updateSideArrowGeometry();
366
+ const widthChanged =
367
+ viewportWidth !== this.viewport.clientWidth ||
368
+ rootWidth !== this.element.clientWidth ||
369
+ slideWidth !== firstSlide?.offsetWidth;
370
+ if (widthChanged || this.instance.params.autoHeight) {
371
+ this.instance.update();
372
+ this.bleedResizeHandler?.();
373
+ this.instance.scrollbar?.updateSize();
374
+ }
375
+ // A height update from SameHeight must not start another Swiper update.
345
376
  this.updateCarouselEnabledState();
346
- this.instance.scrollbar?.updateSize();
377
+ viewportWidth = this.viewport.clientWidth;
378
+ rootWidth = this.element.clientWidth;
379
+ slideWidth = firstSlide?.offsetWidth;
347
380
  });
348
381
 
382
+ this.resizeObserver.observe(this.element);
349
383
  this.resizeObserver.observe(this.viewport);
350
- const firstSlide = this.instance?.slides?.[0] as HTMLElement | undefined;
351
384
  if (firstSlide) {
352
385
  this.resizeObserver.observe(firstSlide);
353
386
  }
@@ -363,7 +396,13 @@ export default class Carousel {
363
396
  const scrollbar = this.instance?.scrollbar;
364
397
  const swiper = this.instance;
365
398
 
366
- if (!viewportWrapper || !scrollbar || !scrollbar.dragEl || !swiper) {
399
+ if (
400
+ this.destroyed ||
401
+ !viewportWrapper ||
402
+ !scrollbar ||
403
+ !scrollbar.dragEl ||
404
+ !swiper
405
+ ) {
367
406
  return;
368
407
  }
369
408
 
@@ -631,7 +670,7 @@ export default class Carousel {
631
670
  */
632
671
  adjustConfigForBleedRight() {
633
672
  requestAnimationFrame(() => {
634
- if (!this.instance) return;
673
+ if (this.destroyed || !this.instance) return;
635
674
 
636
675
  const viewportWrapper = this.getViewportWrapper();
637
676
  if (!viewportWrapper) return;
@@ -976,6 +1015,8 @@ export default class Carousel {
976
1015
  }
977
1016
 
978
1017
  destroy() {
1018
+ if (this.destroyed) return;
1019
+ this.destroyed = true;
979
1020
  // Clean up external controls
980
1021
  if (this.carouselId) {
981
1022
  const controlElements = document.querySelectorAll(
@@ -990,6 +1031,13 @@ export default class Carousel {
990
1031
  );
991
1032
  delete (htmlControl as any)._carouselClickHandler;
992
1033
  }
1034
+ if ((htmlControl as any)._carouselKeydownHandler) {
1035
+ htmlControl.removeEventListener(
1036
+ "keydown",
1037
+ (htmlControl as any)._carouselKeydownHandler,
1038
+ );
1039
+ delete (htmlControl as any)._carouselKeydownHandler;
1040
+ }
993
1041
  htmlControl.removeAttribute("data-carousel-initialized");
994
1042
  });
995
1043
  }
@@ -1023,8 +1071,11 @@ export default class Carousel {
1023
1071
  }
1024
1072
 
1025
1073
  update() {
1026
- if (this.instance) {
1074
+ if (!this.destroyed && this.instance) {
1027
1075
  this.instance.update();
1076
+ this.bleedResizeHandler?.();
1077
+ this.updateCarouselEnabledState();
1078
+ this.updateExternalControlsState();
1028
1079
  }
1029
1080
  }
1030
1081
 
@@ -35,13 +35,6 @@
35
35
  min-width: 0;
36
36
  width: 100%;
37
37
  max-width: 100%;
38
- margin-bottom: convert.to-rem(30px);
39
-
40
- .carousel--scrollbar & {
41
- @include breakpoint.get("md", "down") {
42
- margin-bottom: convert.to-rem(20px);
43
- }
44
- }
45
38
 
46
39
  &.is-draggable {
47
40
  user-select: none;
@@ -56,6 +49,7 @@
56
49
  }
57
50
 
58
51
  @mixin navigation {
52
+ margin-top: convert.to-rem(30px);
59
53
  display: grid;
60
54
  grid-template-columns: minmax(0, 1fr) auto;
61
55
  align-items: center;
@@ -217,8 +211,15 @@
217
211
  }
218
212
 
219
213
  @mixin scrollbar-base {
214
+ margin-top: convert.to-rem(30px);
220
215
  position: relative;
221
216
  overflow: visible;
217
+
218
+ .carousel--scrollbar & {
219
+ @include breakpoint.get("md", "down") {
220
+ margin-top: convert.to-rem(20px);
221
+ }
222
+ }
222
223
  }
223
224
 
224
225
  @mixin scrollbar-horizontal {
@@ -0,0 +1,102 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import CarouselHero from "../../CarouselHero/CarouselHero.static";
3
+ import CarouselPromotions from "../../CarouselPromotions/CarouselPromotions.static";
4
+ import Carousel from "../Carousel.static";
5
+
6
+ describe.each([
7
+ { name: "Carousel", Module: Carousel, prefix: "carousel" },
8
+ { name: "CarouselHero", Module: CarouselHero, prefix: "carousel-hero" },
9
+ {
10
+ name: "CarouselPromotions",
11
+ Module: CarouselPromotions,
12
+ prefix: "carousel-promotions",
13
+ },
14
+ ])("$name pointer interactions with real Swiper", ({ Module, prefix }) => {
15
+ let carousel: Carousel | CarouselHero | CarouselPromotions;
16
+ let icon: SVGElement;
17
+
18
+ beforeEach(() => {
19
+ vi.useFakeTimers();
20
+ document.body.innerHTML = `<div class="${prefix}" data-${prefix}>
21
+ <div class="${prefix}__viewport-wrapper"><div class="${prefix}__viewport"><div class="${prefix}__track">
22
+ <div class="${prefix}__slide"><a href="#card"><svg><path d="M0 0h10v10z" /></svg></a></div>
23
+ <div class="${prefix}__slide">Second</div><div class="${prefix}__slide">Third</div>
24
+ </div></div></div>
25
+ </div>`;
26
+ const element = document.body.firstElementChild as HTMLElement;
27
+ for (const node of [
28
+ element,
29
+ element.querySelector(`.${prefix}__viewport`)!,
30
+ ]) {
31
+ Object.defineProperty(node, "clientWidth", { value: 600 });
32
+ Object.defineProperty(node, "clientHeight", { value: 200 });
33
+ }
34
+ carousel = new Module(element, { loop: false, breakpoints: {} });
35
+ vi.advanceTimersByTime(100);
36
+ icon = element.querySelector("path")!;
37
+ expect(carousel.instance.enabled).toBe(true);
38
+ });
39
+
40
+ afterEach(() => {
41
+ carousel?.destroy();
42
+ vi.clearAllTimers();
43
+ vi.useRealTimers();
44
+ document.body.innerHTML = "";
45
+ });
46
+
47
+ function pointer(type: string, x: number, pointerType = "mouse") {
48
+ const event = new MouseEvent(type, {
49
+ bubbles: true,
50
+ cancelable: true,
51
+ clientX: x,
52
+ clientY: 20,
53
+ button: 0,
54
+ });
55
+ Object.assign(event, { pointerId: 1, pointerType });
56
+ icon.dispatchEvent(event);
57
+ return event;
58
+ }
59
+
60
+ it.each(["mouse", "touch", "pen"])(
61
+ "allows SVG icon selection with %s without canceling pointerdown",
62
+ (pointerType) => {
63
+ const onPointerDown = vi.fn();
64
+ carousel.element.addEventListener("pointerdown", onPointerDown);
65
+ const event = pointer("pointerdown", 400, pointerType);
66
+ expect(onPointerDown).toHaveBeenCalledOnce();
67
+ expect(event.defaultPrevented).toBe(false);
68
+ },
69
+ );
70
+
71
+ it("delivers an ordinary icon click without canceling it", () => {
72
+ const onClick = vi.fn();
73
+ carousel.element.addEventListener("click", onClick);
74
+ pointer("pointerdown", 400);
75
+ pointer("pointerup", 400);
76
+ const click = new MouseEvent("click", { bubbles: true, cancelable: true });
77
+ icon.dispatchEvent(click);
78
+ expect(onClick).toHaveBeenCalledOnce();
79
+ expect(click.defaultPrevented).toBe(false);
80
+ });
81
+
82
+ it("keeps swipe navigation and link protection while letting click handlers run", () => {
83
+ const onIconClick = vi.fn();
84
+ const onClick = vi.fn();
85
+ icon.addEventListener("click", onIconClick);
86
+ carousel.element.addEventListener("click", onClick);
87
+ // The initial translation is zero; jsdom cannot parse CSS transforms with DOMMatrixReadOnly.
88
+ carousel.instance.wrapperEl.style.transform = "none";
89
+ pointer("pointerdown", 400);
90
+ pointer("pointermove", 350);
91
+ pointer("pointermove", 100);
92
+ pointer("pointerup", 100);
93
+ expect(carousel.instance.activeIndex).toBe(1);
94
+ expect(carousel.instance.animating).toBe(true);
95
+
96
+ const click = new MouseEvent("click", { bubbles: true, cancelable: true });
97
+ icon.dispatchEvent(click);
98
+ expect(onIconClick).toHaveBeenCalledOnce();
99
+ expect(onClick).toHaveBeenCalledOnce();
100
+ expect(click.defaultPrevented).toBe(true);
101
+ });
102
+ });
@@ -0,0 +1,223 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import Carousel from "../Carousel.static";
3
+
4
+ describe("Carousel lifecycle with real Swiper", () => {
5
+ let element: HTMLElement;
6
+ let carousel: Carousel;
7
+
8
+ beforeEach(() => {
9
+ vi.useFakeTimers();
10
+ vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) =>
11
+ setTimeout(() => callback(0), 16),
12
+ );
13
+ vi.stubGlobal("cancelAnimationFrame", clearTimeout);
14
+ document.body.innerHTML = `<div class="carousel" data-carousel-id="lifecycle-carousel">
15
+ <div class="carousel__viewport-wrapper"><div class="carousel__viewport"><div class="carousel__track">
16
+ <div class="carousel__slide">First</div><div class="carousel__slide">Second</div><div class="carousel__slide">Third</div>
17
+ </div></div></div>
18
+ </div><span data-carousel-controls="lifecycle-carousel" data-carousel-slide-index="1">Second</span>`;
19
+ element = document.querySelector(".carousel")!;
20
+ for (const node of [
21
+ element,
22
+ element.querySelector(".carousel__viewport")!,
23
+ ]) {
24
+ Object.defineProperty(node, "clientWidth", {
25
+ configurable: true,
26
+ value: 500,
27
+ });
28
+ Object.defineProperty(node, "clientHeight", {
29
+ configurable: true,
30
+ value: 200,
31
+ });
32
+ }
33
+ });
34
+
35
+ afterEach(() => {
36
+ carousel?.destroy();
37
+ vi.clearAllTimers();
38
+ vi.useRealTimers();
39
+ vi.unstubAllGlobals();
40
+ document.body.innerHTML = "";
41
+ });
42
+
43
+ it("does not initialize a carousel destroyed before its first frame", () => {
44
+ carousel = new Carousel(element);
45
+ carousel.destroy();
46
+ vi.advanceTimersByTime(100);
47
+ expect(element.querySelector(".swiper-initialized")).toBeNull();
48
+ expect(Carousel.getInstance(element)).toBeNull();
49
+ });
50
+
51
+ it("reuses the existing instance instead of initializing a second Swiper", () => {
52
+ carousel = new Carousel(element);
53
+ const repeated = new Carousel(element);
54
+ expect(repeated).toBe(carousel);
55
+ });
56
+
57
+ it("removes keyboard navigation handlers when destroyed", () => {
58
+ carousel = new Carousel(element);
59
+ vi.advanceTimersByTime(100);
60
+ const control = document.querySelector("[data-carousel-controls]")!;
61
+ carousel.destroy();
62
+ const event = new KeyboardEvent("keydown", {
63
+ key: "Enter",
64
+ cancelable: true,
65
+ });
66
+ control.dispatchEvent(event);
67
+ expect(event.defaultPrevented).toBe(false);
68
+ });
69
+
70
+ it("does not trigger another Swiper update for a height-only resize", () => {
71
+ const observers: {
72
+ callback: ResizeObserverCallback;
73
+ targets: Element[];
74
+ }[] = [];
75
+ vi.stubGlobal(
76
+ "ResizeObserver",
77
+ class {
78
+ record: { callback: ResizeObserverCallback; targets: Element[] };
79
+ constructor(callback: ResizeObserverCallback) {
80
+ this.record = { callback, targets: [] };
81
+ observers.push(this.record);
82
+ }
83
+ observe(target: Element) {
84
+ this.record.targets.push(target);
85
+ }
86
+ disconnect() {}
87
+ unobserve() {}
88
+ },
89
+ );
90
+ carousel = new Carousel(element);
91
+ vi.advanceTimersByTime(100);
92
+ const firstSlide = element.querySelector(".carousel__slide")!;
93
+ const observer = observers.find((o) => o.targets.includes(firstSlide))!;
94
+ const update = vi.spyOn(carousel.instance, "update");
95
+ const notify = (target: Element, width: number, height: number) =>
96
+ observer.callback(
97
+ [
98
+ {
99
+ target,
100
+ contentRect: new DOMRect(0, 0, width, height),
101
+ borderBoxSize: [],
102
+ contentBoxSize: [],
103
+ devicePixelContentBoxSize: [],
104
+ },
105
+ ],
106
+ {} as ResizeObserver,
107
+ );
108
+ notify(firstSlide, 413.333, 250);
109
+ vi.advanceTimersByTime(32);
110
+ expect(update).not.toHaveBeenCalled();
111
+ Object.defineProperty(carousel.viewport, "clientWidth", { value: 600 });
112
+ notify(carousel.viewport, 600, 250);
113
+ vi.advanceTimersByTime(32);
114
+ expect(carousel.instance.width).toBe(600);
115
+ });
116
+
117
+ it("keeps an overflowing carousel enabled with percentage spacing", () => {
118
+ element
119
+ .querySelectorAll<HTMLElement>(".carousel__slide")
120
+ .forEach((slide) => {
121
+ Object.defineProperty(slide, "offsetWidth", {
122
+ get: () => Math.round(Number.parseFloat(slide.style.width) || 0),
123
+ });
124
+ });
125
+ carousel = new Carousel(element, {
126
+ slidesPerView: 2.8,
127
+ spaceBetween: "10%",
128
+ breakpoints: {},
129
+ });
130
+ vi.advanceTimersByTime(100);
131
+ expect(carousel.instance.virtualSize).toBeGreaterThan(500);
132
+ expect(carousel.instance.enabled).toBe(true);
133
+ });
134
+
135
+ it("keeps unequal centered slides enabled when their content overflows", () => {
136
+ const slides = [
137
+ ...element.querySelectorAll<HTMLElement>(".carousel__slide"),
138
+ ];
139
+ slides[2].remove();
140
+ slides[0].style.cssText =
141
+ "width:350px;box-sizing:border-box;padding:0;margin:0";
142
+ slides[1].style.cssText =
143
+ "width:200px;box-sizing:border-box;padding:0;margin:0";
144
+ carousel = new Carousel(element, {
145
+ slidesPerView: "auto",
146
+ centeredSlides: true,
147
+ spaceBetween: 20,
148
+ breakpoints: {},
149
+ });
150
+ vi.advanceTimersByTime(100);
151
+ expect(carousel.instance.virtualSize).toBe(570);
152
+ expect(carousel.instance.enabled).toBe(true);
153
+ });
154
+
155
+ it("disables auto slides that exactly fill the viewport", () => {
156
+ const slides = [
157
+ ...element.querySelectorAll<HTMLElement>(".carousel__slide"),
158
+ ];
159
+ slides[2].remove();
160
+ slides[0].style.cssText =
161
+ "width:300px;box-sizing:border-box;padding:0;margin:0";
162
+ slides[1].style.cssText =
163
+ "width:180px;box-sizing:border-box;padding:0;margin:0";
164
+ carousel = new Carousel(element, {
165
+ slidesPerView: "auto",
166
+ spaceBetween: 20,
167
+ breakpoints: {},
168
+ });
169
+ vi.advanceTimersByTime(100);
170
+ expect(carousel.instance.virtualSize).toBe(500);
171
+ expect(carousel.instance.enabled).toBe(false);
172
+ });
173
+
174
+ it.each(["", "carousel--bleed-right", "carousel--bleed-right-no-left"])(
175
+ "updates navigation and overflow after changing slides with %s",
176
+ (variant) => {
177
+ if (variant) element.classList.add(variant);
178
+ element.classList.add("carousel--side-arrows");
179
+ element.insertAdjacentHTML(
180
+ "beforeend",
181
+ '<div class="carousel__side-arrows"></div>',
182
+ );
183
+ document.body.insertAdjacentHTML(
184
+ "beforeend",
185
+ '<button data-carousel-controls="lifecycle-carousel" data-carousel-action="next">Next</button>',
186
+ );
187
+ carousel = new Carousel(element, {
188
+ slidesPerView: 3,
189
+ spaceBetween: 20,
190
+ breakpoints: {},
191
+ });
192
+ vi.advanceTimersByTime(100);
193
+ const next = document.querySelector<HTMLButtonElement>(
194
+ '[data-carousel-action="next"]',
195
+ )!;
196
+ const arrows = element.querySelector<HTMLElement>(
197
+ ".carousel__side-arrows",
198
+ )!;
199
+ expect(carousel.instance.enabled).toBe(false);
200
+ carousel.track.insertAdjacentHTML(
201
+ "beforeend",
202
+ '<div class="carousel__slide">Fourth</div>',
203
+ );
204
+ carousel.update();
205
+ expect(carousel.instance.enabled).toBe(true);
206
+ expect(next.disabled).toBe(false);
207
+ expect(arrows.hidden).toBe(false);
208
+ if (variant)
209
+ expect(
210
+ element.style.getPropertyValue("--carousel-bleed-shell-width"),
211
+ ).not.toBe("100%");
212
+ carousel.track.lastElementChild!.remove();
213
+ carousel.update();
214
+ expect(carousel.instance.enabled).toBe(false);
215
+ expect(next.disabled).toBe(true);
216
+ expect(arrows.hidden).toBe(true);
217
+ if (variant)
218
+ expect(
219
+ element.style.getPropertyValue("--carousel-bleed-shell-width"),
220
+ ).toBe("100%");
221
+ },
222
+ );
223
+ });
@@ -27,6 +27,9 @@ import {
27
27
  } from "./constants";
28
28
 
29
29
  export const defaultConfig: SwiperOptions = {
30
+ // Keep pointer selection and click handlers available to CMS editors and integrations.
31
+ touchStartPreventDefault: false,
32
+ preventClicksPropagation: false,
30
33
  pagination: {
31
34
  clickable: true,
32
35
  bulletClass: CLASS_PAGINATION_ITEM,
@@ -22,6 +22,9 @@ import {
22
22
  } from "./constants";
23
23
 
24
24
  export const defaultConfig: SwiperOptions = {
25
+ // Keep pointer selection and click handlers available to CMS editors and integrations.
26
+ touchStartPreventDefault: false,
27
+ preventClicksPropagation: false,
25
28
  pagination: {
26
29
  el: SELECTOR_DOTS,
27
30
  clickable: true,