@codeforamerica/marcomms-design-system 1.13.0 → 1.14.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/dist/index.js +2568 -1
- package/dist/styles.css +1 -1
- package/package.json +14 -14
- package/src/components/carousel.js +516 -114
- package/src/components/carousel.stories.js +105 -1
- package/src/core/grid.scss +5 -1
|
@@ -1,177 +1,579 @@
|
|
|
1
|
-
// TODO: Add mouse click and drag support to scroll carousel
|
|
2
1
|
import { LitElement, html, css } from "lit";
|
|
3
2
|
import { commonStyles } from "../shared/common";
|
|
3
|
+
import "./icon";
|
|
4
4
|
|
|
5
5
|
class Carousel extends LitElement {
|
|
6
|
+
// Configuration constants
|
|
7
|
+
static SWIPE_THRESHOLD = 50;
|
|
8
|
+
static SHADOW_SPACE = 32;
|
|
9
|
+
static PROGRESS_UPDATE_INTERVAL = 100;
|
|
10
|
+
static AUTOPLAY_DURATION = 10000;
|
|
11
|
+
static DESKTOP_BREAKPOINT = 768;
|
|
12
|
+
static ITEMS_PER_VIEW_DESKTOP = 3;
|
|
13
|
+
static ITEMS_PER_VIEW_MOBILE = 1;
|
|
14
|
+
|
|
6
15
|
static properties = {
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
currentPage: { type: Number, state: true },
|
|
17
|
+
totalSlides: { type: Number, state: true },
|
|
18
|
+
progressPercentage: { type: Number, state: true },
|
|
19
|
+
isPaused: { type: Boolean, state: true },
|
|
20
|
+
prefersReducedMotion: { type: Boolean, state: true },
|
|
21
|
+
itemsPerView: { type: Number },
|
|
22
|
+
autoPlay: { type: Boolean },
|
|
23
|
+
autoPlayDuration: { type: Number },
|
|
9
24
|
};
|
|
25
|
+
|
|
10
26
|
static styles = [
|
|
11
27
|
commonStyles,
|
|
12
28
|
css`
|
|
13
29
|
:host {
|
|
14
|
-
--
|
|
15
|
-
--
|
|
30
|
+
--carousel-fade-duration: 300ms;
|
|
31
|
+
--carousel-control-size: var(--spacing-layout-2);
|
|
32
|
+
--carousel-dot-size: var(--spacing-component-3);
|
|
33
|
+
--carousel-dot-gap: var(--spacing-component-3);
|
|
34
|
+
--carousel-dot-color: var(--black-20);
|
|
35
|
+
--carousel-dot-active-color: var(--purple-60);
|
|
36
|
+
--carousel-gap: var(--spacing-layout-1);
|
|
37
|
+
}
|
|
16
38
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
.carousel-container {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: var(--spacing-component-3);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.carousel-slides {
|
|
46
|
+
width: 100%;
|
|
21
47
|
overflow: hidden;
|
|
22
|
-
-
|
|
48
|
+
transition: height 400ms ease-in-out;
|
|
49
|
+
height: auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.carousel-track {
|
|
53
|
+
display: flex;
|
|
54
|
+
gap: var(--carousel-gap);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
::slotted(*) {
|
|
58
|
+
flex: 0 0 calc((100% - (var(--items-per-view, 3) - 1) * var(--carousel-gap)) / var(--items-per-view, 3));
|
|
59
|
+
display: none;
|
|
60
|
+
opacity: 0;
|
|
61
|
+
transition: opacity var(--carousel-fade-duration) ease-in-out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
::slotted([data-slide-active]) {
|
|
65
|
+
display: block;
|
|
66
|
+
opacity: 1;
|
|
23
67
|
}
|
|
24
68
|
|
|
25
|
-
|
|
26
|
-
|
|
69
|
+
@media (prefers-reduced-motion: reduce) {
|
|
70
|
+
.carousel-slides {
|
|
71
|
+
transition: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
::slotted(*) {
|
|
75
|
+
transition: none;
|
|
76
|
+
}
|
|
27
77
|
}
|
|
28
78
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
79
|
+
.carousel-controls {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
gap: var(--spacing-component-3);
|
|
32
84
|
}
|
|
33
85
|
|
|
34
|
-
.carousel {
|
|
86
|
+
.carousel-button {
|
|
87
|
+
background-color: transparent;
|
|
88
|
+
border: 0;
|
|
89
|
+
border-radius: 50%;
|
|
90
|
+
color: var(--text-color);
|
|
91
|
+
cursor: pointer;
|
|
35
92
|
display: flex;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
font-size: 1.5rem;
|
|
96
|
+
height: var(--carousel-control-size);
|
|
97
|
+
width: var(--carousel-control-size);
|
|
98
|
+
transition: all 0.2s ease-in-out;
|
|
99
|
+
flex-shrink: 0;
|
|
41
100
|
}
|
|
42
101
|
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
102
|
+
.carousel-button:hover {
|
|
103
|
+
background-color: var(--purple-20);
|
|
104
|
+
border-color: var(--purple-60);
|
|
105
|
+
color: var(--purple-80);
|
|
47
106
|
}
|
|
48
107
|
|
|
49
|
-
.
|
|
50
|
-
|
|
108
|
+
.carousel-button:focus-visible {
|
|
109
|
+
outline: var(--focus-outline);
|
|
110
|
+
outline-offset: 2px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.carousel-button cfa-icon {
|
|
114
|
+
--size: var(--font-size-base);
|
|
115
|
+
color: var(--purple-80);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.carousel-pagination {
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: var(--carousel-dot-gap);
|
|
122
|
+
flex-wrap: wrap;
|
|
123
|
+
justify-content: center;
|
|
51
124
|
}
|
|
52
125
|
|
|
53
|
-
.
|
|
54
|
-
background:
|
|
126
|
+
.carousel-dot {
|
|
127
|
+
background-color: var(--carousel-dot-color);
|
|
55
128
|
border: none;
|
|
56
129
|
border-radius: 50%;
|
|
57
|
-
color: var(--purple-80);
|
|
58
130
|
cursor: pointer;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
131
|
+
height: var(--carousel-dot-size);
|
|
132
|
+
min-width: var(--carousel-dot-size);
|
|
133
|
+
padding: 0;
|
|
134
|
+
position: relative;
|
|
135
|
+
transition: all 0.3s ease-in-out;
|
|
63
136
|
}
|
|
64
137
|
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
background-color: var(--white, #fff);
|
|
138
|
+
.carousel-dot:hover {
|
|
139
|
+
background-color: var(--gray-60);
|
|
68
140
|
}
|
|
69
141
|
|
|
70
|
-
.
|
|
142
|
+
.carousel-dot:focus-visible {
|
|
71
143
|
outline: var(--focus-outline);
|
|
144
|
+
outline-offset: 3px;
|
|
72
145
|
}
|
|
73
146
|
|
|
74
|
-
.
|
|
75
|
-
color: var(--
|
|
76
|
-
|
|
147
|
+
.carousel-dot[aria-current="true"] {
|
|
148
|
+
background-color: var(--carousel-dot-color);
|
|
149
|
+
border-radius: var(--carousel-dot-size);
|
|
150
|
+
min-width: calc(var(--carousel-dot-size) * 4);
|
|
151
|
+
overflow: hidden;
|
|
77
152
|
}
|
|
78
153
|
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
154
|
+
.carousel-dot-progress {
|
|
155
|
+
background-color: var(--carousel-dot-active-color);
|
|
156
|
+
height: 100%;
|
|
157
|
+
width: 0%;
|
|
158
|
+
transition: width 100ms linear;
|
|
83
159
|
}
|
|
84
160
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
161
|
+
@media (prefers-reduced-motion: reduce) {
|
|
162
|
+
.carousel-dot-progress {
|
|
163
|
+
transition: none;
|
|
164
|
+
}
|
|
89
165
|
}
|
|
90
166
|
|
|
91
|
-
@media (
|
|
92
|
-
|
|
93
|
-
|
|
167
|
+
@media (max-width: 768px) {
|
|
168
|
+
/* Note: 768px matches Carousel.DESKTOP_BREAKPOINT - keep in sync */
|
|
169
|
+
.carousel-button {
|
|
170
|
+
font-size: 1.25rem;
|
|
171
|
+
height: calc(var(--carousel-control-size) * 0.85);
|
|
172
|
+
width: calc(var(--carousel-control-size) * 0.85);
|
|
94
173
|
}
|
|
95
174
|
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
175
|
+
.carousel-dot {
|
|
176
|
+
height: calc(var(--carousel-dot-size) * 0.8);
|
|
177
|
+
min-width: calc(var(--carousel-dot-size) * 0.8);
|
|
99
178
|
}
|
|
100
179
|
}
|
|
101
180
|
`,
|
|
102
181
|
];
|
|
182
|
+
|
|
103
183
|
constructor() {
|
|
104
184
|
super();
|
|
105
|
-
this.
|
|
106
|
-
this.
|
|
185
|
+
this.currentPage = 0;
|
|
186
|
+
this.totalSlides = 0;
|
|
187
|
+
this.progressPercentage = 0;
|
|
188
|
+
this.isPaused = true;
|
|
189
|
+
this.prefersReducedMotion = window.matchMedia(
|
|
190
|
+
"(prefers-reduced-motion: reduce)"
|
|
191
|
+
).matches;
|
|
192
|
+
this.itemsPerView = Carousel.ITEMS_PER_VIEW_DESKTOP;
|
|
193
|
+
this.autoPlay = true;
|
|
194
|
+
// Clamp autoPlayDuration to positive value
|
|
195
|
+
this.autoPlayDuration = Math.max(100, Carousel.AUTOPLAY_DURATION);
|
|
196
|
+
this.autoPlayTimer = null;
|
|
197
|
+
this.autoPlayTimeout = null;
|
|
198
|
+
this.touchStartX = null;
|
|
199
|
+
this.touchEndX = null;
|
|
200
|
+
this._initialized = false;
|
|
201
|
+
this._resizeTimerId = null;
|
|
202
|
+
this._mouseEnterListener = null;
|
|
203
|
+
this._mouseLeaveListener = null;
|
|
107
204
|
}
|
|
205
|
+
|
|
108
206
|
connectedCallback() {
|
|
109
207
|
super.connectedCallback();
|
|
110
208
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
209
|
+
this.style.setProperty("--items-per-view", this.itemsPerView);
|
|
210
|
+
|
|
211
|
+
this.addEventListener("keydown", (e) => this._handleKeydown(e));
|
|
212
|
+
this.addEventListener("touchstart", (e) => this._handleTouchStart(e), false);
|
|
213
|
+
this.addEventListener("touchend", (e) => this._handleTouchEnd(e), false);
|
|
214
|
+
|
|
215
|
+
// Store debounced resize listener for proper cleanup
|
|
216
|
+
this._resizeListener = () => {
|
|
217
|
+
if (this._resizeTimerId) {
|
|
218
|
+
clearTimeout(this._resizeTimerId);
|
|
219
|
+
}
|
|
220
|
+
this._resizeTimerId = setTimeout(() => {
|
|
221
|
+
this.updateItemsPerView();
|
|
222
|
+
this.updateCarouselHeight();
|
|
223
|
+
}, 150);
|
|
224
|
+
};
|
|
225
|
+
window.addEventListener("resize", this._resizeListener);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
disconnectedCallback() {
|
|
229
|
+
super.disconnectedCallback();
|
|
230
|
+
this.stopAutoPlay();
|
|
231
|
+
if (this._resizeListener) {
|
|
232
|
+
window.removeEventListener("resize", this._resizeListener);
|
|
233
|
+
}
|
|
234
|
+
if (this._resizeTimerId) {
|
|
235
|
+
clearTimeout(this._resizeTimerId);
|
|
236
|
+
}
|
|
237
|
+
// Clean up mouse listeners if they were attached
|
|
238
|
+
const slidesContainer = this.shadowRoot?.querySelector(".carousel-slides");
|
|
239
|
+
if (slidesContainer && this._mouseEnterListener && this._mouseLeaveListener) {
|
|
240
|
+
slidesContainer.removeEventListener("mouseenter", this._mouseEnterListener);
|
|
241
|
+
slidesContainer.removeEventListener("mouseleave", this._mouseLeaveListener);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
updated(changedProperties) {
|
|
246
|
+
if (changedProperties.has("itemsPerView")) {
|
|
247
|
+
this.style.setProperty("--items-per-view", this.itemsPerView);
|
|
248
|
+
}
|
|
249
|
+
if (changedProperties.has("currentPage")) {
|
|
250
|
+
this.updateItemVisibility();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
firstUpdated() {
|
|
255
|
+
const slot = this.shadowRoot.querySelector("slot");
|
|
256
|
+
const slidesContainer = this.shadowRoot.querySelector(".carousel-slides");
|
|
257
|
+
|
|
258
|
+
if (slot && !this._initialized) {
|
|
259
|
+
this._initialized = true;
|
|
260
|
+
const items = slot.assignedElements();
|
|
261
|
+
this.totalSlides = items.length;
|
|
262
|
+
this.updateItemsPerView();
|
|
263
|
+
this.updateItemVisibility();
|
|
264
|
+
this.startAutoPlay();
|
|
265
|
+
|
|
266
|
+
// Recalculate height after images load with timeout guard
|
|
267
|
+
this._waitForImages(items, 5000).then(() => {
|
|
268
|
+
this.updateCarouselHeight();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Only pause when hovering over the carousel items, not the controls
|
|
273
|
+
// Store listeners for cleanup on disconnect
|
|
274
|
+
if (slidesContainer && !this._mouseEnterListener) {
|
|
275
|
+
this._mouseEnterListener = () => {
|
|
276
|
+
this.stopAutoPlay();
|
|
277
|
+
};
|
|
278
|
+
this._mouseLeaveListener = () => {
|
|
279
|
+
if (!this.isPaused) {
|
|
280
|
+
this._scheduleNextPage();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
slidesContainer.addEventListener("mouseenter", this._mouseEnterListener);
|
|
284
|
+
slidesContainer.addEventListener("mouseleave", this._mouseLeaveListener);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_waitForImages(items, timeout = 3000) {
|
|
289
|
+
return Promise.race([
|
|
290
|
+
Promise.all(
|
|
291
|
+
Array.from(items).map(item =>
|
|
292
|
+
Promise.all(
|
|
293
|
+
Array.from(item.querySelectorAll('img')).map(img =>
|
|
294
|
+
img.complete
|
|
295
|
+
? Promise.resolve()
|
|
296
|
+
: new Promise(resolve => {
|
|
297
|
+
img.addEventListener('load', resolve, { once: true });
|
|
298
|
+
img.addEventListener('error', resolve, { once: true });
|
|
299
|
+
})
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
),
|
|
304
|
+
new Promise((_, reject) =>
|
|
305
|
+
setTimeout(() => reject(new Error('Image load timeout')), timeout)
|
|
306
|
+
)
|
|
307
|
+
]).catch(() => {
|
|
308
|
+
// If timeout or error, continue anyway
|
|
309
|
+
return Promise.resolve();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
get totalPages() {
|
|
314
|
+
return Math.ceil(this.totalSlides / this.itemsPerView);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
updateItemsPerView() {
|
|
318
|
+
const isDesktop = window.innerWidth >= Carousel.DESKTOP_BREAKPOINT;
|
|
319
|
+
const newItemsPerView = isDesktop
|
|
320
|
+
? Carousel.ITEMS_PER_VIEW_DESKTOP
|
|
321
|
+
: Carousel.ITEMS_PER_VIEW_MOBILE;
|
|
322
|
+
|
|
323
|
+
if (newItemsPerView !== this.itemsPerView) {
|
|
324
|
+
this.itemsPerView = newItemsPerView;
|
|
325
|
+
this.style.setProperty("--items-per-view", this.itemsPerView);
|
|
326
|
+
// Reset to first page if current page is out of bounds
|
|
327
|
+
if (this.currentPage >= this.totalPages) {
|
|
328
|
+
this.currentPage = 0;
|
|
329
|
+
}
|
|
330
|
+
this.requestUpdate();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
updateItemVisibility() {
|
|
335
|
+
const slot = this.shadowRoot.querySelector("slot");
|
|
336
|
+
if (!slot) return;
|
|
337
|
+
|
|
338
|
+
const items = slot.assignedElements();
|
|
339
|
+
if (items.length === 0) return;
|
|
340
|
+
|
|
341
|
+
const startIdx = this.currentPage * this.itemsPerView;
|
|
342
|
+
const endIdx = startIdx + this.itemsPerView;
|
|
343
|
+
|
|
344
|
+
items.forEach((item, idx) => {
|
|
345
|
+
if (idx >= startIdx && idx < endIdx) {
|
|
346
|
+
item.setAttribute("data-slide-active", "");
|
|
347
|
+
} else {
|
|
348
|
+
item.removeAttribute("data-slide-active");
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Update carousel height to match current page content
|
|
353
|
+
this.updateCarouselHeight();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
updateCarouselHeight() {
|
|
357
|
+
const slot = this.shadowRoot.querySelector("slot");
|
|
358
|
+
const slidesContainer = this.shadowRoot.querySelector(".carousel-slides");
|
|
359
|
+
|
|
360
|
+
if (!slot || !slidesContainer) return;
|
|
361
|
+
|
|
362
|
+
const items = slot.assignedElements();
|
|
363
|
+
if (items.length === 0) return;
|
|
364
|
+
|
|
365
|
+
const startIdx = this.currentPage * this.itemsPerView;
|
|
366
|
+
const endIdx = startIdx + this.itemsPerView;
|
|
367
|
+
|
|
368
|
+
// Get max height of current page items
|
|
369
|
+
let maxHeight = 0;
|
|
370
|
+
for (let i = startIdx; i < endIdx && i < items.length; i++) {
|
|
371
|
+
const item = items[i];
|
|
372
|
+
maxHeight = Math.max(maxHeight, item.offsetHeight);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Use requestAnimationFrame to ensure layout is ready
|
|
376
|
+
// Add extra space for box-shadows to display without clipping
|
|
377
|
+
requestAnimationFrame(() => {
|
|
378
|
+
if (maxHeight > 0) {
|
|
379
|
+
slidesContainer.style.height = (maxHeight + Carousel.SHADOW_SPACE) + "px";
|
|
119
380
|
}
|
|
120
381
|
});
|
|
121
382
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
) {
|
|
150
|
-
this.
|
|
151
|
-
|
|
152
|
-
|
|
383
|
+
|
|
384
|
+
startAutoPlay() {
|
|
385
|
+
if (!this.autoPlay || this.prefersReducedMotion || this.totalSlides === 0) {
|
|
386
|
+
this.isPaused = true;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.isPaused = false;
|
|
390
|
+
this.progressPercentage = 0;
|
|
391
|
+
this._scheduleNextPage();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
play() {
|
|
395
|
+
if (!this.prefersReducedMotion) {
|
|
396
|
+
this.startAutoPlay();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
pause() {
|
|
401
|
+
this.isPaused = true;
|
|
402
|
+
this.stopAutoPlay();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
stopAutoPlay() {
|
|
406
|
+
if (this.autoPlayTimer) {
|
|
407
|
+
clearInterval(this.autoPlayTimer);
|
|
408
|
+
this.autoPlayTimer = null;
|
|
409
|
+
}
|
|
410
|
+
if (this.autoPlayTimeout) {
|
|
411
|
+
clearTimeout(this.autoPlayTimeout);
|
|
412
|
+
this.autoPlayTimeout = null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_scheduleNextPage() {
|
|
417
|
+
this.stopAutoPlay();
|
|
418
|
+
|
|
419
|
+
// Progress bar animation - calculate increment dynamically
|
|
420
|
+
const totalUpdates = this.autoPlayDuration / Carousel.PROGRESS_UPDATE_INTERVAL;
|
|
421
|
+
const increment = 100 / totalUpdates;
|
|
422
|
+
|
|
423
|
+
this.autoPlayTimer = setInterval(() => {
|
|
424
|
+
this.progressPercentage = Math.min(100, this.progressPercentage + increment);
|
|
425
|
+
}, Carousel.PROGRESS_UPDATE_INTERVAL);
|
|
426
|
+
|
|
427
|
+
// Advance to next page after configured duration
|
|
428
|
+
this.autoPlayTimeout = setTimeout(() => {
|
|
429
|
+
if (!this.isPaused) {
|
|
430
|
+
this.nextPage();
|
|
431
|
+
this.startAutoPlay();
|
|
432
|
+
}
|
|
433
|
+
}, this.autoPlayDuration);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
nextPage() {
|
|
437
|
+
this.currentPage = (this.currentPage + 1) % this.totalPages;
|
|
438
|
+
this.progressPercentage = 0;
|
|
439
|
+
this.stopAutoPlay();
|
|
440
|
+
this.requestUpdate();
|
|
441
|
+
this.play();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
previousPage() {
|
|
445
|
+
this.currentPage =
|
|
446
|
+
(this.currentPage - 1 + this.totalPages) % this.totalPages;
|
|
447
|
+
this.progressPercentage = 0;
|
|
448
|
+
this.stopAutoPlay();
|
|
449
|
+
this.requestUpdate();
|
|
450
|
+
this.play();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
goToPage(pageIndex) {
|
|
454
|
+
if (pageIndex >= 0 && pageIndex < this.totalPages) {
|
|
455
|
+
// Only update if page is different
|
|
456
|
+
if (pageIndex !== this.currentPage) {
|
|
457
|
+
this.currentPage = pageIndex;
|
|
458
|
+
this.progressPercentage = 0;
|
|
459
|
+
this.stopAutoPlay();
|
|
460
|
+
this.requestUpdate();
|
|
461
|
+
this.play();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
_handleKeydown(event) {
|
|
467
|
+
if (event.key === "ArrowRight") {
|
|
468
|
+
event.preventDefault();
|
|
469
|
+
this.nextPage();
|
|
470
|
+
this.pause();
|
|
471
|
+
} else if (event.key === "ArrowLeft") {
|
|
472
|
+
event.preventDefault();
|
|
473
|
+
this.previousPage();
|
|
474
|
+
this.pause();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
_handleTouchStart(event) {
|
|
479
|
+
if (event.changedTouches && event.changedTouches.length > 0) {
|
|
480
|
+
this.touchStartX = event.changedTouches[0].screenX;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
_handleTouchEnd(event) {
|
|
485
|
+
if (event.changedTouches && event.changedTouches.length > 0) {
|
|
486
|
+
this.touchEndX = event.changedTouches[0].screenX;
|
|
487
|
+
this._handleSwipe();
|
|
153
488
|
}
|
|
154
489
|
}
|
|
490
|
+
|
|
491
|
+
_handleSwipe() {
|
|
492
|
+
// Validate that touch coordinates were captured
|
|
493
|
+
if (this.touchStartX === null || this.touchEndX === null) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const diff = this.touchStartX - this.touchEndX;
|
|
498
|
+
|
|
499
|
+
if (Math.abs(diff) > Carousel.SWIPE_THRESHOLD) {
|
|
500
|
+
if (diff > 0) {
|
|
501
|
+
// Swiped left - go to next page
|
|
502
|
+
this.nextPage();
|
|
503
|
+
this.pause();
|
|
504
|
+
} else {
|
|
505
|
+
// Swiped right - go to previous page
|
|
506
|
+
this.previousPage();
|
|
507
|
+
this.pause();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
155
512
|
render() {
|
|
156
513
|
return html`
|
|
157
|
-
<div
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
514
|
+
<div
|
|
515
|
+
class="carousel-container"
|
|
516
|
+
role="region"
|
|
517
|
+
aria-label="Content carousel"
|
|
518
|
+
>
|
|
519
|
+
<div class="carousel-slides">
|
|
520
|
+
<div class="carousel-track">
|
|
521
|
+
<slot @slotchange=${() => {
|
|
522
|
+
// Only update if not already initialized in firstUpdated
|
|
523
|
+
if (!this._initialized) {
|
|
524
|
+
const slot = this.shadowRoot.querySelector("slot");
|
|
525
|
+
if (slot) {
|
|
526
|
+
const items = slot.assignedElements();
|
|
527
|
+
this.totalSlides = items.length;
|
|
528
|
+
this.updateItemVisibility();
|
|
529
|
+
this.requestUpdate();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}}></slot>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
${this.totalSlides > 0
|
|
537
|
+
? html`
|
|
538
|
+
<div class="carousel-controls">
|
|
539
|
+
<button
|
|
540
|
+
class="carousel-button"
|
|
541
|
+
@click=${() => this.previousPage()}
|
|
542
|
+
aria-label="Previous page"
|
|
543
|
+
>
|
|
544
|
+
<cfa-icon>arrow_back</cfa-icon>
|
|
545
|
+
</button>
|
|
546
|
+
|
|
547
|
+
<div class="carousel-pagination">
|
|
548
|
+
${Array.from({ length: this.totalPages }).map(
|
|
549
|
+
(_, idx) => html`
|
|
550
|
+
<button
|
|
551
|
+
class="carousel-dot"
|
|
552
|
+
@click=${() => this.goToPage(idx)}
|
|
553
|
+
aria-label="Go to page ${idx + 1} of ${this.totalPages}"
|
|
554
|
+
aria-current=${idx === this.currentPage ? "true" : "false"}
|
|
555
|
+
>
|
|
556
|
+
${idx === this.currentPage && !this.prefersReducedMotion
|
|
557
|
+
? html`<div
|
|
558
|
+
class="carousel-dot-progress"
|
|
559
|
+
style="width: ${this.progressPercentage}%"
|
|
560
|
+
></div>`
|
|
561
|
+
: ""}
|
|
562
|
+
</button>
|
|
563
|
+
`
|
|
564
|
+
)}
|
|
565
|
+
</div>
|
|
566
|
+
|
|
567
|
+
<button
|
|
568
|
+
class="carousel-button"
|
|
569
|
+
@click=${() => this.nextPage()}
|
|
570
|
+
aria-label="Next page"
|
|
571
|
+
>
|
|
572
|
+
<cfa-icon>arrow_forward</cfa-icon>
|
|
573
|
+
</button>
|
|
574
|
+
</div>
|
|
575
|
+
`
|
|
576
|
+
: ""}
|
|
175
577
|
</div>
|
|
176
578
|
`;
|
|
177
579
|
}
|