@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.
Files changed (66) hide show
  1. package/build/components/index.js +5 -5
  2. package/build/components/index.js.map +1 -1
  3. package/build/components/tsconfig.tsbuildinfo +1 -1
  4. package/build/components/types/index.d.ts +8 -5
  5. package/build/components/types/src/components/Carousel/Carousel.static.d.ts +34 -1
  6. package/build/components/types/src/components/Cover/Cover.d.ts +3 -3
  7. package/build/components/types/src/components/Pill/Pill.d.ts +1 -1
  8. package/build/components/types/src/components/PromoBanner/PromoBanner.d.ts +1 -1
  9. package/build/components/types/src/components/Table/Table.d.ts +2 -0
  10. package/build/components/types/src/components/Table/docsData.d.ts +2 -0
  11. package/build/components/types/src/components/Table/types.d.ts +1 -0
  12. package/build/components/types/src/components/Tile/Tile.d.ts +2 -1
  13. package/build/lib/after-components.css +1 -1
  14. package/build/lib/after-components.css.map +1 -1
  15. package/build/lib/before-components.css +1 -1
  16. package/build/lib/before-components.css.map +1 -1
  17. package/build/lib/components.css +1 -1
  18. package/build/lib/components.css.map +1 -1
  19. package/build/lib/megamenu.css +1 -1
  20. package/build/lib/megamenu.css.map +1 -1
  21. package/build/lib/scripts.js +4 -4
  22. package/build/lib/scripts.js.map +1 -1
  23. package/build/lib/style.css +1 -1
  24. package/build/lib/style.css.map +1 -1
  25. package/build/lib/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +13 -13
  27. package/src/components/Accordion/styles/mixins.scss +5 -1
  28. package/src/components/AnchorNavigation/AnchorNavigation.static.ts +3 -0
  29. package/src/components/AnchorNavigation/styles/mixins.scss +1 -1
  30. package/src/components/Button/styles/config.scss +5 -4
  31. package/src/components/Carousel/Carousel.static.ts +204 -1
  32. package/src/components/Carousel/tests/Carousel.static.test.js +403 -0
  33. package/src/components/Carousel/tests/Carousel.unit.test.js +68 -0
  34. package/src/components/Cover/Cover.tsx +22 -20
  35. package/src/components/Cover/styles/config.scss +23 -12
  36. package/src/components/Cover/styles/mixins.scss +6 -5
  37. package/src/components/Cover/tests/Cover.unit.test.js +52 -52
  38. package/src/components/Expander/styles/style.scss +3 -1
  39. package/src/components/Forms/Group/styles/mixins.scss +14 -0
  40. package/src/components/Pill/Pill.tsx +10 -2
  41. package/src/components/Pill/styles/config.scss +2 -21
  42. package/src/components/Pill/styles/style.scss +16 -6
  43. package/src/components/Pill/tests/Pill.conformance.test.js +8 -2
  44. package/src/components/Pill/tests/Pill.unit.test.js +69 -11
  45. package/src/components/PromoBanner/PromoBanner.tsx +19 -2
  46. package/src/components/PromoBanner/styles/mixins.scss +1 -1
  47. package/src/components/PromoBanner/tests/PromoBanner.conformance.test.js +32 -0
  48. package/src/components/PromoBanner/tests/PromoBanner.unit.test.js +52 -0
  49. package/src/components/Table/Row.tsx +9 -3
  50. package/src/components/Table/Table.tsx +11 -1
  51. package/src/components/Table/TableContext.ts +1 -0
  52. package/src/components/Table/docsData.ts +25 -0
  53. package/src/components/Table/styles/mixins.scss +40 -4
  54. package/src/components/Table/styles/style.scss +6 -0
  55. package/src/components/Table/types.ts +1 -0
  56. package/src/components/Tile/CHANGELOG.md +15 -1
  57. package/src/components/Tile/Tile.tsx +11 -3
  58. package/src/components/Tile/styles/config.scss +0 -11
  59. package/src/components/Tile/styles/style.scss +0 -4
  60. package/src/components/Tile/tests/Tile.unit.test.js +10 -3
  61. package/src/components/Tooltip/InfoTooltip.tsx +1 -5
  62. package/src/styles/base/globals.scss +7 -2
  63. package/src/styles/tokens/color.scss +1 -1
  64. package/src/styles/typography/mixins.scss +18 -0
  65. package/src/styles/typography/style.scss +6 -3
  66. 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
+ });