@orangesk/orange-design-system 2.0.0-beta.10 → 2.0.0-beta.12
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/index.js +5 -5
- package/build/components/index.js.map +1 -1
- package/build/components/tsconfig.tsbuildinfo +1 -1
- package/build/components/types/index.d.ts +8 -5
- package/build/components/types/src/components/Carousel/Carousel.static.d.ts +34 -1
- package/build/components/types/src/components/Cover/Cover.d.ts +3 -3
- package/build/components/types/src/components/Pill/Pill.d.ts +1 -1
- package/build/components/types/src/components/PromoBanner/PromoBanner.d.ts +1 -1
- package/build/components/types/src/components/Table/Table.d.ts +2 -0
- package/build/components/types/src/components/Table/docsData.d.ts +2 -0
- package/build/components/types/src/components/Table/types.d.ts +1 -0
- package/build/components/types/src/components/Tile/Tile.d.ts +2 -1
- package/build/lib/after-components.css +1 -1
- package/build/lib/after-components.css.map +1 -1
- package/build/lib/before-components.css +1 -1
- package/build/lib/before-components.css.map +1 -1
- 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 +4 -4
- package/build/lib/scripts.js.map +1 -1
- package/build/lib/style.css +1 -1
- package/build/lib/style.css.map +1 -1
- package/build/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -13
- package/src/components/Accordion/styles/mixins.scss +5 -1
- package/src/components/AnchorNavigation/AnchorNavigation.static.ts +3 -0
- package/src/components/AnchorNavigation/styles/mixins.scss +1 -1
- package/src/components/Button/styles/config.scss +5 -4
- package/src/components/Carousel/Carousel.static.ts +204 -1
- package/src/components/Carousel/tests/Carousel.static.test.js +403 -0
- package/src/components/Carousel/tests/Carousel.unit.test.js +68 -0
- package/src/components/Cover/Cover.tsx +22 -20
- package/src/components/Cover/styles/config.scss +23 -12
- package/src/components/Cover/styles/mixins.scss +6 -5
- package/src/components/Cover/tests/Cover.unit.test.js +52 -52
- package/src/components/Expander/styles/style.scss +3 -1
- package/src/components/Forms/Group/styles/mixins.scss +14 -0
- package/src/components/Pill/Pill.tsx +10 -2
- package/src/components/Pill/styles/config.scss +2 -21
- package/src/components/Pill/styles/style.scss +16 -6
- package/src/components/Pill/tests/Pill.conformance.test.js +8 -2
- package/src/components/Pill/tests/Pill.unit.test.js +69 -11
- package/src/components/PromoBanner/PromoBanner.tsx +19 -2
- package/src/components/PromoBanner/styles/mixins.scss +1 -1
- package/src/components/PromoBanner/tests/PromoBanner.conformance.test.js +32 -0
- package/src/components/PromoBanner/tests/PromoBanner.unit.test.js +52 -0
- package/src/components/Table/Row.tsx +9 -3
- package/src/components/Table/Table.tsx +11 -1
- package/src/components/Table/TableContext.ts +1 -0
- package/src/components/Table/docsData.ts +25 -0
- package/src/components/Table/styles/mixins.scss +40 -4
- package/src/components/Table/styles/style.scss +6 -0
- package/src/components/Table/types.ts +1 -0
- package/src/components/Tile/CHANGELOG.md +15 -1
- package/src/components/Tile/Tile.tsx +11 -3
- package/src/components/Tile/styles/config.scss +0 -11
- package/src/components/Tile/styles/style.scss +0 -4
- package/src/components/Tile/tests/Tile.unit.test.js +10 -3
- package/src/components/Tooltip/InfoTooltip.tsx +1 -5
- package/src/styles/base/globals.scss +7 -2
- package/src/styles/tokens/color.scss +1 -1
- package/src/styles/typography/mixins.scss +18 -0
- package/src/styles/typography/style.scss +6 -3
- package/src/styles/utilities/text.scss +1 -0
|
@@ -84,6 +84,7 @@ export default class Carousel {
|
|
|
84
84
|
viewport!: HTMLElement;
|
|
85
85
|
track!: HTMLElement;
|
|
86
86
|
instance!: Swiper;
|
|
87
|
+
carouselId?: string;
|
|
87
88
|
|
|
88
89
|
constructor(element: HTMLElement, config?: Partial<SwiperOptions>) {
|
|
89
90
|
this.element = element;
|
|
@@ -123,6 +124,10 @@ export default class Carousel {
|
|
|
123
124
|
modules: [Navigation, Pagination, Scrollbar, A11y, Keyboard],
|
|
124
125
|
on: {
|
|
125
126
|
slideChange: this.handleSlideChange,
|
|
127
|
+
slideChangeTransitionEnd: () => {
|
|
128
|
+
// Update external controls after transition completes
|
|
129
|
+
this.updateExternalControlsState();
|
|
130
|
+
},
|
|
126
131
|
},
|
|
127
132
|
});
|
|
128
133
|
|
|
@@ -130,6 +135,9 @@ export default class Carousel {
|
|
|
130
135
|
if (this.element.classList.contains(CLASS_BLEED_RIGHT)) {
|
|
131
136
|
this.adjustConfigForBleedRight();
|
|
132
137
|
}
|
|
138
|
+
|
|
139
|
+
// Initialize external controls
|
|
140
|
+
this.initExternalControls();
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
getElements() {
|
|
@@ -241,7 +249,7 @@ export default class Carousel {
|
|
|
241
249
|
/**
|
|
242
250
|
* Handles the slide change event on the carousel.
|
|
243
251
|
* Updates the tooltip position for the active slide and hides tooltips for non-active slides.
|
|
244
|
-
* Also updates accessibility attributes for pagination buttons.
|
|
252
|
+
* Also updates accessibility attributes for pagination buttons and external controls state.
|
|
245
253
|
*/
|
|
246
254
|
handleSlideChange() {
|
|
247
255
|
const activeSlide = this.track.querySelector(SELECTOR_ACTIVE);
|
|
@@ -256,6 +264,9 @@ export default class Carousel {
|
|
|
256
264
|
if (nonActiveSlides.length > 0) {
|
|
257
265
|
this.hideAllTooltips(nonActiveSlides);
|
|
258
266
|
}
|
|
267
|
+
|
|
268
|
+
// Update external controls state based on current position
|
|
269
|
+
this.updateExternalControlsState();
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
/**
|
|
@@ -309,7 +320,187 @@ export default class Carousel {
|
|
|
309
320
|
});
|
|
310
321
|
}
|
|
311
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Initialize external controls that reference this carousel
|
|
325
|
+
*/
|
|
326
|
+
initExternalControls() {
|
|
327
|
+
// Get carousel ID from data-carousel-id or element id
|
|
328
|
+
const carouselId = this.element.dataset.carouselId || this.element.id;
|
|
329
|
+
|
|
330
|
+
if (!carouselId) {
|
|
331
|
+
return; // No ID to reference, skip external controls
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Store carousel ID for later use
|
|
335
|
+
this.carouselId = carouselId;
|
|
336
|
+
|
|
337
|
+
// Find all elements with data-carousel-controls matching this carousel's ID
|
|
338
|
+
const controlElements = document.querySelectorAll(
|
|
339
|
+
`[data-carousel-controls="${carouselId}"]`,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
controlElements.forEach((control) => {
|
|
343
|
+
const htmlControl = control as HTMLElement;
|
|
344
|
+
|
|
345
|
+
// Skip if already initialized to avoid duplicate event listeners
|
|
346
|
+
if (htmlControl.hasAttribute("data-carousel-initialized")) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Remove any existing event listener first (just in case)
|
|
351
|
+
if ((htmlControl as any)._carouselClickHandler) {
|
|
352
|
+
htmlControl.removeEventListener(
|
|
353
|
+
"click",
|
|
354
|
+
(htmlControl as any)._carouselClickHandler,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const action = htmlControl.dataset.carouselAction;
|
|
359
|
+
|
|
360
|
+
// Create bound event handler to avoid multiple listeners
|
|
361
|
+
const clickHandler = (e: Event) => {
|
|
362
|
+
e.preventDefault();
|
|
363
|
+
if (htmlControl.hasAttribute("disabled")) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (action === "next") {
|
|
367
|
+
this.slideNext();
|
|
368
|
+
} else if (action === "prev") {
|
|
369
|
+
this.slidePrev();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
if (action === "next" || action === "prev") {
|
|
374
|
+
htmlControl.addEventListener("click", clickHandler);
|
|
375
|
+
|
|
376
|
+
// Add ARIA attributes for accessibility
|
|
377
|
+
htmlControl.setAttribute(
|
|
378
|
+
"aria-label",
|
|
379
|
+
action === "next"
|
|
380
|
+
? this.config.a11y?.nextSlideMessage || "Nasledujúci snímok"
|
|
381
|
+
: this.config.a11y?.prevSlideMessage || "Predchádzajúci snímok",
|
|
382
|
+
);
|
|
383
|
+
htmlControl.setAttribute("type", "button");
|
|
384
|
+
|
|
385
|
+
// Store the handler reference for potential cleanup
|
|
386
|
+
(htmlControl as any)._carouselClickHandler = clickHandler;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Mark as initialized to avoid duplicate event listeners
|
|
390
|
+
htmlControl.setAttribute("data-carousel-initialized", "true");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Update initial state of external controls
|
|
394
|
+
this.updateExternalControlsState();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Update the disabled state of external controls based on current slide position
|
|
399
|
+
*/
|
|
400
|
+
updateExternalControlsState() {
|
|
401
|
+
if (!this.carouselId || !this.instance) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const isAtStart = this.instance.isBeginning;
|
|
406
|
+
const isAtEnd = this.instance.isEnd;
|
|
407
|
+
|
|
408
|
+
// Find all prev controls for this carousel
|
|
409
|
+
const prevControls = document.querySelectorAll(
|
|
410
|
+
`[data-carousel-controls="${this.carouselId}"][data-carousel-action="prev"]`,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Find all next controls for this carousel
|
|
414
|
+
const nextControls = document.querySelectorAll(
|
|
415
|
+
`[data-carousel-controls="${this.carouselId}"][data-carousel-action="next"]`,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Update prev controls
|
|
419
|
+
prevControls.forEach((control) => {
|
|
420
|
+
const htmlControl = control as HTMLElement;
|
|
421
|
+
if (isAtStart) {
|
|
422
|
+
htmlControl.setAttribute("disabled", "true");
|
|
423
|
+
htmlControl.setAttribute("aria-disabled", "true");
|
|
424
|
+
htmlControl.style.cursor = "not-allowed";
|
|
425
|
+
} else {
|
|
426
|
+
htmlControl.removeAttribute("disabled");
|
|
427
|
+
htmlControl.setAttribute("aria-disabled", "false");
|
|
428
|
+
htmlControl.style.cursor = "pointer";
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Update next controls
|
|
433
|
+
nextControls.forEach((control) => {
|
|
434
|
+
const htmlControl = control as HTMLElement;
|
|
435
|
+
if (isAtEnd) {
|
|
436
|
+
htmlControl.setAttribute("disabled", "true");
|
|
437
|
+
htmlControl.setAttribute("aria-disabled", "true");
|
|
438
|
+
htmlControl.style.cursor = "not-allowed";
|
|
439
|
+
} else {
|
|
440
|
+
htmlControl.removeAttribute("disabled");
|
|
441
|
+
htmlControl.setAttribute("aria-disabled", "false");
|
|
442
|
+
htmlControl.style.cursor = "pointer";
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Go to the next slide
|
|
449
|
+
*/
|
|
450
|
+
slideNext() {
|
|
451
|
+
if (this.instance) {
|
|
452
|
+
this.instance.slideNext();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Go to the previous slide
|
|
458
|
+
*/
|
|
459
|
+
slidePrev() {
|
|
460
|
+
if (this.instance) {
|
|
461
|
+
this.instance.slidePrev();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get the current active slide index
|
|
467
|
+
* @returns {number} The current slide index
|
|
468
|
+
*/
|
|
469
|
+
getActiveIndex(): number {
|
|
470
|
+
return this.instance ? this.instance.activeIndex : 0;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Add event listener for slide changes
|
|
475
|
+
* @param {Function} callback - Callback function to execute on slide change
|
|
476
|
+
*/
|
|
477
|
+
onSlideChange(callback: (activeIndex: number) => void) {
|
|
478
|
+
if (this.instance) {
|
|
479
|
+
this.instance.on("slideChange", () => {
|
|
480
|
+
callback(this.instance.activeIndex);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
312
485
|
destroy() {
|
|
486
|
+
// Clean up external controls
|
|
487
|
+
if (this.carouselId) {
|
|
488
|
+
const controlElements = document.querySelectorAll(
|
|
489
|
+
`[data-carousel-controls="${this.carouselId}"]`,
|
|
490
|
+
);
|
|
491
|
+
controlElements.forEach((control) => {
|
|
492
|
+
const htmlControl = control as HTMLElement;
|
|
493
|
+
if ((htmlControl as any)._carouselClickHandler) {
|
|
494
|
+
htmlControl.removeEventListener(
|
|
495
|
+
"click",
|
|
496
|
+
(htmlControl as any)._carouselClickHandler,
|
|
497
|
+
);
|
|
498
|
+
delete (htmlControl as any)._carouselClickHandler;
|
|
499
|
+
}
|
|
500
|
+
htmlControl.removeAttribute("data-carousel-initialized");
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
313
504
|
if (this.instance) {
|
|
314
505
|
this.instance.destroy();
|
|
315
506
|
}
|
|
@@ -324,4 +515,16 @@ export default class Carousel {
|
|
|
324
515
|
static getInstance(el: HTMLElement): Carousel | null {
|
|
325
516
|
return el && (el as any).ODS_Carousel ? (el as any).ODS_Carousel : null;
|
|
326
517
|
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Find carousel instance by ID or data attribute
|
|
521
|
+
* @param {string} carouselId - The ID or data attribute value of the carousel
|
|
522
|
+
* @returns {Carousel | null} The carousel instance or null
|
|
523
|
+
*/
|
|
524
|
+
static getInstanceById(carouselId: string): Carousel | null {
|
|
525
|
+
const element =
|
|
526
|
+
document.querySelector(`[data-carousel-id="${carouselId}"]`) ||
|
|
527
|
+
document.getElementById(carouselId);
|
|
528
|
+
return element ? this.getInstance(element as HTMLElement) : null;
|
|
529
|
+
}
|
|
327
530
|
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Swiper } from "swiper";
|
|
6
|
+
import Carousel from "../Carousel.static";
|
|
7
|
+
|
|
8
|
+
// Mock Swiper
|
|
9
|
+
jest.mock("swiper", () => ({
|
|
10
|
+
Swiper: jest.fn().mockImplementation(() => ({
|
|
11
|
+
slideNext: jest.fn(),
|
|
12
|
+
slidePrev: jest.fn(),
|
|
13
|
+
destroy: jest.fn(),
|
|
14
|
+
update: jest.fn(),
|
|
15
|
+
on: jest.fn(),
|
|
16
|
+
activeIndex: 0,
|
|
17
|
+
isBeginning: true,
|
|
18
|
+
isEnd: false,
|
|
19
|
+
})),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock Swiper modules
|
|
23
|
+
jest.mock("swiper/modules", () => ({
|
|
24
|
+
Navigation: {},
|
|
25
|
+
Pagination: {},
|
|
26
|
+
Scrollbar: {},
|
|
27
|
+
A11y: {},
|
|
28
|
+
Keyboard: {},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
describe("Carousel Static - External Controls", () => {
|
|
32
|
+
let carouselElement;
|
|
33
|
+
let carouselInstance;
|
|
34
|
+
let mockSwiperInstance;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
// Clear all mocks
|
|
38
|
+
jest.clearAllMocks();
|
|
39
|
+
|
|
40
|
+
// Create mock Swiper instance
|
|
41
|
+
mockSwiperInstance = {
|
|
42
|
+
slideNext: jest.fn(),
|
|
43
|
+
slidePrev: jest.fn(),
|
|
44
|
+
destroy: jest.fn(),
|
|
45
|
+
update: jest.fn(),
|
|
46
|
+
on: jest.fn(),
|
|
47
|
+
activeIndex: 0,
|
|
48
|
+
isBeginning: true,
|
|
49
|
+
isEnd: false,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
Swiper.mockImplementation(() => mockSwiperInstance);
|
|
53
|
+
|
|
54
|
+
// Set up DOM
|
|
55
|
+
document.body.innerHTML = `
|
|
56
|
+
<div class="carousel" data-carousel-id="test-carousel" id="test-carousel">
|
|
57
|
+
<div class="carousel__viewport">
|
|
58
|
+
<div class="carousel__track">
|
|
59
|
+
<div class="carousel__slide">Slide 1</div>
|
|
60
|
+
<div class="carousel__slide">Slide 2</div>
|
|
61
|
+
<div class="carousel__slide">Slide 3</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="carousel__pagination"></div>
|
|
65
|
+
</div>
|
|
66
|
+
<button data-carousel-controls="test-carousel" data-carousel-action="prev" id="prev-btn">Previous</button>
|
|
67
|
+
<button data-carousel-controls="test-carousel" data-carousel-action="next" id="next-btn">Next</button>
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
carouselElement = document.querySelector(".carousel");
|
|
71
|
+
|
|
72
|
+
// Mock requestAnimationFrame to execute synchronously
|
|
73
|
+
global.requestAnimationFrame = jest.fn((cb) => cb());
|
|
74
|
+
|
|
75
|
+
carouselInstance = new Carousel(carouselElement);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
document.body.innerHTML = "";
|
|
80
|
+
// Restore requestAnimationFrame
|
|
81
|
+
global.requestAnimationFrame = undefined;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("External Controls Initialization", () => {
|
|
85
|
+
it("should find and initialize external controls", () => {
|
|
86
|
+
const prevButton = document.getElementById("prev-btn");
|
|
87
|
+
const nextButton = document.getElementById("next-btn");
|
|
88
|
+
|
|
89
|
+
expect(prevButton.hasAttribute("data-carousel-initialized")).toBe(true);
|
|
90
|
+
expect(nextButton.hasAttribute("data-carousel-initialized")).toBe(true);
|
|
91
|
+
expect(prevButton.getAttribute("aria-label")).toBe(
|
|
92
|
+
"Predchádzajúci snímok",
|
|
93
|
+
);
|
|
94
|
+
expect(nextButton.getAttribute("aria-label")).toBe("Nasledujúci snímok");
|
|
95
|
+
expect(prevButton.getAttribute("type")).toBe("button");
|
|
96
|
+
expect(nextButton.getAttribute("type")).toBe("button");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should not initialize controls without carousel ID", () => {
|
|
100
|
+
// Create carousel without ID
|
|
101
|
+
document.body.innerHTML = `
|
|
102
|
+
<div class="carousel">
|
|
103
|
+
<div class="carousel__viewport">
|
|
104
|
+
<div class="carousel__track">
|
|
105
|
+
<div class="carousel__slide">Slide 1</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<button data-carousel-controls="missing-carousel" data-carousel-action="next">Next</button>
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const element = document.querySelector(".carousel");
|
|
113
|
+
new Carousel(element);
|
|
114
|
+
const button = document.querySelector("button");
|
|
115
|
+
|
|
116
|
+
expect(button.hasAttribute("data-carousel-initialized")).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should skip already initialized controls", () => {
|
|
120
|
+
// Mark button as already initialized
|
|
121
|
+
const prevButton = document.getElementById("prev-btn");
|
|
122
|
+
prevButton.setAttribute("data-carousel-initialized", "true");
|
|
123
|
+
|
|
124
|
+
// Create new carousel instance
|
|
125
|
+
new Carousel(carouselElement);
|
|
126
|
+
|
|
127
|
+
// Should not double-initialize
|
|
128
|
+
expect(prevButton.getAttribute("data-carousel-initialized")).toBe("true");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("External Controls Navigation", () => {
|
|
133
|
+
it("should call slideNext when next button is clicked", () => {
|
|
134
|
+
const nextButton = document.getElementById("next-btn");
|
|
135
|
+
nextButton.click();
|
|
136
|
+
|
|
137
|
+
expect(mockSwiperInstance.slideNext).toHaveBeenCalledTimes(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should call slidePrev when prev button is clicked", () => {
|
|
141
|
+
// Ensure button is not disabled by setting carousel state
|
|
142
|
+
mockSwiperInstance.isBeginning = false;
|
|
143
|
+
mockSwiperInstance.isEnd = false;
|
|
144
|
+
carouselInstance.updateExternalControlsState();
|
|
145
|
+
|
|
146
|
+
const prevButton = document.getElementById("prev-btn");
|
|
147
|
+
prevButton.click();
|
|
148
|
+
|
|
149
|
+
expect(mockSwiperInstance.slidePrev).toHaveBeenCalledTimes(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should prevent default behavior on button click", () => {
|
|
153
|
+
const nextButton = document.getElementById("next-btn");
|
|
154
|
+
const mockEvent = new Event("click");
|
|
155
|
+
mockEvent.preventDefault = jest.fn();
|
|
156
|
+
|
|
157
|
+
nextButton.dispatchEvent(mockEvent);
|
|
158
|
+
|
|
159
|
+
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should not navigate when button is disabled", () => {
|
|
163
|
+
const nextButton = document.getElementById("next-btn");
|
|
164
|
+
nextButton.setAttribute("disabled", "true");
|
|
165
|
+
|
|
166
|
+
nextButton.click();
|
|
167
|
+
|
|
168
|
+
expect(mockSwiperInstance.slideNext).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("External Controls State Management", () => {
|
|
173
|
+
it("should disable prev button when at beginning", () => {
|
|
174
|
+
mockSwiperInstance.isBeginning = true;
|
|
175
|
+
mockSwiperInstance.isEnd = false;
|
|
176
|
+
|
|
177
|
+
carouselInstance.updateExternalControlsState();
|
|
178
|
+
|
|
179
|
+
const prevButton = document.getElementById("prev-btn");
|
|
180
|
+
const nextButton = document.getElementById("next-btn");
|
|
181
|
+
|
|
182
|
+
expect(prevButton.hasAttribute("disabled")).toBe(true);
|
|
183
|
+
expect(prevButton.getAttribute("aria-disabled")).toBe("true");
|
|
184
|
+
|
|
185
|
+
expect(nextButton.hasAttribute("disabled")).toBe(false);
|
|
186
|
+
expect(nextButton.getAttribute("aria-disabled")).toBe("false");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should disable next button when at end", () => {
|
|
190
|
+
mockSwiperInstance.isBeginning = false;
|
|
191
|
+
mockSwiperInstance.isEnd = true;
|
|
192
|
+
|
|
193
|
+
carouselInstance.updateExternalControlsState();
|
|
194
|
+
|
|
195
|
+
const prevButton = document.getElementById("prev-btn");
|
|
196
|
+
const nextButton = document.getElementById("next-btn");
|
|
197
|
+
|
|
198
|
+
expect(nextButton.hasAttribute("disabled")).toBe(true);
|
|
199
|
+
expect(nextButton.getAttribute("aria-disabled")).toBe("true");
|
|
200
|
+
|
|
201
|
+
expect(prevButton.hasAttribute("disabled")).toBe(false);
|
|
202
|
+
expect(prevButton.getAttribute("aria-disabled")).toBe("false");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should enable both buttons when in middle", () => {
|
|
206
|
+
mockSwiperInstance.isBeginning = false;
|
|
207
|
+
mockSwiperInstance.isEnd = false;
|
|
208
|
+
|
|
209
|
+
carouselInstance.updateExternalControlsState();
|
|
210
|
+
|
|
211
|
+
const prevButton = document.getElementById("prev-btn");
|
|
212
|
+
const nextButton = document.getElementById("next-btn");
|
|
213
|
+
|
|
214
|
+
expect(prevButton.hasAttribute("disabled")).toBe(false);
|
|
215
|
+
expect(nextButton.hasAttribute("disabled")).toBe(false);
|
|
216
|
+
expect(prevButton.getAttribute("aria-disabled")).toBe("false");
|
|
217
|
+
expect(nextButton.getAttribute("aria-disabled")).toBe("false");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should update controls state on slide change transition end", () => {
|
|
221
|
+
const updateStatesSpy = jest.spyOn(
|
|
222
|
+
carouselInstance,
|
|
223
|
+
"updateExternalControlsState",
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Get the slideChangeTransitionEnd callback from Swiper initialization
|
|
227
|
+
const swiperOnCallArgs = mockSwiperInstance.on.mock.calls.find(
|
|
228
|
+
(call) => call[0] === "slideChangeTransitionEnd",
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (swiperOnCallArgs) {
|
|
232
|
+
const callback = swiperOnCallArgs[1];
|
|
233
|
+
callback();
|
|
234
|
+
expect(updateStatesSpy).toHaveBeenCalled();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("Multiple Control Sets", () => {
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
document.body.innerHTML = `
|
|
242
|
+
<div class="carousel" data-carousel-id="test-carousel">
|
|
243
|
+
<div class="carousel__viewport">
|
|
244
|
+
<div class="carousel__track">
|
|
245
|
+
<div class="carousel__slide">Slide 1</div>
|
|
246
|
+
<div class="carousel__slide">Slide 2</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
<button data-carousel-controls="test-carousel" data-carousel-action="prev" id="prev-btn-1">Prev 1</button>
|
|
251
|
+
<button data-carousel-controls="test-carousel" data-carousel-action="next" id="next-btn-1">Next 1</button>
|
|
252
|
+
<button data-carousel-controls="test-carousel" data-carousel-action="prev" id="prev-btn-2">Prev 2</button>
|
|
253
|
+
<button data-carousel-controls="test-carousel" data-carousel-action="next" id="next-btn-2">Next 2</button>
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
carouselElement = document.querySelector(".carousel");
|
|
257
|
+
|
|
258
|
+
// Mock requestAnimationFrame to execute synchronously
|
|
259
|
+
global.requestAnimationFrame = jest.fn((cb) => cb());
|
|
260
|
+
|
|
261
|
+
carouselInstance = new Carousel(carouselElement);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should initialize multiple control sets", () => {
|
|
265
|
+
const buttons = document.querySelectorAll(
|
|
266
|
+
'[data-carousel-controls="test-carousel"]',
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
buttons.forEach((button) => {
|
|
270
|
+
expect(button.hasAttribute("data-carousel-initialized")).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should update state for all control sets", () => {
|
|
275
|
+
mockSwiperInstance.isBeginning = true;
|
|
276
|
+
mockSwiperInstance.isEnd = false;
|
|
277
|
+
|
|
278
|
+
carouselInstance.updateExternalControlsState();
|
|
279
|
+
|
|
280
|
+
const prevButtons = document.querySelectorAll(
|
|
281
|
+
'[data-carousel-action="prev"]',
|
|
282
|
+
);
|
|
283
|
+
const nextButtons = document.querySelectorAll(
|
|
284
|
+
'[data-carousel-action="next"]',
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
prevButtons.forEach((button) => {
|
|
288
|
+
expect(button.hasAttribute("disabled")).toBe(true);
|
|
289
|
+
expect(button.getAttribute("aria-disabled")).toBe("true");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
nextButtons.forEach((button) => {
|
|
293
|
+
expect(button.hasAttribute("disabled")).toBe(false);
|
|
294
|
+
expect(button.getAttribute("aria-disabled")).toBe("false");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("Cleanup", () => {
|
|
300
|
+
it("should clean up external controls on destroy", () => {
|
|
301
|
+
const prevButton = document.getElementById("prev-btn");
|
|
302
|
+
const nextButton = document.getElementById("next-btn");
|
|
303
|
+
|
|
304
|
+
// Verify controls are initialized
|
|
305
|
+
expect(prevButton.hasAttribute("data-carousel-initialized")).toBe(true);
|
|
306
|
+
expect(nextButton.hasAttribute("data-carousel-initialized")).toBe(true);
|
|
307
|
+
|
|
308
|
+
// Destroy carousel
|
|
309
|
+
carouselInstance.destroy();
|
|
310
|
+
|
|
311
|
+
// Verify cleanup
|
|
312
|
+
expect(prevButton.hasAttribute("data-carousel-initialized")).toBe(false);
|
|
313
|
+
expect(nextButton.hasAttribute("data-carousel-initialized")).toBe(false);
|
|
314
|
+
expect(mockSwiperInstance.destroy).toHaveBeenCalled();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("should remove event listeners on destroy", () => {
|
|
318
|
+
const nextButton = document.getElementById("next-btn");
|
|
319
|
+
|
|
320
|
+
// Store reference to click handler
|
|
321
|
+
const clickHandler = nextButton._carouselClickHandler;
|
|
322
|
+
expect(clickHandler).toBeDefined();
|
|
323
|
+
|
|
324
|
+
carouselInstance.destroy();
|
|
325
|
+
|
|
326
|
+
// Handler reference should be removed
|
|
327
|
+
expect(nextButton._carouselClickHandler).toBeUndefined();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("Static Methods", () => {
|
|
332
|
+
it("should get instance by ID", () => {
|
|
333
|
+
const foundInstance = Carousel.getInstanceById("test-carousel");
|
|
334
|
+
expect(foundInstance).toBe(carouselInstance);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should get instance by data-carousel-id", () => {
|
|
338
|
+
document.body.innerHTML = `
|
|
339
|
+
<div class="carousel" data-carousel-id="data-id-carousel">
|
|
340
|
+
<div class="carousel__viewport">
|
|
341
|
+
<div class="carousel__track">
|
|
342
|
+
<div class="carousel__slide">Slide 1</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
`;
|
|
347
|
+
|
|
348
|
+
const element = document.querySelector(".carousel");
|
|
349
|
+
const instance = new Carousel(element);
|
|
350
|
+
const foundInstance = Carousel.getInstanceById("data-id-carousel");
|
|
351
|
+
|
|
352
|
+
expect(foundInstance).toBe(instance);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("should return null for non-existent carousel", () => {
|
|
356
|
+
const foundInstance = Carousel.getInstanceById("non-existent");
|
|
357
|
+
expect(foundInstance).toBe(null);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("Edge Cases", () => {
|
|
362
|
+
it("should handle controls without valid action", () => {
|
|
363
|
+
document.body.innerHTML = `
|
|
364
|
+
<div class="carousel" data-carousel-id="test-carousel">
|
|
365
|
+
<div class="carousel__viewport">
|
|
366
|
+
<div class="carousel__track">
|
|
367
|
+
<div class="carousel__slide">Slide 1</div>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
<button data-carousel-controls="test-carousel" data-carousel-action="invalid">Invalid</button>
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
const element = document.querySelector(".carousel");
|
|
375
|
+
new Carousel(element);
|
|
376
|
+
|
|
377
|
+
const button = document.querySelector("button");
|
|
378
|
+
button.click();
|
|
379
|
+
|
|
380
|
+
// Should not crash and should not call slideNext/slidePrev
|
|
381
|
+
expect(mockSwiperInstance.slideNext).not.toHaveBeenCalled();
|
|
382
|
+
expect(mockSwiperInstance.slidePrev).not.toHaveBeenCalled();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("should handle updateExternalControlsState without instance", () => {
|
|
386
|
+
carouselInstance.instance = null;
|
|
387
|
+
|
|
388
|
+
// Should not crash
|
|
389
|
+
expect(() => {
|
|
390
|
+
carouselInstance.updateExternalControlsState();
|
|
391
|
+
}).not.toThrow();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("should handle updateExternalControlsState without carouselId", () => {
|
|
395
|
+
carouselInstance.carouselId = null;
|
|
396
|
+
|
|
397
|
+
// Should not crash
|
|
398
|
+
expect(() => {
|
|
399
|
+
carouselInstance.updateExternalControlsState();
|
|
400
|
+
}).not.toThrow();
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|