@codeforamerica/marcomms-design-system 1.14.0 → 1.15.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 +4 -4
- package/package.json +1 -1
- package/src/components/carousel.js +106 -28
- package/src/components/slide.js +3 -3
package/dist/index.js
CHANGED
|
@@ -2075,7 +2075,7 @@
|
|
|
2075
2075
|
|
|
2076
2076
|
.slide {
|
|
2077
2077
|
background: var(--background);
|
|
2078
|
-
|
|
2078
|
+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
|
2079
2079
|
color: inherit;
|
|
2080
2080
|
display: flex;
|
|
2081
2081
|
flex-direction: column;
|
|
@@ -2084,13 +2084,13 @@
|
|
|
2084
2084
|
margin-inline: auto;
|
|
2085
2085
|
max-width: var(--width);
|
|
2086
2086
|
text-decoration: none;
|
|
2087
|
-
transition:
|
|
2087
|
+
transition: filter 0.2s ease-in-out;
|
|
2088
2088
|
width: 100%;
|
|
2089
2089
|
}
|
|
2090
2090
|
|
|
2091
2091
|
.slide:hover,
|
|
2092
2092
|
.slide:focus {
|
|
2093
|
-
|
|
2093
|
+
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15));
|
|
2094
2094
|
}
|
|
2095
2095
|
|
|
2096
2096
|
.image {
|
|
@@ -2518,7 +2518,7 @@
|
|
|
2518
2518
|
min-width: calc(var(--carousel-dot-size) * 0.8);
|
|
2519
2519
|
}
|
|
2520
2520
|
}
|
|
2521
|
-
`])();constructor(){super(),this.currentPage=0,this.totalSlides=0,this.progressPercentage=0,this.isPaused=!0,this.prefersReducedMotion=window.matchMedia("(prefers-reduced-motion: reduce)").matches,this.itemsPerView=Ft.ITEMS_PER_VIEW_DESKTOP,this.autoPlay=!0,this.autoPlayDuration=Math.max(100,Ft.AUTOPLAY_DURATION),this.autoPlayTimer=null,this.autoPlayTimeout=null,this.touchStartX=null,this.touchEndX=null,this._initialized=!1,this._resizeTimerId=null,this._mouseEnterListener=null,this._mouseLeaveListener=null}connectedCallback(){super.connectedCallback(),this.style.setProperty("--items-per-view",this.itemsPerView),this.addEventListener("keydown",t=>this._handleKeydown(t)),this.addEventListener("touchstart",t=>this._handleTouchStart(t),!1),this.addEventListener("touchend",t=>this._handleTouchEnd(t),!1),this._resizeListener=()=>{this._resizeTimerId&&clearTimeout(this._resizeTimerId),this._resizeTimerId=setTimeout(()=>{this.updateItemsPerView(),this.updateCarouselHeight()},150)},window.addEventListener("resize",this._resizeListener)}disconnectedCallback(){super.disconnectedCallback(),this.stopAutoPlay(),this._resizeListener&&window.removeEventListener("resize",this._resizeListener),this._resizeTimerId&&clearTimeout(this._resizeTimerId);const t=this.shadowRoot?.querySelector(".carousel-slides");t&&this._mouseEnterListener&&this._mouseLeaveListener&&(t.removeEventListener("mouseenter",this._mouseEnterListener),t.removeEventListener("mouseleave",this._mouseLeaveListener))}updated(t){t.has("itemsPerView")&&this.style.setProperty("--items-per-view",this.itemsPerView),t.has("currentPage")&&this.updateItemVisibility()}firstUpdated(){const t=this.shadowRoot.querySelector("slot"),e=this.shadowRoot.querySelector(".carousel-slides");
|
|
2521
|
+
`])();constructor(){super(),this.currentPage=0,this.totalSlides=0,this.progressPercentage=0,this.isPaused=!0,this.prefersReducedMotion=window.matchMedia("(prefers-reduced-motion: reduce)").matches,this.itemsPerView=Ft.ITEMS_PER_VIEW_DESKTOP,this.autoPlay=!0,this.autoPlayDuration=Math.max(100,Ft.AUTOPLAY_DURATION),this.autoPlayTimer=null,this.autoPlayTimeout=null,this.touchStartX=null,this.touchEndX=null,this._initialized=!1,this._resizeTimerId=null,this._mouseEnterListener=null,this._mouseLeaveListener=null}connectedCallback(){super.connectedCallback();const t=this.getAttribute("items-per-view");if(null!==t){const e=parseInt(t,10);isNaN(e)||(this.itemsPerView=e)}this.style.setProperty("--items-per-view",this.itemsPerView),this.addEventListener("keydown",t=>this._handleKeydown(t)),this.addEventListener("touchstart",t=>this._handleTouchStart(t),!1),this.addEventListener("touchend",t=>this._handleTouchEnd(t),!1),this._resizeListener=()=>{this._resizeTimerId&&clearTimeout(this._resizeTimerId),this._resizeTimerId=setTimeout(()=>{this.updateItemsPerView(),this.updateCarouselHeight()},150)},window.addEventListener("resize",this._resizeListener)}disconnectedCallback(){super.disconnectedCallback(),this.stopAutoPlay(),this._resizeListener&&window.removeEventListener("resize",this._resizeListener),this._resizeTimerId&&clearTimeout(this._resizeTimerId);const t=this.shadowRoot?.querySelector(".carousel-slides");t&&this._mouseEnterListener&&this._mouseLeaveListener&&(t.removeEventListener("mouseenter",this._mouseEnterListener),t.removeEventListener("mouseleave",this._mouseLeaveListener))}updated(t){t.has("itemsPerView")&&this.style.setProperty("--items-per-view",this.itemsPerView),t.has("currentPage")&&this.updateItemVisibility()}firstUpdated(){const t=this.shadowRoot.querySelector("slot"),e=this.shadowRoot.querySelector(".carousel-slides");t&&!this._slotChangeSetup&&(this._slotChangeSetup=!0,t.addEventListener("slotchange",()=>{this._slotChangeTimeout&&clearTimeout(this._slotChangeTimeout),this._slotChangeTimeout=setTimeout(()=>{this._initialized||this._initializeCarousel()},50)})),e&&!this._mouseEnterListener&&(this._mouseEnterListener=()=>{this.stopAutoPlay()},this._mouseLeaveListener=()=>{this.isPaused||this._scheduleNextPage()},e.addEventListener("mouseenter",this._mouseEnterListener),e.addEventListener("mouseleave",this._mouseLeaveListener))}_initializeCarousel(){if(this._initialized)return;const t=this.shadowRoot.querySelector("slot");if(!t)return;const e=t.assignedElements();0!==e.length&&(this._initialized=!0,this.totalSlides=e.length,this.updateItemsPerView(),this.updateItemVisibility(),this.startAutoPlay(),this._waitForImages(e,5e3).then(()=>{this.updateCarouselHeight()}))}_waitForImages(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3e3;return Promise.race([Promise.all(Array.from(t).map(t=>Promise.all(Array.from(t.querySelectorAll("img")).map(t=>t.complete?Promise.resolve():new Promise(e=>{t.addEventListener("load",e,{once:!0}),t.addEventListener("error",e,{once:!0})}))))),new Promise((t,i)=>setTimeout(()=>i(new Error("Image load timeout")),e))]).catch(()=>Promise.resolve())}get totalPages(){return Math.ceil(this.totalSlides/this.itemsPerView)}updateItemsPerView(){if(this.hasAttribute("items-per-view"))return;const t=window.innerWidth>=Ft.DESKTOP_BREAKPOINT?Ft.ITEMS_PER_VIEW_DESKTOP:Ft.ITEMS_PER_VIEW_MOBILE;t!==this.itemsPerView&&(this.itemsPerView=t,this.style.setProperty("--items-per-view",this.itemsPerView),this.currentPage>=this.totalPages&&(this.currentPage=0),this.requestUpdate())}updateItemVisibility(){const t=this.shadowRoot.querySelector("slot");if(!t)return;const e=t.assignedElements();if(0===e.length)return;const i=this.currentPage*this.itemsPerView,o=i+this.itemsPerView;e.forEach((t,e)=>{e>=i&&e<o?t.setAttribute("data-slide-active",""):t.removeAttribute("data-slide-active")}),this.updateCarouselHeight()}updateCarouselHeight(){const t=this.shadowRoot.querySelector("slot"),e=this.shadowRoot.querySelector(".carousel-slides");if(!t||!e)return;const i=t.assignedElements();if(0===i.length)return;const o=this.currentPage*this.itemsPerView,a=o+this.itemsPerView;requestAnimationFrame(()=>{requestAnimationFrame(()=>{setTimeout(()=>{let t=0;for(let e=o;e<a&&e<i.length;e++){const o=i[e].offsetHeight;t=Math.max(t,o)}const s=Math.max(t,Ft.SHADOW_SPACE);e.style.height=s+"px"},0)})})}startAutoPlay(){this.autoPlay&&!this.prefersReducedMotion&&0!==this.totalSlides?(this.isPaused=!1,this.progressPercentage=0,this._scheduleNextPage()):this.isPaused=!0}play(){this.prefersReducedMotion||this.startAutoPlay()}pause(){this.isPaused=!0,this.stopAutoPlay()}stopAutoPlay(){this.autoPlayTimer&&(clearInterval(this.autoPlayTimer),this.autoPlayTimer=null),this.autoPlayTimeout&&(clearTimeout(this.autoPlayTimeout),this.autoPlayTimeout=null)}_scheduleNextPage(){this.stopAutoPlay();const t=100/(this.autoPlayDuration/Ft.PROGRESS_UPDATE_INTERVAL);this.autoPlayTimer=setInterval(()=>{this.progressPercentage=Math.min(100,this.progressPercentage+t)},Ft.PROGRESS_UPDATE_INTERVAL),this.autoPlayTimeout=setTimeout(()=>{this.isPaused||(this.nextPage(),this.startAutoPlay())},this.autoPlayDuration)}nextPage(){this.currentPage=(this.currentPage+1)%this.totalPages,this.progressPercentage=0,this.stopAutoPlay(),this.requestUpdate(),this.play()}previousPage(){this.currentPage=(this.currentPage-1+this.totalPages)%this.totalPages,this.progressPercentage=0,this.stopAutoPlay(),this.requestUpdate(),this.play()}goToPage(t){t>=0&&t<this.totalPages&&t!==this.currentPage&&(this.currentPage=t,this.progressPercentage=0,this.stopAutoPlay(),this.requestUpdate(),this.play())}_handleKeydown(t){"ArrowRight"===t.key?(t.preventDefault(),this.nextPage(),this.pause()):"ArrowLeft"===t.key&&(t.preventDefault(),this.previousPage(),this.pause())}_handleTouchStart(t){t.changedTouches&&t.changedTouches.length>0&&(this.touchStartX=t.changedTouches[0].screenX)}_handleTouchEnd(t){t.changedTouches&&t.changedTouches.length>0&&(this.touchEndX=t.changedTouches[0].screenX,this._handleSwipe())}_handleSwipe(){if(null===this.touchStartX||null===this.touchEndX)return;const t=this.touchStartX-this.touchEndX;Math.abs(t)>Ft.SWIPE_THRESHOLD&&(t>0?(this.nextPage(),this.pause()):(this.previousPage(),this.pause()))}render(){return D`
|
|
2522
2522
|
<div
|
|
2523
2523
|
class="carousel-container"
|
|
2524
2524
|
role="region"
|
package/package.json
CHANGED
|
@@ -2,6 +2,25 @@ import { LitElement, html, css } from "lit";
|
|
|
2
2
|
import { commonStyles } from "../shared/common";
|
|
3
3
|
import "./icon";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Carousel Component - Pagination-based content carousel with fade transitions
|
|
7
|
+
*
|
|
8
|
+
* Supports HTML attributes (kebab-case):
|
|
9
|
+
* - items-per-view: Number of items to show per page (default: 3 on desktop, 1 on mobile)
|
|
10
|
+
* - auto-play: Enable/disable auto-play (default: true)
|
|
11
|
+
* - auto-play-duration: Duration in milliseconds for auto-play (default: 10000)
|
|
12
|
+
*
|
|
13
|
+
* Example usage:
|
|
14
|
+
* <cfa-carousel items-per-view="3" auto-play="true" auto-play-duration="5000">
|
|
15
|
+
* <cfa-slide>...</cfa-slide>
|
|
16
|
+
* <cfa-slide>...</cfa-slide>
|
|
17
|
+
* </cfa-carousel>
|
|
18
|
+
*
|
|
19
|
+
* CSS Custom Properties (for styling):
|
|
20
|
+
* - --carousel-fade-duration: Fade transition duration (default: 300ms)
|
|
21
|
+
* - --carousel-dot-color: Pagination dot color (default: var(--black-20))
|
|
22
|
+
* - --carousel-dot-active-color: Active dot progress bar color (default: var(--purple-60))
|
|
23
|
+
*/
|
|
5
24
|
class Carousel extends LitElement {
|
|
6
25
|
// Configuration constants
|
|
7
26
|
static SWIPE_THRESHOLD = 50;
|
|
@@ -206,6 +225,15 @@ class Carousel extends LitElement {
|
|
|
206
225
|
connectedCallback() {
|
|
207
226
|
super.connectedCallback();
|
|
208
227
|
|
|
228
|
+
// Parse items-per-view attribute before setting CSS property
|
|
229
|
+
const attrValue = this.getAttribute('items-per-view');
|
|
230
|
+
if (attrValue !== null) {
|
|
231
|
+
const parsedValue = parseInt(attrValue, 10);
|
|
232
|
+
if (!isNaN(parsedValue)) {
|
|
233
|
+
this.itemsPerView = parsedValue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
209
237
|
this.style.setProperty("--items-per-view", this.itemsPerView);
|
|
210
238
|
|
|
211
239
|
this.addEventListener("keydown", (e) => this._handleKeydown(e));
|
|
@@ -255,17 +283,19 @@ class Carousel extends LitElement {
|
|
|
255
283
|
const slot = this.shadowRoot.querySelector("slot");
|
|
256
284
|
const slidesContainer = this.shadowRoot.querySelector(".carousel-slides");
|
|
257
285
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
286
|
+
// Set up the slotchange listener which will actually initialize when elements are assigned
|
|
287
|
+
if (slot && !this._slotChangeSetup) {
|
|
288
|
+
this._slotChangeSetup = true;
|
|
289
|
+
slot.addEventListener('slotchange', () => {
|
|
290
|
+
// Debounce initialization - wait for DOM to stabilize
|
|
291
|
+
if (this._slotChangeTimeout) {
|
|
292
|
+
clearTimeout(this._slotChangeTimeout);
|
|
293
|
+
}
|
|
294
|
+
this._slotChangeTimeout = setTimeout(() => {
|
|
295
|
+
if (!this._initialized) {
|
|
296
|
+
this._initializeCarousel();
|
|
297
|
+
}
|
|
298
|
+
}, 50);
|
|
269
299
|
});
|
|
270
300
|
}
|
|
271
301
|
|
|
@@ -285,6 +315,34 @@ class Carousel extends LitElement {
|
|
|
285
315
|
}
|
|
286
316
|
}
|
|
287
317
|
|
|
318
|
+
_initializeCarousel() {
|
|
319
|
+
if (this._initialized) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const slot = this.shadowRoot.querySelector("slot");
|
|
324
|
+
if (!slot) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const items = slot.assignedElements();
|
|
329
|
+
|
|
330
|
+
if (items.length === 0) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this._initialized = true;
|
|
335
|
+
this.totalSlides = items.length;
|
|
336
|
+
this.updateItemsPerView();
|
|
337
|
+
this.updateItemVisibility();
|
|
338
|
+
this.startAutoPlay();
|
|
339
|
+
|
|
340
|
+
// Recalculate height after images load with timeout guard
|
|
341
|
+
this._waitForImages(items, 5000).then(() => {
|
|
342
|
+
this.updateCarouselHeight();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
288
346
|
_waitForImages(items, timeout = 3000) {
|
|
289
347
|
return Promise.race([
|
|
290
348
|
Promise.all(
|
|
@@ -315,6 +373,11 @@ class Carousel extends LitElement {
|
|
|
315
373
|
}
|
|
316
374
|
|
|
317
375
|
updateItemsPerView() {
|
|
376
|
+
// If itemsPerView was explicitly set via attribute, don't override it with responsive defaults
|
|
377
|
+
if (this.hasAttribute('items-per-view')) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
318
381
|
const isDesktop = window.innerWidth >= Carousel.DESKTOP_BREAKPOINT;
|
|
319
382
|
const newItemsPerView = isDesktop
|
|
320
383
|
? Carousel.ITEMS_PER_VIEW_DESKTOP
|
|
@@ -333,14 +396,19 @@ class Carousel extends LitElement {
|
|
|
333
396
|
|
|
334
397
|
updateItemVisibility() {
|
|
335
398
|
const slot = this.shadowRoot.querySelector("slot");
|
|
336
|
-
if (!slot)
|
|
399
|
+
if (!slot) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
337
402
|
|
|
338
403
|
const items = slot.assignedElements();
|
|
339
|
-
|
|
404
|
+
|
|
405
|
+
if (items.length === 0) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
340
408
|
|
|
341
409
|
const startIdx = this.currentPage * this.itemsPerView;
|
|
342
410
|
const endIdx = startIdx + this.itemsPerView;
|
|
343
|
-
|
|
411
|
+
|
|
344
412
|
items.forEach((item, idx) => {
|
|
345
413
|
if (idx >= startIdx && idx < endIdx) {
|
|
346
414
|
item.setAttribute("data-slide-active", "");
|
|
@@ -357,27 +425,37 @@ class Carousel extends LitElement {
|
|
|
357
425
|
const slot = this.shadowRoot.querySelector("slot");
|
|
358
426
|
const slidesContainer = this.shadowRoot.querySelector(".carousel-slides");
|
|
359
427
|
|
|
360
|
-
if (!slot || !slidesContainer)
|
|
428
|
+
if (!slot || !slidesContainer) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
361
431
|
|
|
362
432
|
const items = slot.assignedElements();
|
|
363
|
-
if (items.length === 0)
|
|
433
|
+
if (items.length === 0) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
364
436
|
|
|
365
437
|
const startIdx = this.currentPage * this.itemsPerView;
|
|
366
438
|
const endIdx = startIdx + this.itemsPerView;
|
|
367
439
|
|
|
368
|
-
//
|
|
369
|
-
|
|
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
|
|
440
|
+
// Use multiple animation frames and a small delay to ensure layout is complete
|
|
441
|
+
// after CSS changes have been applied to show/hide slides
|
|
377
442
|
requestAnimationFrame(() => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
443
|
+
requestAnimationFrame(() => {
|
|
444
|
+
// Small timeout to ensure layout has been computed
|
|
445
|
+
setTimeout(() => {
|
|
446
|
+
let maxHeight = 0;
|
|
447
|
+
|
|
448
|
+
for (let i = startIdx; i < endIdx && i < items.length; i++) {
|
|
449
|
+
const item = items[i];
|
|
450
|
+
const height = item.offsetHeight;
|
|
451
|
+
maxHeight = Math.max(maxHeight, height);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Set height (minimum SHADOW_SPACE to accommodate buttons/controls)
|
|
455
|
+
const totalHeight = Math.max(maxHeight, Carousel.SHADOW_SPACE);
|
|
456
|
+
slidesContainer.style.height = totalHeight + "px";
|
|
457
|
+
}, 0);
|
|
458
|
+
});
|
|
381
459
|
});
|
|
382
460
|
}
|
|
383
461
|
|
package/src/components/slide.js
CHANGED
|
@@ -18,7 +18,7 @@ class Slide extends LitElement {
|
|
|
18
18
|
|
|
19
19
|
.slide {
|
|
20
20
|
background: var(--background);
|
|
21
|
-
|
|
21
|
+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
|
22
22
|
color: inherit;
|
|
23
23
|
display: flex;
|
|
24
24
|
flex-direction: column;
|
|
@@ -27,13 +27,13 @@ class Slide extends LitElement {
|
|
|
27
27
|
margin-inline: auto;
|
|
28
28
|
max-width: var(--width);
|
|
29
29
|
text-decoration: none;
|
|
30
|
-
transition:
|
|
30
|
+
transition: filter 0.2s ease-in-out;
|
|
31
31
|
width: 100%;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
.slide:hover,
|
|
35
35
|
.slide:focus {
|
|
36
|
-
|
|
36
|
+
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
.image {
|