@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.
- package/build/components/Carousel/style.css +1 -1
- package/build/components/Carousel/style.css.map +1 -1
- package/build/components/Megamenu/style.css +1 -1
- package/build/components/Megamenu/style.css.map +1 -1
- package/build/components/index.js +1 -1
- package/build/components/index.js.map +1 -1
- package/build/footer-sprite-dark.svg +1 -0
- package/build/footer-sprite-light.svg +1 -0
- package/build/lib/components.css +1 -1
- package/build/lib/components.css.map +1 -1
- package/build/lib/megamenu.css +1 -1
- package/build/lib/megamenu.css.map +1 -1
- package/build/lib/scripts.js +1 -1
- package/build/lib/scripts.js.map +1 -1
- package/build/lib/style.css +1 -1
- package/build/lib/style.css.map +1 -1
- package/package.json +26 -26
- package/public/footer-sprite-dark.svg +1 -0
- package/public/footer-sprite-light.svg +1 -0
- package/src/assets/footer/appstore.svg +31 -0
- package/src/assets/footer/googleplay.svg +52 -0
- package/src/assets/footer/logo.svg +33 -0
- package/src/{components/Footer/PlanObnovy.tsx → assets/footer/plan-obnovy.svg} +53 -211
- package/src/assets/icons/age-rating-3.svg +0 -0
- package/src/assets/icons/assistent.svg +0 -0
- package/src/assets/icons/boxes.svg +0 -0
- package/src/assets/icons/business-lounge.svg +0 -0
- package/src/assets/icons/calendar-30.svg +0 -0
- package/src/assets/icons/calendar-month.svg +0 -0
- package/src/assets/icons/communication-settings.svg +0 -0
- package/src/assets/icons/cookies.svg +0 -0
- package/src/assets/icons/currency-euro.svg +0 -0
- package/src/assets/icons/data-services.svg +0 -0
- package/src/assets/icons/device-mobile-package.svg +0 -0
- package/src/assets/icons/futher-education.svg +0 -0
- package/src/assets/icons/modifier-delete.svg +0 -0
- package/src/assets/icons/orange-experts-orange-business-consultant.svg +0 -0
- package/src/assets/icons/orange-messenger.svg +0 -0
- package/src/assets/icons/orange-reflect.svg +0 -0
- package/src/assets/icons/personal-smartphone-trainer.svg +0 -0
- package/src/assets/icons/unlimited-calls.svg +0 -0
- package/src/components/Carousel/Carousel.static.ts +67 -16
- package/src/components/Carousel/styles/mixins.scss +8 -7
- package/src/components/Carousel/tests/Carousel.interactions.test.ts +102 -0
- package/src/components/Carousel/tests/Carousel.lifecycle.test.ts +223 -0
- package/src/components/CarouselHero/CarouselHero.static.ts +3 -0
- package/src/components/CarouselPromotions/CarouselPromotions.static.ts +3 -0
- package/src/components/Footer/Footer.tsx +44 -54
- package/src/components/Footer/FooterIcon.tsx +12 -0
- package/src/components/Footer/data.ts +4 -2
- package/src/components/Footer/tests/Footer.unit.test.jsx +63 -0
- 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
|
|
100
|
-
config
|
|
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
|
|
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
|
|
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 !==
|
|
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
|
-
|
|
344
|
-
|
|
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.
|
|
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 (
|
|
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,
|