From e843c50d4142cba8737e04e77b1f300d69bcd4a6 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Fri, 12 Jun 2026 16:50:48 -0500 Subject: [PATCH 01/21] WIP cover-flow slideshow --- js/components/dcf-slideshow.js | 38 ++++++++++++++++------ scss/components-js/_slideshows.scss | 49 ++++++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 76601b65..f2e85585 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -34,6 +34,8 @@ export default class DCFSlideshow { mouseOver = false; + layout = 'default'; + slideContainerClassList = [ 'dcf-relative', ]; @@ -186,7 +188,13 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } this.slideshowContainer.setAttribute('aria-roledescription', 'carousel'); this.slideshowContainer.classList.add('dcf-slideshow-initialized'); - this.slideshowContainer.classList.add(...this.slideContainerClassList); + if (this.layout !== 'cover-flow') { + this.slideshowContainer.classList.add(...this.slideContainerClassList); + } + + if (this.slideshowContainer.dataset.layout === 'cover-flow') { + this.layout = this.slideshowContainer.dataset.layout; + } // If the tabGroup has no ID then it will set it if (this.slideshowContainer.getAttribute('id') === '' || this.slideshowContainer.getAttribute('id') === null) { @@ -201,7 +209,9 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if (this.slideDeck.getAttribute('id') === '' || this.slideDeck.getAttribute('id') === null) { this.slideDeck.setAttribute('id', this.uuid.concat('-slide-deck')); } - this.slideDeck.classList.add(...this.slideDeckClassList); + if (this.layout !== 'cover-flow') { + this.slideDeck.classList.add(...this.slideDeckClassList); + } // Select all the slides this.slides = Array.from(this.slideDeck.children); @@ -242,7 +252,10 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.#shuffleSlides(); } this.#initSlides(); - this.#initControls(); + + if (this.layout !== 'cover-flow') { + this.#initControls(); + } // This needs to go after init controls so we can change the state of the toggle button if (this.allowPlay) { @@ -310,16 +323,21 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> // Set up slide slide.setAttribute('id', this.uuid.concat('-slide-', slideIndex)); slide.classList.add('dcf-slide'); - slide.classList.add(...this.slideClassList); + if (this.layout !== 'cover-flow') { + slide.classList.add(...this.slideClassList); + } + slide.setAttribute('aria-roledescription', 'slide'); slide.setAttribute('aria-label', `${slideIndex + 1} of ${this.slides.length}`); - // If we are not the current slide then hide it - if (slideIndex !== this.currentSlide) { - slide.classList.add('dcf-d-none'); - slide.classList.add('dcf-z-0'); - } else { - slide.classList.add('dcf-z-1'); + if (this.layout !== 'cover-flow') { + // If we are not the current slide then hide it + if (slideIndex !== this.currentSlide) { + slide.classList.add('dcf-d-none'); + slide.classList.add('dcf-z-0'); + } else { + slide.classList.add('dcf-z-1'); + } } // Figure out if we need to do figcaption toggles diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index d59d948b..810fdfdf 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -10,12 +10,12 @@ padding-left: 0; } -.dcf-slideshow.dcf-slideshow-initialized .dcf-slide-deck li { +.dcf-slideshow.dcf-slideshow-initialized:not([data-layout="cover-flow"]) .dcf-slide-deck li { grid-column: 1; grid-row: 1; } -.dcf-slideshow.dcf-slideshow-initialized .dcf-slide-deck { +.dcf-slideshow.dcf-slideshow-initialized:not([data-layout="cover-flow"]) .dcf-slide-deck { clip-path: polygon( 0 0, calc(100% - (#{var.$size-btn-x} * 2) - (#{var.$border-width-button} * 5) - #{var.$size-clip-path}) 0, @@ -26,8 +26,8 @@ ); } -.dcf-slideshow.dcf-slideshow-initialized[data-play="true"] .dcf-slide-deck, -.dcf-slideshow.dcf-slideshow-initialized[data-play="auto"] .dcf-slide-deck { +.dcf-slideshow.dcf-slideshow-initialized[data-play="true"]:not([data-layout="cover-flow"]) .dcf-slide-deck, +.dcf-slideshow.dcf-slideshow-initialized[data-play="auto"]:not([data-layout="cover-flow"]) .dcf-slide-deck { clip-path: polygon( 0 0, calc(100% - (#{var.$size-btn-x} * 3) - (#{var.$border-width-button} * 8) - #{var.$size-clip-path}) 0, @@ -45,3 +45,44 @@ animation-timing-function: var.$animation-timing-fn-fade-in; opacity: 1; // Starting opacity } + +.dcf-slideshow[data-layout="cover-flow"] { + container-type: inline-size; +} + +.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck { + list-style: none; + white-space: nowrap; + overflow-x: auto; + scroll-snap-type: x mandatory; + margin: 0px; + padding: 0px; +} +.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck::before { + content: ' '; + width: 25cqw; + display: inline-block; +} +.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck::after { + content: ' '; + width: 25cqw; + display: inline-block; +} + +/* List item setup */ +.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li { + display: inline-block; + scroll-snap-align: center; + margin: 0px; + padding: 0px; +} + + +/* Image styling and animation */ +.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li figure, +.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li div { + width: 100%; + height: auto; + + position: relative; +} From 60ec93eb85546229f89f7a61be25fc67ae822bdf Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Tue, 16 Jun 2026 13:57:01 -0500 Subject: [PATCH 02/21] WIP update styles and added logic to wait for transition to end --- js/components/dcf-slideshow.js | 38 +++++++++++++++++++++++- js/dcf-utility.js | 23 +++++++++++++++ scss/components-js/_slideshows.scss | 46 +++++++++++++++++------------ 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index f2e85585..982137a2 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -1,4 +1,4 @@ -import { uuidv4 } from '../dcf-utility.js'; +import { uuidv4, waitForTransition } from '../dcf-utility.js'; import DCFFigcaptionToggles from './dcf-figcaption-toggle.js'; export default class DCFSlideshow { @@ -257,6 +257,42 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.#initControls(); } + if (this.layout === 'cover-flow') { + // Set up the Intersection Observer + const observerOptions = { + root: this.slideDeck, + + // This rootMargin creates a narrow "detection band" right in the middle + // of the container (shrinks the detection area by 48% on the left and right) + rootMargin: '0px -48% 0px -48%', + threshold: 0, + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + // Item is in the center + entry.target.classList.add('active'); + waitForTransition(entry.target).then(() => { + this.slides.forEach((singleSlide) => { + if (!singleSlide.isSameNode(entry.target)) { + singleSlide.classList.remove('active'); + } + }); + }); + } + }); + }, observerOptions); + + // Tell the observer to watch every item + this.slides.forEach((item) => { + observer.observe(item); + item.addEventListener('click', () => { + item.scrollIntoView({'inline': 'center'}); + }); + }); + } + // This needs to go after init controls so we can change the state of the toggle button if (this.allowPlay) { if (!(window.matchMedia('(prefers-reduced-motion: reduce)').matches)) { diff --git a/js/dcf-utility.js b/js/dcf-utility.js index 2491bd98..2b51adaf 100644 --- a/js/dcf-utility.js +++ b/js/dcf-utility.js @@ -159,3 +159,26 @@ export function stringToDom(htmlString) { const doc = new DOMParser().parseFromString(htmlString, 'text/html'); return doc.body.firstChild; } + +export function waitForTransition(element) { + return new Promise((resolve) => { + const styles = window.getComputedStyle(element); + const duration = parseFloat(styles.transitionDuration); + + if (!duration) { + resolve(); + return; + } + + const propertyCount = styles.transitionProperty.split(',').length; + let fired = 0; + + element.addEventListener('transitionend', function handler() { + fired++; + if (fired >= propertyCount) { + element.removeEventListener('transitionend', handler); + resolve(); + } + }); + }); +} diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index 810fdfdf..1840ce2a 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -52,37 +52,45 @@ .dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck { list-style: none; - white-space: nowrap; + display: grid; + grid-auto-flow: column; + grid-auto-columns: max-content; + gap: 1rem; overflow-x: auto; + overflow-y: hidden; scroll-snap-type: x mandatory; margin: 0px; - padding: 0px; -} -.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck::before { - content: ' '; - width: 25cqw; - display: inline-block; -} -.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck::after { - content: ' '; - width: 25cqw; - display: inline-block; + padding-block: 0px; + scroll-behavior: smooth; + padding-inline: calc(50cqw - (33cqw / 2) + 2cqw); // We add +2 to fix some browser rounding issues } -/* List item setup */ +// /* List item setup */ .dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li { - display: inline-block; + overflow: hidden; scroll-snap-align: center; margin: 0px; padding: 0px; + position: relative; + width: calc(33cqw * 2 / 3); + flex-shrink: 0; + height: 100%; + transition: width 0.25s ease-in-out; } +.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li.active { + width: 33cqw; + height: 100%; +} -/* Image styling and animation */ -.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li figure, -.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li div { +.dcf-slideshow-test-2 { + height: 100%; width: 100%; - height: auto; +} - position: relative; +.dcf-slideshow-test { + aspect-ratio: 9 / 16; + height: 100%; + width: 100%; + object-fit: cover; } From cb8a28b105fce73acc1f10bbb2f785a098400f93 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Thu, 18 Jun 2026 15:15:20 -0500 Subject: [PATCH 03/21] Update slideshow styles --- js/components/dcf-slideshow.js | 44 +++++++++-------------------- scss/components-js/_slideshows.scss | 3 +- 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 982137a2..fa698cfc 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -258,39 +258,23 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } if (this.layout === 'cover-flow') { - // Set up the Intersection Observer - const observerOptions = { - root: this.slideDeck, - - // This rootMargin creates a narrow "detection band" right in the middle - // of the container (shrinks the detection area by 48% on the left and right) - rootMargin: '0px -48% 0px -48%', - threshold: 0, - }; - - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - // Item is in the center - entry.target.classList.add('active'); - waitForTransition(entry.target).then(() => { - this.slides.forEach((singleSlide) => { - if (!singleSlide.isSameNode(entry.target)) { - singleSlide.classList.remove('active'); - } - }); + this.slides.forEach((singleSlide) => { + singleSlide.addEventListener('click', () => { + singleSlide.classList.add('active'); + singleSlide.scrollIntoView({'inline': 'center'}); + waitForTransition(singleSlide).then(() => { + this.slides.forEach((slideToRemoveClass) => { + if (!slideToRemoveClass.isSameNode(singleSlide) && slideToRemoveClass.classList.contains('active')) { + slideToRemoveClass.classList.remove('active'); + waitForTransition(slideToRemoveClass).then(() => { + singleSlide.scrollIntoView({'inline': 'center'}); + }); + } }); - } - }); - }, observerOptions); - - // Tell the observer to watch every item - this.slides.forEach((item) => { - observer.observe(item); - item.addEventListener('click', () => { - item.scrollIntoView({'inline': 'center'}); + }); }); }); + this.slides[0].classList.add('active'); } // This needs to go after init controls so we can change the state of the toggle button diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index 1840ce2a..18b79811 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -56,8 +56,7 @@ grid-auto-flow: column; grid-auto-columns: max-content; gap: 1rem; - overflow-x: auto; - overflow-y: hidden; + overflow: hidden; scroll-snap-type: x mandatory; margin: 0px; padding-block: 0px; From 261787cfb0852cb6e8f51e4e9a51a5566455dc90 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Fri, 19 Jun 2026 12:09:09 -0500 Subject: [PATCH 04/21] Working styles and animations --- js/components/dcf-slideshow.js | 71 ++++++++-- js/dcf-animation.js | 203 ++++++++++++++++++++++++++++ scss/components-js/_slideshows.scss | 38 ++++-- 3 files changed, 289 insertions(+), 23 deletions(-) create mode 100644 js/dcf-animation.js diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index fa698cfc..a57ea6e5 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -1,4 +1,5 @@ -import { uuidv4, waitForTransition } from '../dcf-utility.js'; +import { uuidv4 } from '../dcf-utility.js'; +import { easingInOutCubic, lerp } from '../dcf-animation.js'; import DCFFigcaptionToggles from './dcf-figcaption-toggle.js'; export default class DCFSlideshow { @@ -36,6 +37,8 @@ export default class DCFSlideshow { layout = 'default'; + scrollingId = null; + slideContainerClassList = [ 'dcf-relative', ]; @@ -261,17 +264,16 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.slides.forEach((singleSlide) => { singleSlide.addEventListener('click', () => { singleSlide.classList.add('active'); - singleSlide.scrollIntoView({'inline': 'center'}); - waitForTransition(singleSlide).then(() => { - this.slides.forEach((slideToRemoveClass) => { - if (!slideToRemoveClass.isSameNode(singleSlide) && slideToRemoveClass.classList.contains('active')) { - slideToRemoveClass.classList.remove('active'); - waitForTransition(slideToRemoveClass).then(() => { - singleSlide.scrollIntoView({'inline': 'center'}); - }); - } - }); + this.slides.forEach((slideToRemoveClass) => { + if (!slideToRemoveClass.isSameNode(singleSlide) && slideToRemoveClass.classList.contains('active')) { + slideToRemoveClass.classList.remove('active'); + } }); + + if (this.scrollingId !== singleSlide.id) { + this.scrollingId = singleSlide.id; + this.smoothScrollToItem(singleSlide, singleSlide.id); + } }); }); this.slides[0].classList.add('active'); @@ -299,6 +301,53 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> })); } + smoothScrollToItem(item, currentId) { + //!IMPORTANT: This needs to be a little longer than the animation to grow the item + const scrollDurationMs = 505; + let scrollProgressMs = 0; + let oldLeft = parseFloat(this.slideDeck.style.getPropertyValue('left')); + if (Number.isNaN(oldLeft)) { + oldLeft = 0; + } + let previousLoopTime = Date.now(); + const animationLoop = () => { + if (this.scrollingId !== currentId) { + return; + } + + const startLoopTime = Date.now(); + const deltaTime = startLoopTime - previousLoopTime; + scrollProgressMs += deltaTime; + + const newLeft = this.calculateSlideDeckOffset(item); + + if (scrollProgressMs < scrollDurationMs) { + const easedPercent = easingInOutCubic(scrollProgressMs / scrollDurationMs); + const lerpLeft = lerp(oldLeft, newLeft, easedPercent); + console.log('lerpLeft', lerpLeft); + + this.slideDeck.style.left = `${lerpLeft}px`; + + window.requestAnimationFrame(animationLoop); + } else { + this.slideDeck.style.left = `${newLeft}px`; + this.scrollingId = null; + } + previousLoopTime = startLoopTime; + }; + + window.requestAnimationFrame(animationLoop); + } + + calculateSlideDeckOffset(item) { + const itemMidPoint = item.offsetWidth / 2; + const itemOffsetToList = item.offsetLeft; + const wrapperMidPoint = this.slideshowContainer.offsetWidth / 2; + + const getMidItemToLeftEdge = (-1 * ( itemOffsetToList + itemMidPoint)); + return getMidItemToLeftEdge + wrapperMidPoint; + } + /** * Validates and returns standardized name of events for tabs * @static diff --git a/js/dcf-animation.js b/js/dcf-animation.js new file mode 100644 index 00000000..cc36d5d0 --- /dev/null +++ b/js/dcf-animation.js @@ -0,0 +1,203 @@ +// Exporting Easing Functions +// Thanks to https://easings.net/ + + +export function lerp(start, end, time) { + return start + (end - start) * time; +} + + +export function easingLinear(time) { + return time; +} + +// Sine + +export function easingInSine(time) { + return 1 - Math.cos((time * Math.PI) / 2); +} + +export function easingOutSine(time) { + return Math.sin((time * Math.PI) / 2); +} + +export function easingInOutSine(time) { + return -(Math.cos(Math.PI * time) - 1) / 2; +} + +// Cubic + +export function easingInCubic(time) { + return time * time * time; +} + +export function easingOutCubic(time) { + return 1 - Math.pow(1 - time, 3); +} + +export function easingInOutCubic(time) { + return time < 0.5 ? 4 * time * time * time : 1 - Math.pow(-2 * time + 2, 3) / 2; +} + +// Quint + +export function easingInQuint(time) { + return time * time * time * time * time; +} + +export function easingOutQuint(time) { + return 1 - Math.pow(1 - time, 5); +} + +export function easingInOutQuint(time) { + return time < 0.5 ? 16 * time * time * time * time * time : 1 - Math.pow(-2 * time + 2, 5) / 2; +} + +// Circ + +export function easingInCirc(time) { + return 1 - Math.sqrt(1 - Math.pow(time, 2)); +} + +export function easingOutCirc(time) { + return Math.sqrt(1 - Math.pow(time - 1, 2)); +} + +export function easingInOutCirc(time) { + return time < 0.5 + ? (1 - Math.sqrt(1 - Math.pow(2 * time, 2))) / 2 + : (Math.sqrt(1 - Math.pow(-2 * time + 2, 2)) + 1) / 2; +} + +// Elastic + +export function easingInElastic(time) { + const check4 = (2 * Math.PI) / 3; + + return time === 0 + ? 0 + : time === 1 + ? 1 + : -Math.pow(2, 10 * time - 10) * Math.sin((time * 10 - 10.75) * check4); +} + +export function easingOutElastic(time) { + const check4 = (2 * Math.PI) / 3; + + return time === 0 + ? 0 + : time === 1 + ? 1 + : Math.pow(2, -10 * time) * Math.sin((time * 10 - 0.75) * check4) + 1; +} + +export function easingInOutElastic(time) { + const check5 = (2 * Math.PI) / 4.5; + + return time === 0 + ? 0 + : time === 1 + ? 1 + : time < 0.5 + ? -(Math.pow(2, 20 * time - 10) * Math.sin((20 * time - 11.125) * check5)) / 2 + : (Math.pow(2, -20 * time + 10) * Math.sin((20 * time - 11.125) * check5)) / 2 + 1; +} + +// Quad + +export function easingInQuad(time) { + return time * time; +} + +export function easingOutQuad(time) { + return 1 - (1 - time) * (1 - time); +} + +export function easingInOutQuad(time) { + return time < 0.5 ? 2 * time * time : 1 - Math.pow(-2 * time + 2, 2) / 2; +} + +// Quart + +export function easingInQuart(time) { + return time * time * time * time; +} + +export function easingOutQuart(time) { + return 1 - Math.pow(1 - time, 4); +} + +export function easingInOutQuart(time) { + return time < 0.5 ? 8 * time * time * time * time : 1 - Math.pow(-2 * time + 2, 4) / 2; +} + +// Expo + +export function easingInExpo(time) { + return time === 0 ? 0 : Math.pow(2, 10 * time - 10); +} + +export function easingOutExpo(time) { + return time === 1 ? 1 : 1 - Math.pow(2, -10 * time); +} + +export function easingInOutExpo(time) { + return time === 0 + ? 0 + : time === 1 + ? 1 + : time < 0.5 ? Math.pow(2, 20 * time - 10) / 2 + : (2 - Math.pow(2, -20 * time + 10)) / 2; +} + +// Back + +export function easingInBack(time) { + const check1 = 1.70158; + const check3 = check1 + 1; + + return check3 * time * time * time - check1 * time * time; +} + +export function easingOutBack(time) { + const check1 = 1.70158; + const check3 = check1 + 1; + + return 1 + check3 * Math.pow(time - 1, 3) + check1 * Math.pow(time - 1, 2); +} + +export function easingInOutBack(time) { + const check1 = 1.70158; + const check2 = check1 * 1.525; + + return time < 0.5 + ? (Math.pow(2 * time, 2) * ((check2 + 1) * 2 * time - check2)) / 2 + : (Math.pow(2 * time - 2, 2) * ((check2 + 1) * (time * 2 - 2) + check2) + 2) / 2; +} + +// Bounce + +export function easingInBounce(time) { + return 1 - easingOutBounce(1 - time); +} + +export function easingOutBounce(time) { + const night1 = 7.5625; + const dry1 = 2.75; + + if (time < 1 / dry1) { + return night1 * time * time; + } else if (time < 2 / dry1) { + return night1 * (time -= 1.5 / dry1) * time + 0.75; + } else if (time < 2.5 / dry1) { + return night1 * (time -= 2.25 / dry1) * time + 0.9375; + } else { + return night1 * (time -= 2.625 / dry1) * time + 0.984375; + } +} + +export function easingInOutBounce(time) { + return time < 0.5 + ? (1 - easingOutBounce(1 - 2 * time)) / 2 + : (1 + easingOutBounce(2 * time - 1)) / 2; +} diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index 18b79811..ad31bfc4 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -48,38 +48,52 @@ .dcf-slideshow[data-layout="cover-flow"] { container-type: inline-size; + position: relative; + overflow-x: hidden; +} +.dcf-slideshow[data-layout="cover-flow"]::before { + content: " "; + display: block; + position: absolute; + top: 0px; + bottom: 0px; + left: 49.5%; + right: 49.5%; + background-color: blue; + z-index: 999; } .dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck { list-style: none; - display: grid; - grid-auto-flow: column; - grid-auto-columns: max-content; - gap: 1rem; - overflow: hidden; - scroll-snap-type: x mandatory; + display: flex; + flex-flow: row nowrap; + // overflow: hidden; + position: relative; + // scroll-snap-type: x mandatory; margin: 0px; padding-block: 0px; - scroll-behavior: smooth; + // scroll-behavior: smooth; padding-inline: calc(50cqw - (33cqw / 2) + 2cqw); // We add +2 to fix some browser rounding issues } // /* List item setup */ .dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li { overflow: hidden; - scroll-snap-align: center; + // scroll-snap-align: center; margin: 0px; + margin-inline: 0px; padding: 0px; position: relative; - width: calc(33cqw * 2 / 3); + width: 25cqw; flex-shrink: 0; height: 100%; - transition: width 0.25s ease-in-out; + clip-path: inset(10% 10% 10% 10% round 14px); + transition: clip-path 0.5s ease-in-out, margin-inline 0.5s ease-in-out; } .dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li.active { - width: 33cqw; - height: 100%; + clip-path: inset(0% 0% 0% 0% round 14px); + margin-inline: 10%; } .dcf-slideshow-test-2 { From 1c8c34f5bef30bc171a4e354d4dbc161396d0bd4 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Fri, 19 Jun 2026 12:33:53 -0500 Subject: [PATCH 05/21] Added cover flow buttons and moved swapping slides logic into the actual functions --- js/components/dcf-slideshow.js | 155 +++++++++++++++++----------- scss/components-js/_slideshows.scss | 22 +++- 2 files changed, 116 insertions(+), 61 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index a57ea6e5..331c2594 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -255,28 +255,15 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.#shuffleSlides(); } this.#initSlides(); - - if (this.layout !== 'cover-flow') { - this.#initControls(); - } + this.#initControls(); if (this.layout === 'cover-flow') { - this.slides.forEach((singleSlide) => { + this.slides.forEach((singleSlide, index) => { singleSlide.addEventListener('click', () => { - singleSlide.classList.add('active'); - this.slides.forEach((slideToRemoveClass) => { - if (!slideToRemoveClass.isSameNode(singleSlide) && slideToRemoveClass.classList.contains('active')) { - slideToRemoveClass.classList.remove('active'); - } - }); - - if (this.scrollingId !== singleSlide.id) { - this.scrollingId = singleSlide.id; - this.smoothScrollToItem(singleSlide, singleSlide.id); - } + this.coverFlowSwapSlides(index); }); }); - this.slides[0].classList.add('active'); + this.coverFlowSwapSlides(0); } // This needs to go after init controls so we can change the state of the toggle button @@ -442,49 +429,74 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.prevButton = document.createElement('button'); this.nextButton = document.createElement('button'); - // Set up controls container - this.controlsContainer.classList.add('dcf-slideshow-controls'); - this.controlsContainer.classList.add(...this.slideButtonContainerClassList); - - // Set up previous button - this.prevButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-prev'); - this.prevButton.classList.add(...this.slidePrevBtnClassList); - this.prevButton.classList.add(...this.slideBtnClassList); - this.prevButton.innerHTML = this.slidePrevBtnInnerHTML; - this.prevButton.setAttribute('id', this.uuid.concat('-previous')); - this.prevButton.setAttribute('aria-label', 'Previous slide'); - this.prevButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); - this.prevButton.addEventListener('click', () => { - this.previousSlide(); - }); + if (this.layout !== 'cover-flow') { + // Set up controls container + this.controlsContainer.classList.add('dcf-slideshow-controls'); + this.controlsContainer.classList.add(...this.slideButtonContainerClassList); + + // Set up previous button + this.prevButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-prev'); + this.prevButton.classList.add(...this.slidePrevBtnClassList); + this.prevButton.classList.add(...this.slideBtnClassList); + this.prevButton.innerHTML = this.slidePrevBtnInnerHTML; + this.prevButton.setAttribute('id', this.uuid.concat('-previous')); + this.prevButton.setAttribute('aria-label', 'Previous slide'); + this.prevButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); + this.prevButton.addEventListener('click', () => { + this.previousSlide(); + }); - // Set up next button - this.nextButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-next'); - this.nextButton.classList.add(...this.slideNextBtnClassList); - this.nextButton.classList.add(...this.slideBtnClassList); - this.nextButton.innerHTML = this.slideNextBtnInnerHTML; - this.nextButton.setAttribute('id', this.uuid.concat('-next')); - this.nextButton.setAttribute('aria-label', 'Next slide'); - this.nextButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); - this.nextButton.addEventListener('click', () => { - this.nextSlide(); - }); + // Set up next button + this.nextButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-next'); + this.nextButton.classList.add(...this.slideNextBtnClassList); + this.nextButton.classList.add(...this.slideBtnClassList); + this.nextButton.innerHTML = this.slideNextBtnInnerHTML; + this.nextButton.setAttribute('id', this.uuid.concat('-next')); + this.nextButton.setAttribute('aria-label', 'Next slide'); + this.nextButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); + this.nextButton.addEventListener('click', () => { + this.nextSlide(); + }); - // If we allow play then set up the play button - if (this.allowPlay) { - this.playToggleButton = document.createElement('button'); - this.playToggleButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-prev'); - this.playToggleButton.classList.add(...this.slidePlayToggleBtnClassList); - this.playToggleButton.classList.add(...this.slideBtnClassList); - this.playToggleButton.innerHTML = this.slidePlayBtnInnerHTML; - this.playToggleButton.setAttribute('aria-label', 'Start automatic slideshow'); - this.playToggleButton.setAttribute('id', this.uuid.concat('-play-toggle')); - this.playToggleButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); - this.playToggleButton.addEventListener('click', () => { - this.togglePlayState(); + // If we allow play then set up the play button + if (this.allowPlay) { + this.playToggleButton = document.createElement('button'); + this.playToggleButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-prev'); + this.playToggleButton.classList.add(...this.slidePlayToggleBtnClassList); + this.playToggleButton.classList.add(...this.slideBtnClassList); + this.playToggleButton.innerHTML = this.slidePlayBtnInnerHTML; + this.playToggleButton.setAttribute('aria-label', 'Start automatic slideshow'); + this.playToggleButton.setAttribute('id', this.uuid.concat('-play-toggle')); + this.playToggleButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); + this.playToggleButton.addEventListener('click', () => { + this.togglePlayState(); + }); + } + } else { + this.controlsContainer.classList.add('dcf-slideshow-controls'); + this.controlsContainer.classList.add(...['dcf-absolute', 'dcf-d-flex', 'dcf-jc-between', 'dcf-top-50%', 'dcf-left-50%', 'dcf-z-1']); + + // Set up previous button + this.prevButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-circle', 'dcf-btn-slide', 'dcf-btn-slide-prev'); + this.prevButton.innerHTML = this.slidePrevBtnInnerHTML; + this.prevButton.setAttribute('id', this.uuid.concat('-previous')); + this.prevButton.setAttribute('aria-label', 'Previous slide'); + this.prevButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); + this.prevButton.addEventListener('click', () => { + this.previousSlide(); + }); + + this.nextButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-circle', 'dcf-btn-slide', 'dcf-btn-slide-next'); + this.nextButton.innerHTML = this.slideNextBtnInnerHTML; + this.nextButton.setAttribute('id', this.uuid.concat('-next')); + this.nextButton.setAttribute('aria-label', 'Next slide'); + this.nextButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); + this.nextButton.addEventListener('click', () => { + this.nextSlide(); }); } + // Add relative class for absolute positioning of slideshow controls this.slideshowContainer.classList.add('dcf-relative'); @@ -560,6 +572,25 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.currentSlide = newCurrentSlideIndex; } + coverFlowSwapSlides(newCurrentSlideIndex) { + this.currentSlide = newCurrentSlideIndex; + + this.slides[newCurrentSlideIndex].classList.add('active'); + this.slides.forEach((slideToRemoveClass) => { + if ( + !slideToRemoveClass.isSameNode(this.slides[newCurrentSlideIndex]) + && slideToRemoveClass.classList.contains('active') + ) { + slideToRemoveClass.classList.remove('active'); + } + }); + + if (this.scrollingId !== this.slides[newCurrentSlideIndex].id) { + this.scrollingId = this.slides[newCurrentSlideIndex].id; + this.smoothScrollToItem( this.slides[newCurrentSlideIndex], this.slides[newCurrentSlideIndex].id); + } + } + /** * Switches to the previous slide, will loop to end * @returns void @@ -574,7 +605,9 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> newCurrentSlideIndex = this.slides.length - 1; } - if (this.transition === 'fade') { + if (this.layout === 'cover-flow') { + this.coverFlowSwapSlides(newCurrentSlideIndex); + } else if (this.transition === 'fade') { this.#fadeSlides(newCurrentSlideIndex); } else { this.#swapSlides(newCurrentSlideIndex); @@ -595,7 +628,9 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> newCurrentSlideIndex = 0; } - if (this.transition === 'fade') { + if (this.layout === 'cover-flow') { + this.coverFlowSwapSlides(newCurrentSlideIndex); + } else if (this.transition === 'fade') { this.#fadeSlides(newCurrentSlideIndex); } else { this.#swapSlides(newCurrentSlideIndex); @@ -674,6 +709,10 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> throw new Error(`Slide ${index} does not exist`); } - this.#swapSlides(index); + if (this.layout === 'cover-flow') { + this.coverFlowSwapSlides(index); + } else { + this.#swapSlides(index); + } } } diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index ad31bfc4..bf270663 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -57,9 +57,20 @@ position: absolute; top: 0px; bottom: 0px; - left: 49.5%; - right: 49.5%; - background-color: blue; + left: 49.75%; + right: 49.75%; + background-color: rgba(0, 0, 255, 0.5); + z-index: 999; +} +.dcf-slideshow[data-layout="cover-flow"]::after { + content: " "; + display: block; + position: absolute; + right: 0px; + left: 0px; + top: 49.75%; + bottom: 49.75%; + background-color: rgba(0, 0, 255, 0.5); z-index: 999; } @@ -107,3 +118,8 @@ width: 100%; object-fit: cover; } + +.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls { + width: 35cqw; + transform: translate(-50%, -50%); +} From 7898c36180dfd6954e7dfa823950250fb974d916 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Fri, 19 Jun 2026 12:52:54 -0500 Subject: [PATCH 06/21] Added options for button layout --- js/components/dcf-slideshow.js | 30 ++++++++++++++++++++++------- scss/components-js/_slideshows.scss | 12 +++++++++++- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 331c2594..52370b93 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -37,6 +37,8 @@ export default class DCFSlideshow { layout = 'default'; + coverFlowButtonPosition = 'middle'; + scrollingId = null; slideContainerClassList = [ @@ -199,6 +201,13 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.layout = this.slideshowContainer.dataset.layout; } + if ( + this.slideshowContainer.dataset.buttonPosition === 'top' || + this.slideshowContainer.dataset.buttonPosition === 'bottom' + ) { + this.coverFlowButtonPosition = this.slideshowContainer.dataset.buttonPosition; + } + // If the tabGroup has no ID then it will set it if (this.slideshowContainer.getAttribute('id') === '' || this.slideshowContainer.getAttribute('id') === null) { this.slideshowContainer.setAttribute('id', this.uuid.concat('-slideshow')); @@ -260,10 +269,10 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if (this.layout === 'cover-flow') { this.slides.forEach((singleSlide, index) => { singleSlide.addEventListener('click', () => { - this.coverFlowSwapSlides(index); + this.#coverFlowSwapSlides(index); }); }); - this.coverFlowSwapSlides(0); + this.#coverFlowSwapSlides(0); } // This needs to go after init controls so we can change the state of the toggle button @@ -474,7 +483,14 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } } else { this.controlsContainer.classList.add('dcf-slideshow-controls'); - this.controlsContainer.classList.add(...['dcf-absolute', 'dcf-d-flex', 'dcf-jc-between', 'dcf-top-50%', 'dcf-left-50%', 'dcf-z-1']); + this.controlsContainer.classList.add(...['dcf-absolute', 'dcf-d-flex', 'dcf-jc-between', 'dcf-left-50%', 'dcf-z-1']); + if (this.coverFlowButtonPosition === 'top') { + this.controlsContainer.classList.add('dcf-slideshow-controls-top'); + } else if (this.coverFlowButtonPosition === 'bottom') { + this.controlsContainer.classList.add('dcf-slideshow-controls-bottom'); + } else { + this.controlsContainer.classList.add('dcf-top-50%'); + } // Set up previous button this.prevButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-circle', 'dcf-btn-slide', 'dcf-btn-slide-prev'); @@ -572,7 +588,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.currentSlide = newCurrentSlideIndex; } - coverFlowSwapSlides(newCurrentSlideIndex) { + #coverFlowSwapSlides(newCurrentSlideIndex) { this.currentSlide = newCurrentSlideIndex; this.slides[newCurrentSlideIndex].classList.add('active'); @@ -606,7 +622,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } if (this.layout === 'cover-flow') { - this.coverFlowSwapSlides(newCurrentSlideIndex); + this.#coverFlowSwapSlides(newCurrentSlideIndex); } else if (this.transition === 'fade') { this.#fadeSlides(newCurrentSlideIndex); } else { @@ -629,7 +645,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } if (this.layout === 'cover-flow') { - this.coverFlowSwapSlides(newCurrentSlideIndex); + this.#coverFlowSwapSlides(newCurrentSlideIndex); } else if (this.transition === 'fade') { this.#fadeSlides(newCurrentSlideIndex); } else { @@ -710,7 +726,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } if (this.layout === 'cover-flow') { - this.coverFlowSwapSlides(index); + this.#coverFlowSwapSlides(index); } else { this.#swapSlides(index); } diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index bf270663..9ea29a60 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -119,7 +119,17 @@ object-fit: cover; } -.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls { +.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls:not(.dcf-slideshow-controls-top):not(.dcf-slideshow-controls-bottom) { width: 35cqw; transform: translate(-50%, -50%); } +.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls.dcf-slideshow-controls-top { + top: 1rem; + width: calc(25cqw - (2 * 1rem)); + transform: translateX(-50%); +} +.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls.dcf-slideshow-controls-bottom { + bottom: 1rem; + width: calc(25cqw - (2 * 1rem)); + transform: translateX(-50%); +} From 9744e69f0d5972e1d1f097b6c68f1d6be31610e6 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Fri, 19 Jun 2026 15:02:20 -0500 Subject: [PATCH 07/21] Set up styles and CSS variables and fixed bug with resizing window --- js/components/dcf-slideshow.js | 78 ++++++++++++++++++--------- scss/components-js/_slideshows.scss | 84 +++++++++-------------------- 2 files changed, 76 insertions(+), 86 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 52370b93..8846e383 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -188,19 +188,20 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> // Set up slide show container this.slideshowContainer = slideshowContainer; + if (this.slideshowContainer.dataset.layout === 'cover-flow') { + this.layout = this.slideshowContainer.dataset.layout; + } + if (this.slideshowContainer.tagName !== 'SECTION') { this.slideshowContainer.setAttribute('role', 'region'); } this.slideshowContainer.setAttribute('aria-roledescription', 'carousel'); - this.slideshowContainer.classList.add('dcf-slideshow-initialized'); - if (this.layout !== 'cover-flow') { + if (this.layout === 'cover-flow') { + this.slideshowContainer.classList.add(...['dcf-relative', 'dcf-overflow-x-hidden']); + } else { this.slideshowContainer.classList.add(...this.slideContainerClassList); } - if (this.slideshowContainer.dataset.layout === 'cover-flow') { - this.layout = this.slideshowContainer.dataset.layout; - } - if ( this.slideshowContainer.dataset.buttonPosition === 'top' || this.slideshowContainer.dataset.buttonPosition === 'bottom' @@ -221,7 +222,9 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if (this.slideDeck.getAttribute('id') === '' || this.slideDeck.getAttribute('id') === null) { this.slideDeck.setAttribute('id', this.uuid.concat('-slide-deck')); } - if (this.layout !== 'cover-flow') { + if (this.layout === 'cover-flow') { + this.slideDeck.classList.add(...['dcf-d-flex', 'dcf-flex-row', 'dcf-flex-nowrap', 'dcf-relative', 'dcf-m-0', 'dcf-pt-0', 'dcf-pb-0']); + } else { this.slideDeck.classList.add(...this.slideDeckClassList); } @@ -267,12 +270,11 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.#initControls(); if (this.layout === 'cover-flow') { - this.slides.forEach((singleSlide, index) => { - singleSlide.addEventListener('click', () => { - this.#coverFlowSwapSlides(index); - }); + // If we resize then the active slide might not be in the middle + // so we will need to re-scroll to it + window.addEventListener('resize', () => { + this.#coverFlowSwapSlides(this.currentSlide, true); }); - this.#coverFlowSwapSlides(0); } // This needs to go after init controls so we can change the state of the toggle button @@ -297,9 +299,19 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> })); } + jumpScrollToItem(item) { + const newLeft = this.calculateSlideDeckOffset(item); + this.slideDeck.style.left = `${newLeft}px`; + } + smoothScrollToItem(item, currentId) { - //!IMPORTANT: This needs to be a little longer than the animation to grow the item - const scrollDurationMs = 505; + //!IMPORTANT: This needs to be a little longer than the transition to grow the item + let scrollDurationMs = parseFloat(window.getComputedStyle(this.slideshowContainer).getPropertyValue('--transition-time')); + if (Number.isNaN(scrollDurationMs)) { + scrollDurationMs = 500; + } else if (scrollDurationMs < 100) { + scrollDurationMs = scrollDurationMs * 1000; + } let scrollProgressMs = 0; let oldLeft = parseFloat(this.slideDeck.style.getPropertyValue('left')); if (Number.isNaN(oldLeft)) { @@ -320,8 +332,6 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if (scrollProgressMs < scrollDurationMs) { const easedPercent = easingInOutCubic(scrollProgressMs / scrollDurationMs); const lerpLeft = lerp(oldLeft, newLeft, easedPercent); - console.log('lerpLeft', lerpLeft); - this.slideDeck.style.left = `${lerpLeft}px`; window.requestAnimationFrame(animationLoop); @@ -388,21 +398,35 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> // Set up slide slide.setAttribute('id', this.uuid.concat('-slide-', slideIndex)); slide.classList.add('dcf-slide'); - if (this.layout !== 'cover-flow') { + if (this.layout === 'cover-flow') { + slide.classList.add(...['dcf-relative', 'dcf-overflow-hidden', 'dcf-mt-0', 'dcf-mb-0', 'dcf-p-0', 'dcf-flex-shrink-0', 'dcf-h-100%']); + } else { slide.classList.add(...this.slideClassList); } slide.setAttribute('aria-roledescription', 'slide'); slide.setAttribute('aria-label', `${slideIndex + 1} of ${this.slides.length}`); - if (this.layout !== 'cover-flow') { + + // Cover-flow layout will let us see all the slides so if we click on one then jump to it + if (slideIndex !== this.currentSlide && this.layout === 'cover-flow') { + slide.addEventListener('click', () => { + this.#coverFlowSwapSlides(slideIndex); + }); + } else if (this.layout === 'cover-flow') { + slide.addEventListener('click', () => { + this.#coverFlowSwapSlides(slideIndex); + }); + + // This is the selected slide so go to it + this.#coverFlowSwapSlides(slideIndex); + } else if (slideIndex !== this.currentSlide) { + // If we are not the current slide then hide it - if (slideIndex !== this.currentSlide) { - slide.classList.add('dcf-d-none'); - slide.classList.add('dcf-z-0'); - } else { - slide.classList.add('dcf-z-1'); - } + slide.classList.add('dcf-d-none'); + slide.classList.add('dcf-z-0'); + } else { + slide.classList.add('dcf-z-1'); } // Figure out if we need to do figcaption toggles @@ -588,7 +612,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.currentSlide = newCurrentSlideIndex; } - #coverFlowSwapSlides(newCurrentSlideIndex) { + #coverFlowSwapSlides(newCurrentSlideIndex, jump = false) { this.currentSlide = newCurrentSlideIndex; this.slides[newCurrentSlideIndex].classList.add('active'); @@ -601,7 +625,9 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } }); - if (this.scrollingId !== this.slides[newCurrentSlideIndex].id) { + if (jump) { + this.jumpScrollToItem(this.slides[newCurrentSlideIndex]); + } else if (this.scrollingId !== this.slides[newCurrentSlideIndex].id) { this.scrollingId = this.slides[newCurrentSlideIndex].id; this.smoothScrollToItem( this.slides[newCurrentSlideIndex], this.slides[newCurrentSlideIndex].id); } diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index 9ea29a60..f1b89836 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -48,88 +48,52 @@ .dcf-slideshow[data-layout="cover-flow"] { container-type: inline-size; - position: relative; - overflow-x: hidden; -} -.dcf-slideshow[data-layout="cover-flow"]::before { - content: " "; - display: block; - position: absolute; - top: 0px; - bottom: 0px; - left: 49.75%; - right: 49.75%; - background-color: rgba(0, 0, 255, 0.5); - z-index: 999; -} -.dcf-slideshow[data-layout="cover-flow"]::after { - content: " "; - display: block; - position: absolute; - right: 0px; - left: 0px; - top: 49.75%; - bottom: 49.75%; - background-color: rgba(0, 0, 255, 0.5); - z-index: 999; + --item-width: 25cqw; + --button-padding: 1rem; + --clip-path-inline: 10%; + --clip-path-block: 10%; + --transition-time: 400ms; //!IMPORTANT this is referenced in the JS + --transition-easing: cubic-bezier(.25, .46, .45, .94); } .dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck { - list-style: none; - display: flex; - flex-flow: row nowrap; - // overflow: hidden; - position: relative; - // scroll-snap-type: x mandatory; - margin: 0px; - padding-block: 0px; - // scroll-behavior: smooth; - padding-inline: calc(50cqw - (33cqw / 2) + 2cqw); // We add +2 to fix some browser rounding issues + padding-inline: calc(50cqw - (var(--item-width) / 2) + 2cqw); // We add +2 to fix some browser rounding issues } // /* List item setup */ .dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li { - overflow: hidden; - // scroll-snap-align: center; - margin: 0px; - margin-inline: 0px; - padding: 0px; - position: relative; - width: 25cqw; - flex-shrink: 0; - height: 100%; - clip-path: inset(10% 10% 10% 10% round 14px); - transition: clip-path 0.5s ease-in-out, margin-inline 0.5s ease-in-out; + width: var(--item-width); + clip-path: inset(var(--clip-path-block) var(--clip-path-inline) var(--clip-path-block) var(--clip-path-inline) round var.$roundrect); + transition: + clip-path var(--transition-time) var(--transition-easing), + margin-inline var(--transition-time) var(--transition-easing); } .dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li.active { - clip-path: inset(0% 0% 0% 0% round 14px); - margin-inline: 10%; + clip-path: inset(0% 0% 0% 0% round var.$roundrect); + margin-inline: var(--clip-path-inline); } -.dcf-slideshow-test-2 { - height: 100%; - width: 100%; +.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li .dcf-slideshow-nonactive-hide { + opacity: 0; + transition: opacity var(--transition-time) var(--transition-easing); } -.dcf-slideshow-test { - aspect-ratio: 9 / 16; - height: 100%; - width: 100%; - object-fit: cover; +.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li.active .dcf-slideshow-nonactive-hide { + opacity: 1; } .dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls:not(.dcf-slideshow-controls-top):not(.dcf-slideshow-controls-bottom) { - width: 35cqw; + width: calc(var(--item-width) * 1.4); transform: translate(-50%, -50%); } .dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls.dcf-slideshow-controls-top { - top: 1rem; - width: calc(25cqw - (2 * 1rem)); + top: var(--button-padding); + width: calc(var(--item-width) - (2 * var(--button-padding))); transform: translateX(-50%); } .dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls.dcf-slideshow-controls-bottom { - bottom: 1rem; - width: calc(25cqw - (2 * 1rem)); + bottom: var(--button-padding); + width: calc(var(--item-width) - (2 * var(--button-padding))); transform: translateX(-50%); } From 550e119930cebc5e62026905d8e7380c0f39b881 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Fri, 19 Jun 2026 15:29:29 -0500 Subject: [PATCH 08/21] Fix button spacing --- scss/components-js/_slideshows.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index f1b89836..3a03df14 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -84,9 +84,15 @@ } .dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls:not(.dcf-slideshow-controls-top):not(.dcf-slideshow-controls-bottom) { - width: calc(var(--item-width) * 1.4); + width: calc(var(--item-width) + var(--button-padding)); transform: translate(-50%, -50%); } +.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls:not(.dcf-slideshow-controls-top):not(.dcf-slideshow-controls-bottom) .dcf-btn-slide-prev { + transform: translateX(-100%); +} +.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls:not(.dcf-slideshow-controls-top):not(.dcf-slideshow-controls-bottom) .dcf-btn-slide-next { + transform: translateX(100%); +} .dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls.dcf-slideshow-controls-top { top: var(--button-padding); width: calc(var(--item-width) - (2 * var(--button-padding))); From 1a9057e1b996b24821633ad15841a6ad0a661a03 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Fri, 19 Jun 2026 15:49:41 -0500 Subject: [PATCH 09/21] Adjust the item width to make it work better for phones and big monitors --- scss/components-js/_slideshows.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index 3a03df14..ddf1ecc5 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -48,7 +48,7 @@ .dcf-slideshow[data-layout="cover-flow"] { container-type: inline-size; - --item-width: 25cqw; + --item-width: clamp(15rem, 25cqw, 20rem); --button-padding: 1rem; --clip-path-inline: 10%; --clip-path-block: 10%; From 5a37c9ff2bcb80cf0b32a27d268ab90901896a84 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Tue, 23 Jun 2026 09:25:29 -0500 Subject: [PATCH 10/21] Add swipe gestures --- js/components/dcf-slideshow.js | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 8846e383..a32a89a7 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -275,6 +275,41 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> window.addEventListener('resize', () => { this.#coverFlowSwapSlides(this.currentSlide, true); }); + + // Add swipe gestures for mobile + const mouseStatus = { + down: false, + xPos: 0, + yPos: 0, + }; + this.slideshowContainer.addEventListener('pointerdown', (event) => { + if (event.target.closest('dcf-btn-slide')) { + return; + } + mouseStatus.down = true; + mouseStatus.xPos = event.clientX; + mouseStatus.yPos = event.clientY; + }); + this.slideshowContainer.addEventListener('pointermove', (event) => { + if (mouseStatus.down === false) { + return; + } + event.preventDefault(); + if (event.clientX - mouseStatus.xPos > 100) { + this.previousSlide(); + mouseStatus.down = false; + } + if (event.clientX - mouseStatus.xPos < -100) { + this.nextSlide(); + mouseStatus.down = false; + } + }); + this.slideshowContainer.addEventListener('pointerup', () => { + mouseStatus.down = false; + }); + this.slideshowContainer.addEventListener('pointerleave', () => { + mouseStatus.down = false; + }); } // This needs to go after init controls so we can change the state of the toggle button From c17d28d90d0923bc06fde7753eaeef0040116b36 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Tue, 23 Jun 2026 09:27:05 -0500 Subject: [PATCH 11/21] Fix logic in if statement --- js/components/dcf-slideshow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index a32a89a7..9cbad83b 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -283,7 +283,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> yPos: 0, }; this.slideshowContainer.addEventListener('pointerdown', (event) => { - if (event.target.closest('dcf-btn-slide')) { + if (event.target.closest('.dcf-btn-slide') !== null) { return; } mouseStatus.down = true; From c4153e7c60866fe61caa466d8584d8cbd87815eb Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Tue, 23 Jun 2026 09:48:36 -0500 Subject: [PATCH 12/21] Disable swipe gesture with mouse --- js/components/dcf-slideshow.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 9cbad83b..a7acc126 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -286,6 +286,9 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if (event.target.closest('.dcf-btn-slide') !== null) { return; } + if (event.pointerType === 'mouse') { + return; + } mouseStatus.down = true; mouseStatus.xPos = event.clientX; mouseStatus.yPos = event.clientY; From 9ef401d0c547b3cadd1b3302f2ca84347a129c64 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Tue, 23 Jun 2026 10:25:12 -0500 Subject: [PATCH 13/21] Add infinite scrolling option --- js/components/dcf-slideshow.js | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index a7acc126..444fe81b 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -267,6 +267,12 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.#shuffleSlides(); } this.#initSlides(); + if ( + this.slideshowContainer.hasAttribute('data-infinite') && + this.slideshowContainer.dataset.infinite.toLowerCase() === 'true' + ) { + this.#setupInfiniteScroll(); + } this.#initControls(); if (this.layout === 'cover-flow') { @@ -428,6 +434,38 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> }); } + #setupInfiniteScroll() { + const originalLength = this.slides.length; + const numCopies = 5; + + for (let copyCount = 0; copyCount < numCopies; copyCount++) { + const prevList = []; + this.slides.forEach((slide) => { + slide.dataset.original = true; + const newSlidePrev = slide.cloneNode(true); + newSlidePrev.removeAttribute('data-original'); + const newSlideNext = slide.cloneNode(true); + newSlideNext.removeAttribute('data-original'); + prevList.push(newSlidePrev); + this.slideDeck.append(newSlideNext); + }); + prevList.reverse().forEach((newSlidePrev) => { + this.slideDeck.prepend(newSlidePrev); + }); + } + + this.slides = Array.from(this.slideDeck.children); + this.currentSlide = numCopies * originalLength; + this.#coverFlowSwapSlides(this.currentSlide); + + // Loop through all the slides + // Give the original slides some data attribute so we can get them later + // Append/prepend each slide a couple times + // Reset slides + // Reset currentSlide to the first original slide + + } + /** * Initializes the slides */ From 09969251c5b69a075f1d84d7e0e6d8c4fa4f7d34 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Tue, 23 Jun 2026 10:53:30 -0500 Subject: [PATCH 14/21] Comment code, clean up methods, moved class lists to properties --- js/components/dcf-slideshow.js | 315 +++++++++++++++++++++------------ 1 file changed, 203 insertions(+), 112 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 444fe81b..4dcc748a 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -45,6 +45,11 @@ export default class DCFSlideshow { 'dcf-relative', ]; + slideContainerCoverFlowClassList = [ + 'dcf-relative', + 'dcf-overflow-x-hidden', + ]; + slideDeckClassList = [ 'dcf-d-grid', 'dcf-grid-cols-1', @@ -55,6 +60,16 @@ export default class DCFSlideshow { 'dcf-h-100%', ]; + slideDeckCoverFlowClassList = [ + 'dcf-d-flex', + 'dcf-flex-row', + 'dcf-flex-nowrap', + 'dcf-relative', + 'dcf-m-0', + 'dcf-pt-0', + 'dcf-pb-0', + ]; + slideButtonContainerClassList = [ 'dcf-btn-group', 'dcf-absolute', @@ -62,10 +77,28 @@ export default class DCFSlideshow { 'dcf-top-0', ]; + slideButtonContainerCoverFlowClassList = [ + 'dcf-absolute', + 'dcf-d-flex', + 'dcf-jc-between', + 'dcf-left-50%', + 'dcf-z-1', + ]; + slideClassList = [ 'dcf-mb-0', ]; + slideCoverFlowClassList = [ + 'dcf-relative', + 'dcf-overflow-hidden', + 'dcf-mt-0', + 'dcf-mb-0', + 'dcf-p-0', + 'dcf-flex-shrink-0', + 'dcf-h-100%', + ]; + slideBtnClassList = [ 'dcf-d-flex', 'dcf-ai-center', @@ -74,6 +107,10 @@ export default class DCFSlideshow { 'dcf-white', ]; + slideBtnCoverFlowClassList = [ + 'dcf-circle', + ]; + slidePrevBtnClassList = [ 'dcf-d-flex', 'dcf-ai-center', @@ -149,18 +186,33 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if ('slideContainerClassList' in options && Array.isArray(options.slideContainerClassList)) { this.slideContainerClassList = options.slideContainerClassList; } + if ('slideContainerCoverFlowClassList' in options && Array.isArray(options.slideContainerCoverFlowClassList)) { + this.slideContainerCoverFlowClassList = options.slideContainerCoverFlowClassList; + } if ('slideDeckClassList' in options && Array.isArray(options.slideDeckClassList)) { this.slideDeckClassList = options.slideDeckClassList; } + if ('slideDeckCoverFlowClassList' in options && Array.isArray(options.slideDeckCoverFlowClassList)) { + this.slideDeckCoverFlowClassList = options.slideDeckCoverFlowClassList; + } if ('slideButtonContainerClassList' in options && Array.isArray(options.slideButtonContainerClassList)) { this.slideButtonContainerClassList = options.slideButtonContainerClassList; } + if ('slideButtonContainerCoverFlowClassList' in options && Array.isArray(options.slideButtonContainerCoverFlowClassList)) { + this.slideButtonContainerCoverFlowClassList = options.slideButtonContainerCoverFlowClassList; + } if ('slideClassList' in options && Array.isArray(options.slideClassList)) { this.slideClassList = options.slideClassList; } + if ('slideCoverFlowClassList' in options && Array.isArray(options.slideCoverFlowClassList)) { + this.slideCoverFlowClassList = options.slideCoverFlowClassList; + } if ('slideBtnClassList' in options && Array.isArray(options.slideBtnClassList)) { this.slideBtnClassList = options.slideBtnClassList; } + if ('slideBtnCoverFlowClassList' in options && Array.isArray(options.slideBtnCoverFlowClassList)) { + this.slideBtnCoverFlowClassList = options.slideBtnCoverFlowClassList; + } if ('slidePrevBtnClassList' in options && Array.isArray(options.slidePrevBtnClassList)) { this.slidePrevBtnClassList = options.slidePrevBtnClassList; } @@ -197,7 +249,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } this.slideshowContainer.setAttribute('aria-roledescription', 'carousel'); if (this.layout === 'cover-flow') { - this.slideshowContainer.classList.add(...['dcf-relative', 'dcf-overflow-x-hidden']); + this.slideshowContainer.classList.add(...this.slideContainerCoverFlowClassList); } else { this.slideshowContainer.classList.add(...this.slideContainerClassList); } @@ -223,7 +275,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.slideDeck.setAttribute('id', this.uuid.concat('-slide-deck')); } if (this.layout === 'cover-flow') { - this.slideDeck.classList.add(...['dcf-d-flex', 'dcf-flex-row', 'dcf-flex-nowrap', 'dcf-relative', 'dcf-m-0', 'dcf-pt-0', 'dcf-pb-0']); + this.slideDeck.classList.add(...this.slideDeckCoverFlowClassList); } else { this.slideDeck.classList.add(...this.slideDeckClassList); } @@ -343,61 +395,6 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> })); } - jumpScrollToItem(item) { - const newLeft = this.calculateSlideDeckOffset(item); - this.slideDeck.style.left = `${newLeft}px`; - } - - smoothScrollToItem(item, currentId) { - //!IMPORTANT: This needs to be a little longer than the transition to grow the item - let scrollDurationMs = parseFloat(window.getComputedStyle(this.slideshowContainer).getPropertyValue('--transition-time')); - if (Number.isNaN(scrollDurationMs)) { - scrollDurationMs = 500; - } else if (scrollDurationMs < 100) { - scrollDurationMs = scrollDurationMs * 1000; - } - let scrollProgressMs = 0; - let oldLeft = parseFloat(this.slideDeck.style.getPropertyValue('left')); - if (Number.isNaN(oldLeft)) { - oldLeft = 0; - } - let previousLoopTime = Date.now(); - const animationLoop = () => { - if (this.scrollingId !== currentId) { - return; - } - - const startLoopTime = Date.now(); - const deltaTime = startLoopTime - previousLoopTime; - scrollProgressMs += deltaTime; - - const newLeft = this.calculateSlideDeckOffset(item); - - if (scrollProgressMs < scrollDurationMs) { - const easedPercent = easingInOutCubic(scrollProgressMs / scrollDurationMs); - const lerpLeft = lerp(oldLeft, newLeft, easedPercent); - this.slideDeck.style.left = `${lerpLeft}px`; - - window.requestAnimationFrame(animationLoop); - } else { - this.slideDeck.style.left = `${newLeft}px`; - this.scrollingId = null; - } - previousLoopTime = startLoopTime; - }; - - window.requestAnimationFrame(animationLoop); - } - - calculateSlideDeckOffset(item) { - const itemMidPoint = item.offsetWidth / 2; - const itemOffsetToList = item.offsetLeft; - const wrapperMidPoint = this.slideshowContainer.offsetWidth / 2; - - const getMidItemToLeftEdge = (-1 * ( itemOffsetToList + itemMidPoint)); - return getMidItemToLeftEdge + wrapperMidPoint; - } - /** * Validates and returns standardized name of events for tabs * @static @@ -475,7 +472,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> slide.setAttribute('id', this.uuid.concat('-slide-', slideIndex)); slide.classList.add('dcf-slide'); if (this.layout === 'cover-flow') { - slide.classList.add(...['dcf-relative', 'dcf-overflow-hidden', 'dcf-mt-0', 'dcf-mb-0', 'dcf-p-0', 'dcf-flex-shrink-0', 'dcf-h-100%']); + slide.classList.add(...this.slideCoverFlowClassList); } else { slide.classList.add(...this.slideClassList); } @@ -538,34 +535,57 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.prevButton = document.createElement('button'); this.nextButton = document.createElement('button'); - if (this.layout !== 'cover-flow') { - // Set up controls container - this.controlsContainer.classList.add('dcf-slideshow-controls'); + // Set up controls container + this.controlsContainer.classList.add('dcf-slideshow-controls'); + + // Set up previous button + this.prevButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-prev'); + this.prevButton.innerHTML = this.slidePrevBtnInnerHTML; + this.prevButton.setAttribute('id', this.uuid.concat('-previous')); + this.prevButton.setAttribute('aria-label', 'Previous slide'); + this.prevButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); + this.prevButton.addEventListener('click', () => { + this.previousSlide(); + }); + + // Set up next button + this.nextButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-next'); + this.nextButton.innerHTML = this.slideNextBtnInnerHTML; + this.nextButton.setAttribute('id', this.uuid.concat('-next')); + this.nextButton.setAttribute('aria-label', 'Next slide'); + this.nextButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); + this.nextButton.addEventListener('click', () => { + this.nextSlide(); + }); + + // Set cover flow specific classes + if (this.layout === 'cover-flow') { + + // Set the classes for the button container + this.controlsContainer.classList.add(...this.slideButtonContainerCoverFlowClassList); + if (this.coverFlowButtonPosition === 'top') { + this.controlsContainer.classList.add('dcf-slideshow-controls-top'); + } else if (this.coverFlowButtonPosition === 'bottom') { + this.controlsContainer.classList.add('dcf-slideshow-controls-bottom'); + } else { + this.controlsContainer.classList.add('dcf-top-50%'); + } + + // Set the classes for the prev and next buttons + this.prevButton.classList.add(...this.slideBtnCoverFlowClassList); + this.nextButton.classList.add(...this.slideBtnCoverFlowClassList); + + // If we are in a normal layout + } else { + + // Set the classes for the button container this.controlsContainer.classList.add(...this.slideButtonContainerClassList); - // Set up previous button - this.prevButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-prev'); + // Set the classes for the prev and next buttons this.prevButton.classList.add(...this.slidePrevBtnClassList); this.prevButton.classList.add(...this.slideBtnClassList); - this.prevButton.innerHTML = this.slidePrevBtnInnerHTML; - this.prevButton.setAttribute('id', this.uuid.concat('-previous')); - this.prevButton.setAttribute('aria-label', 'Previous slide'); - this.prevButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); - this.prevButton.addEventListener('click', () => { - this.previousSlide(); - }); - - // Set up next button - this.nextButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-btn-slide', 'dcf-btn-slide-next'); this.nextButton.classList.add(...this.slideNextBtnClassList); this.nextButton.classList.add(...this.slideBtnClassList); - this.nextButton.innerHTML = this.slideNextBtnInnerHTML; - this.nextButton.setAttribute('id', this.uuid.concat('-next')); - this.nextButton.setAttribute('aria-label', 'Next slide'); - this.nextButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); - this.nextButton.addEventListener('click', () => { - this.nextSlide(); - }); // If we allow play then set up the play button if (this.allowPlay) { @@ -581,38 +601,8 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.togglePlayState(); }); } - } else { - this.controlsContainer.classList.add('dcf-slideshow-controls'); - this.controlsContainer.classList.add(...['dcf-absolute', 'dcf-d-flex', 'dcf-jc-between', 'dcf-left-50%', 'dcf-z-1']); - if (this.coverFlowButtonPosition === 'top') { - this.controlsContainer.classList.add('dcf-slideshow-controls-top'); - } else if (this.coverFlowButtonPosition === 'bottom') { - this.controlsContainer.classList.add('dcf-slideshow-controls-bottom'); - } else { - this.controlsContainer.classList.add('dcf-top-50%'); - } - - // Set up previous button - this.prevButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-circle', 'dcf-btn-slide', 'dcf-btn-slide-prev'); - this.prevButton.innerHTML = this.slidePrevBtnInnerHTML; - this.prevButton.setAttribute('id', this.uuid.concat('-previous')); - this.prevButton.setAttribute('aria-label', 'Previous slide'); - this.prevButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); - this.prevButton.addEventListener('click', () => { - this.previousSlide(); - }); - - this.nextButton.classList.add('dcf-btn', 'dcf-btn-primary', 'dcf-circle', 'dcf-btn-slide', 'dcf-btn-slide-next'); - this.nextButton.innerHTML = this.slideNextBtnInnerHTML; - this.nextButton.setAttribute('id', this.uuid.concat('-next')); - this.nextButton.setAttribute('aria-label', 'Next slide'); - this.nextButton.setAttribute('aria-controls', this.slideDeck.getAttribute('id')); - this.nextButton.addEventListener('click', () => { - this.nextSlide(); - }); } - // Add relative class for absolute positioning of slideshow controls this.slideshowContainer.classList.add('dcf-relative'); @@ -688,10 +678,18 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.currentSlide = newCurrentSlideIndex; } + /** + * In cover flow layout adjust the left position of the slide deck to "scroll" to image + * @param { Number } newCurrentSlideIndex The new slide which to scroll to + * @param { Boolean } jump Do the animation to scroll, or jump right to it and avoid animation + */ #coverFlowSwapSlides(newCurrentSlideIndex, jump = false) { this.currentSlide = newCurrentSlideIndex; + // Add the active class to start that transition this.slides[newCurrentSlideIndex].classList.add('active'); + + // Remove the active class from all the other slides this.slides.forEach((slideToRemoveClass) => { if ( !slideToRemoveClass.isSameNode(this.slides[newCurrentSlideIndex]) @@ -701,12 +699,105 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } }); + // "Scroll" to the the new item if (jump) { - this.jumpScrollToItem(this.slides[newCurrentSlideIndex]); + this.#jumpScrollToItem(this.slides[newCurrentSlideIndex]); } else if (this.scrollingId !== this.slides[newCurrentSlideIndex].id) { this.scrollingId = this.slides[newCurrentSlideIndex].id; - this.smoothScrollToItem( this.slides[newCurrentSlideIndex], this.slides[newCurrentSlideIndex].id); + this.#smoothScrollToItem( this.slides[newCurrentSlideIndex], this.slides[newCurrentSlideIndex].id); + } + } + + /** + * We calculate the new left position of the slide deck and we jump right to it + * @param { HTMLElement } slideToJumpTo The slide to jump to + */ + #jumpScrollToItem(slideToJumpTo) { + const newLeft = this.#calculateSlideDeckOffset(slideToJumpTo); + this.slideDeck.style.left = `${newLeft}px`; + } + + /** + * We calculate the new left position of the slide deck and we smoothly animate to it + * we also recalculate the scroll position in case it changes like the widths of the slides change + * @param { HTMLElement } slideToScrollTo The slide to scroll to + * @param { String } thisScrollsId The id of this scroll, this is to cancel the animation if needed + */ + #smoothScrollToItem(slideToScrollTo, thisScrollsId) { + //!IMPORTANT: This needs to be a little longer than the transition to grow the item + let scrollDurationMs = parseFloat(window.getComputedStyle(this.slideshowContainer).getPropertyValue('--transition-time')); + if (Number.isNaN(scrollDurationMs)) { + scrollDurationMs = 500; + } else if (scrollDurationMs < 100) { + scrollDurationMs = scrollDurationMs * 1000; + } + let scrollProgressMs = 0; + + // Get the starting point for the animation + let oldLeft = parseFloat(this.slideDeck.style.getPropertyValue('left')); + if (Number.isNaN(oldLeft)) { + oldLeft = 0; } + + // We use this to determine how much time between animation frames we had + let previousLoopTime = Date.now(); + const animationLoop = () => { + // If we started a new animation then cancel this one + if (this.scrollingId !== thisScrollsId) { + return; + } + + // Calculate how long between loops we were and increment the progress time + const startLoopTime = Date.now(); + const deltaTime = startLoopTime - previousLoopTime; + scrollProgressMs += deltaTime; + + // Figure out where we need to scroll to + const newLeft = this.#calculateSlideDeckOffset(slideToScrollTo); + + // If we are still in the animation + if (scrollProgressMs < scrollDurationMs) { + + // Use the easing function to do a nicer animation + const easedPercent = easingInOutCubic(scrollProgressMs / scrollDurationMs); + + // Based on the end, start, and how far into the animation we are + // determine where we need to set the left value to + const lerpLeft = lerp(oldLeft, newLeft, easedPercent); + + // Set the left style to simulate a scroll + this.slideDeck.style.left = `${lerpLeft}px`; + + // Request the next animation frame for the next loop + window.requestAnimationFrame(animationLoop); + + // If the animation has finished + } else { + + // Once the animation set the left to what the target endpoint it + // this is in case the scrollProgressMs makes the animation percent not 100% + this.slideDeck.style.left = `${newLeft}px`; + this.scrollingId = null; + } + previousLoopTime = startLoopTime; + }; + + // Start the animation loop + window.requestAnimationFrame(animationLoop); + } + + /** + * Determine where the slide deck's left style needs to be to have the slide in the middle of the container + * @param { HTMLElement } targetSlide Slide we are calculating for + * @returns { Number } The px that the left style of the slide deck should be set to + */ + #calculateSlideDeckOffset(targetSlide) { + const slideMidPoint = targetSlide.offsetWidth / 2; + const slideOffsetToList = targetSlide.offsetLeft; + const wrapperMidPoint = this.slideshowContainer.offsetWidth / 2; + + const getMidItemToLeftEdge = (-1 * ( slideOffsetToList + slideMidPoint)); + return getMidItemToLeftEdge + wrapperMidPoint; } /** From 86dc1b82f4593eebd6f41132618b1b2f54963c74 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Tue, 23 Jun 2026 11:45:59 -0500 Subject: [PATCH 15/21] Fix event listeners on infinite scrolling --- js/components/dcf-slideshow.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 4dcc748a..0f4a9f56 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -41,6 +41,8 @@ export default class DCFSlideshow { scrollingId = null; + infiniteSlideOriginalLength = 0; + slideContainerClassList = [ 'dcf-relative', ]; @@ -318,13 +320,13 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> ) { this.#shuffleSlides(); } - this.#initSlides(); if ( this.slideshowContainer.hasAttribute('data-infinite') && this.slideshowContainer.dataset.infinite.toLowerCase() === 'true' ) { this.#setupInfiniteScroll(); } + this.#initSlides(); this.#initControls(); if (this.layout === 'cover-flow') { @@ -432,7 +434,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } #setupInfiniteScroll() { - const originalLength = this.slides.length; + this.infiniteSlideOriginalLength = this.slides.length; const numCopies = 5; for (let copyCount = 0; copyCount < numCopies; copyCount++) { @@ -452,15 +454,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } this.slides = Array.from(this.slideDeck.children); - this.currentSlide = numCopies * originalLength; - this.#coverFlowSwapSlides(this.currentSlide); - - // Loop through all the slides - // Give the original slides some data attribute so we can get them later - // Append/prepend each slide a couple times - // Reset slides - // Reset currentSlide to the first original slide - + this.currentSlide = numCopies * this.infiniteSlideOriginalLength; } /** @@ -478,8 +472,12 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } slide.setAttribute('aria-roledescription', 'slide'); - slide.setAttribute('aria-label', `${slideIndex + 1} of ${this.slides.length}`); - + if (this.infiniteSlideOriginalLength !== 0) { + const originalSlideIndex = (slideIndex % this.infiniteSlideOriginalLength) + 1; + slide.setAttribute('aria-label', `${originalSlideIndex} of ${this.infiniteSlideOriginalLength}`); + } else { + slide.setAttribute('aria-label', `${slideIndex + 1} of ${this.slides.length}`); + } // Cover-flow layout will let us see all the slides so if we click on one then jump to it if (slideIndex !== this.currentSlide && this.layout === 'cover-flow') { From 5a991f936121d454bbbbb4b1098b68dda96b2e44 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Tue, 23 Jun 2026 13:15:04 -0500 Subject: [PATCH 16/21] Preliminary multi-view layout --- js/components/dcf-slideshow.js | 52 +++++++++++++++++++++++------ scss/components-js/_slideshows.scss | 29 +++++++++++++--- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 0f4a9f56..ceb504c8 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -244,13 +244,15 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.slideshowContainer = slideshowContainer; if (this.slideshowContainer.dataset.layout === 'cover-flow') { this.layout = this.slideshowContainer.dataset.layout; + } else if (this.slideshowContainer.dataset.layout === 'multi-view') { + this.layout = this.slideshowContainer.dataset.layout; } if (this.slideshowContainer.tagName !== 'SECTION') { this.slideshowContainer.setAttribute('role', 'region'); } this.slideshowContainer.setAttribute('aria-roledescription', 'carousel'); - if (this.layout === 'cover-flow') { + if (this.layout === 'cover-flow' || this.layout === 'multi-view') { this.slideshowContainer.classList.add(...this.slideContainerCoverFlowClassList); } else { this.slideshowContainer.classList.add(...this.slideContainerClassList); @@ -276,7 +278,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if (this.slideDeck.getAttribute('id') === '' || this.slideDeck.getAttribute('id') === null) { this.slideDeck.setAttribute('id', this.uuid.concat('-slide-deck')); } - if (this.layout === 'cover-flow') { + if (this.layout === 'cover-flow' || this.layout === 'multi-view') { this.slideDeck.classList.add(...this.slideDeckCoverFlowClassList); } else { this.slideDeck.classList.add(...this.slideDeckClassList); @@ -329,7 +331,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.#initSlides(); this.#initControls(); - if (this.layout === 'cover-flow') { + if (this.layout === 'cover-flow' || this.layout === 'multi-view') { // If we resize then the active slide might not be in the middle // so we will need to re-scroll to it window.addEventListener('resize', () => { @@ -465,7 +467,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> // Set up slide slide.setAttribute('id', this.uuid.concat('-slide-', slideIndex)); slide.classList.add('dcf-slide'); - if (this.layout === 'cover-flow') { + if (this.layout === 'cover-flow' || this.layout === 'multi-view') { slide.classList.add(...this.slideCoverFlowClassList); } else { slide.classList.add(...this.slideClassList); @@ -480,11 +482,11 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } // Cover-flow layout will let us see all the slides so if we click on one then jump to it - if (slideIndex !== this.currentSlide && this.layout === 'cover-flow') { + if (slideIndex !== this.currentSlide && (this.layout === 'cover-flow' || this.layout === 'multi-view')) { slide.addEventListener('click', () => { this.#coverFlowSwapSlides(slideIndex); }); - } else if (this.layout === 'cover-flow') { + } else if (this.layout === 'cover-flow' || this.layout === 'multi-view') { slide.addEventListener('click', () => { this.#coverFlowSwapSlides(slideIndex); }); @@ -573,6 +575,15 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.prevButton.classList.add(...this.slideBtnCoverFlowClassList); this.nextButton.classList.add(...this.slideBtnCoverFlowClassList); + } else if (this.layout === 'multi-view') { + + // Set the classes for the button container + this.controlsContainer.classList.add(...['dcf-d-flex', 'dcf-jc-start', 'dcf-ai-center', 'dcf-mt-4']); + + // Set the classes for the prev and next buttons + this.prevButton.classList.add(...this.slideBtnCoverFlowClassList); + this.nextButton.classList.add(...this.slideBtnCoverFlowClassList); + // If we are in a normal layout } else { @@ -610,7 +621,12 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.controlsContainer.appendChild(this.playToggleButton); } this.controlsContainer.appendChild(this.nextButton); - this.slideshowContainer.prepend(this.controlsContainer); + + if (this.layout === 'multi-view') { + this.slideshowContainer.append(this.controlsContainer); + } else { + this.slideshowContainer.prepend(this.controlsContainer); + } } /** @@ -795,6 +811,22 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> const wrapperMidPoint = this.slideshowContainer.offsetWidth / 2; const getMidItemToLeftEdge = (-1 * ( slideOffsetToList + slideMidPoint)); + + // If we are in multi-view then images snap to edge + if (this.layout === 'multi-view') { + + // Determine the width (in px) the css var --gap is + const dummy = document.createElement('div'); + dummy.style.position = 'absolute'; + dummy.style.visibility = 'hidden'; + dummy.style.width = 'var(--gap)'; + this.slideshowContainer.appendChild(dummy); + const gapPx = dummy.getBoundingClientRect().width; + this.slideshowContainer.removeChild(dummy); + + return getMidItemToLeftEdge + slideMidPoint + gapPx; + } + return getMidItemToLeftEdge + wrapperMidPoint; } @@ -812,7 +844,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> newCurrentSlideIndex = this.slides.length - 1; } - if (this.layout === 'cover-flow') { + if (this.layout === 'cover-flow' || this.layout === 'multi-view') { this.#coverFlowSwapSlides(newCurrentSlideIndex); } else if (this.transition === 'fade') { this.#fadeSlides(newCurrentSlideIndex); @@ -835,7 +867,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> newCurrentSlideIndex = 0; } - if (this.layout === 'cover-flow') { + if (this.layout === 'cover-flow' || this.layout === 'multi-view') { this.#coverFlowSwapSlides(newCurrentSlideIndex); } else if (this.transition === 'fade') { this.#fadeSlides(newCurrentSlideIndex); @@ -916,7 +948,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> throw new Error(`Slide ${index} does not exist`); } - if (this.layout === 'cover-flow') { + if (this.layout === 'cover-flow' || this.layout === 'multi-view') { this.#coverFlowSwapSlides(index); } else { this.#swapSlides(index); diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index ddf1ecc5..67cc3166 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -10,12 +10,12 @@ padding-left: 0; } -.dcf-slideshow.dcf-slideshow-initialized:not([data-layout="cover-flow"]) .dcf-slide-deck li { +.dcf-slideshow.dcf-slideshow-initialized:not([data-layout="cover-flow"]):not([data-layout="multi-view"]) .dcf-slide-deck li { grid-column: 1; grid-row: 1; } -.dcf-slideshow.dcf-slideshow-initialized:not([data-layout="cover-flow"]) .dcf-slide-deck { +.dcf-slideshow.dcf-slideshow-initialized:not([data-layout="cover-flow"]):not([data-layout="multi-view"]) .dcf-slide-deck { clip-path: polygon( 0 0, calc(100% - (#{var.$size-btn-x} * 2) - (#{var.$border-width-button} * 5) - #{var.$size-clip-path}) 0, @@ -26,8 +26,8 @@ ); } -.dcf-slideshow.dcf-slideshow-initialized[data-play="true"]:not([data-layout="cover-flow"]) .dcf-slide-deck, -.dcf-slideshow.dcf-slideshow-initialized[data-play="auto"]:not([data-layout="cover-flow"]) .dcf-slide-deck { +.dcf-slideshow.dcf-slideshow-initialized[data-play="true"]:not([data-layout="cover-flow"]):not([data-layout="multi-view"]) .dcf-slide-deck, +.dcf-slideshow.dcf-slideshow-initialized[data-play="auto"]:not([data-layout="cover-flow"]):not([data-layout="multi-view"]) .dcf-slide-deck { clip-path: polygon( 0 0, calc(100% - (#{var.$size-btn-x} * 3) - (#{var.$border-width-button} * 8) - #{var.$size-clip-path}) 0, @@ -103,3 +103,24 @@ width: calc(var(--item-width) - (2 * var(--button-padding))); transform: translateX(-50%); } + +.dcf-slideshow[data-layout="multi-view"] { + container-type: inline-size; + --gap: 1rem; + --item-width: clamp(15rem, 25cqw, 20rem); + --transition-time: 400ms; //!IMPORTANT this is referenced in the JS + --transition-easing: cubic-bezier(.25, .46, .45, .94); +} + +.dcf-slideshow[data-layout="multi-view"] .dcf-slide-deck { + gap: var(--gap); +} + +.dcf-slideshow[data-layout="multi-view"] .dcf-slide-deck li { + width: var(--item-width); +} + +.dcf-slideshow[data-layout="multi-view"] .dcf-slideshow-controls { + gap: var(--gap); + margin-left: var(--gap); +} From 936449f28aaa484c3ee8df80523b91acbb1f1d51 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Thu, 25 Jun 2026 11:06:23 -0500 Subject: [PATCH 17/21] Combine layouts and move things out of dcf --- js/components/dcf-slideshow.js | 141 ++++++++++++++-------------- scss/components-js/_slideshows.scss | 70 +++++--------- 2 files changed, 93 insertions(+), 118 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index ceb504c8..2d53ca9b 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -37,7 +37,9 @@ export default class DCFSlideshow { layout = 'default'; - coverFlowButtonPosition = 'middle'; + multiViewButtonPosition = 'center'; + + multiViewSlidePosition = 'center'; scrollingId = null; @@ -47,7 +49,7 @@ export default class DCFSlideshow { 'dcf-relative', ]; - slideContainerCoverFlowClassList = [ + slideContainerMultiViewClassList = [ 'dcf-relative', 'dcf-overflow-x-hidden', ]; @@ -62,7 +64,7 @@ export default class DCFSlideshow { 'dcf-h-100%', ]; - slideDeckCoverFlowClassList = [ + slideDeckMultiViewClassList = [ 'dcf-d-flex', 'dcf-flex-row', 'dcf-flex-nowrap', @@ -79,7 +81,7 @@ export default class DCFSlideshow { 'dcf-top-0', ]; - slideButtonContainerCoverFlowClassList = [ + slideButtonContainerMultiViewClassList = [ 'dcf-absolute', 'dcf-d-flex', 'dcf-jc-between', @@ -91,7 +93,7 @@ export default class DCFSlideshow { 'dcf-mb-0', ]; - slideCoverFlowClassList = [ + slideMultiViewClassList = [ 'dcf-relative', 'dcf-overflow-hidden', 'dcf-mt-0', @@ -109,7 +111,7 @@ export default class DCFSlideshow { 'dcf-white', ]; - slideBtnCoverFlowClassList = [ + slideBtnMultiViewClassList = [ 'dcf-circle', ]; @@ -188,32 +190,32 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if ('slideContainerClassList' in options && Array.isArray(options.slideContainerClassList)) { this.slideContainerClassList = options.slideContainerClassList; } - if ('slideContainerCoverFlowClassList' in options && Array.isArray(options.slideContainerCoverFlowClassList)) { - this.slideContainerCoverFlowClassList = options.slideContainerCoverFlowClassList; + if ('slideContainerMultiViewClassList' in options && Array.isArray(options.slideContainerMultiViewClassList)) { + this.slideContainerMultiViewClassList = options.slideContainerMultiViewClassList; } if ('slideDeckClassList' in options && Array.isArray(options.slideDeckClassList)) { this.slideDeckClassList = options.slideDeckClassList; } - if ('slideDeckCoverFlowClassList' in options && Array.isArray(options.slideDeckCoverFlowClassList)) { - this.slideDeckCoverFlowClassList = options.slideDeckCoverFlowClassList; + if ('slideDeckMultiViewClassList' in options && Array.isArray(options.slideDeckMultiViewClassList)) { + this.slideDeckMultiViewClassList = options.slideDeckMultiViewClassList; } if ('slideButtonContainerClassList' in options && Array.isArray(options.slideButtonContainerClassList)) { this.slideButtonContainerClassList = options.slideButtonContainerClassList; } - if ('slideButtonContainerCoverFlowClassList' in options && Array.isArray(options.slideButtonContainerCoverFlowClassList)) { - this.slideButtonContainerCoverFlowClassList = options.slideButtonContainerCoverFlowClassList; + if ('slideButtonContainerMultiViewClassList' in options && Array.isArray(options.slideButtonContainerMultiViewClassList)) { + this.slideButtonContainerMultiViewClassList = options.slideButtonContainerMultiViewClassList; } if ('slideClassList' in options && Array.isArray(options.slideClassList)) { this.slideClassList = options.slideClassList; } - if ('slideCoverFlowClassList' in options && Array.isArray(options.slideCoverFlowClassList)) { - this.slideCoverFlowClassList = options.slideCoverFlowClassList; + if ('slideMultiViewClassList' in options && Array.isArray(options.slideMultiViewClassList)) { + this.slideMultiViewClassList = options.slideMultiViewClassList; } if ('slideBtnClassList' in options && Array.isArray(options.slideBtnClassList)) { this.slideBtnClassList = options.slideBtnClassList; } - if ('slideBtnCoverFlowClassList' in options && Array.isArray(options.slideBtnCoverFlowClassList)) { - this.slideBtnCoverFlowClassList = options.slideBtnCoverFlowClassList; + if ('slideBtnMultiViewClassList' in options && Array.isArray(options.slideBtnMultiViewClassList)) { + this.slideBtnMultiViewClassList = options.slideBtnMultiViewClassList; } if ('slidePrevBtnClassList' in options && Array.isArray(options.slidePrevBtnClassList)) { this.slidePrevBtnClassList = options.slidePrevBtnClassList; @@ -242,9 +244,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> // Set up slide show container this.slideshowContainer = slideshowContainer; - if (this.slideshowContainer.dataset.layout === 'cover-flow') { - this.layout = this.slideshowContainer.dataset.layout; - } else if (this.slideshowContainer.dataset.layout === 'multi-view') { + if (this.slideshowContainer.dataset.layout === 'multi-view') { this.layout = this.slideshowContainer.dataset.layout; } @@ -252,17 +252,25 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.slideshowContainer.setAttribute('role', 'region'); } this.slideshowContainer.setAttribute('aria-roledescription', 'carousel'); - if (this.layout === 'cover-flow' || this.layout === 'multi-view') { - this.slideshowContainer.classList.add(...this.slideContainerCoverFlowClassList); + if (this.layout === 'multi-view') { + this.slideshowContainer.classList.add(...this.slideContainerMultiViewClassList); } else { this.slideshowContainer.classList.add(...this.slideContainerClassList); } if ( this.slideshowContainer.dataset.buttonPosition === 'top' || - this.slideshowContainer.dataset.buttonPosition === 'bottom' + this.slideshowContainer.dataset.buttonPosition === 'bottom' || + this.slideshowContainer.dataset.buttonPosition === 'under' + ) { + this.multiViewButtonPosition = this.slideshowContainer.dataset.buttonPosition; + } + + if ( + this.slideshowContainer.dataset.snap === 'center' || + this.slideshowContainer.dataset.snap === 'left' ) { - this.coverFlowButtonPosition = this.slideshowContainer.dataset.buttonPosition; + this.multiViewSlidePosition = this.slideshowContainer.dataset.snap; } // If the tabGroup has no ID then it will set it @@ -278,8 +286,8 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if (this.slideDeck.getAttribute('id') === '' || this.slideDeck.getAttribute('id') === null) { this.slideDeck.setAttribute('id', this.uuid.concat('-slide-deck')); } - if (this.layout === 'cover-flow' || this.layout === 'multi-view') { - this.slideDeck.classList.add(...this.slideDeckCoverFlowClassList); + if (this.layout === 'multi-view') { + this.slideDeck.classList.add(...this.slideDeckMultiViewClassList); } else { this.slideDeck.classList.add(...this.slideDeckClassList); } @@ -331,11 +339,11 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.#initSlides(); this.#initControls(); - if (this.layout === 'cover-flow' || this.layout === 'multi-view') { + if (this.layout === 'multi-view') { // If we resize then the active slide might not be in the middle // so we will need to re-scroll to it window.addEventListener('resize', () => { - this.#coverFlowSwapSlides(this.currentSlide, true); + this.#multiViewSwapSlides(this.currentSlide, true); }); // Add swipe gestures for mobile @@ -467,8 +475,8 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> // Set up slide slide.setAttribute('id', this.uuid.concat('-slide-', slideIndex)); slide.classList.add('dcf-slide'); - if (this.layout === 'cover-flow' || this.layout === 'multi-view') { - slide.classList.add(...this.slideCoverFlowClassList); + if (this.layout === 'multi-view') { + slide.classList.add(...this.slideMultiViewClassList); } else { slide.classList.add(...this.slideClassList); } @@ -481,18 +489,18 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> slide.setAttribute('aria-label', `${slideIndex + 1} of ${this.slides.length}`); } - // Cover-flow layout will let us see all the slides so if we click on one then jump to it - if (slideIndex !== this.currentSlide && (this.layout === 'cover-flow' || this.layout === 'multi-view')) { + // Multi view layout will let us see all the slides so if we click on one then jump to it + if (slideIndex !== this.currentSlide && this.layout === 'multi-view') { slide.addEventListener('click', () => { - this.#coverFlowSwapSlides(slideIndex); + this.#multiViewSwapSlides(slideIndex); }); - } else if (this.layout === 'cover-flow' || this.layout === 'multi-view') { + } else if (this.layout === 'multi-view') { slide.addEventListener('click', () => { - this.#coverFlowSwapSlides(slideIndex); + this.#multiViewSwapSlides(slideIndex); }); // This is the selected slide so go to it - this.#coverFlowSwapSlides(slideIndex); + this.#multiViewSwapSlides(slideIndex); } else if (slideIndex !== this.currentSlide) { // If we are not the current slide then hide it @@ -558,33 +566,29 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.nextSlide(); }); - // Set cover flow specific classes - if (this.layout === 'cover-flow') { + // Set multi view specific classes + if (this.layout === 'multi-view') { // Set the classes for the button container - this.controlsContainer.classList.add(...this.slideButtonContainerCoverFlowClassList); - if (this.coverFlowButtonPosition === 'top') { + if (this.multiViewButtonPosition === 'top') { + this.controlsContainer.classList.add(...this.slideButtonContainerMultiViewClassList); this.controlsContainer.classList.add('dcf-slideshow-controls-top'); - } else if (this.coverFlowButtonPosition === 'bottom') { + } else if (this.multiViewButtonPosition === 'under') { + this.controlsContainer.classList.add(...['dcf-d-flex', 'dcf-jc-start', 'dcf-ai-center', 'dcf-mt-4']); + this.controlsContainer.classList.add('dcf-slideshow-controls-under'); + } else if (this.multiViewButtonPosition === 'bottom') { + this.controlsContainer.classList.add(...this.slideButtonContainerMultiViewClassList); this.controlsContainer.classList.add('dcf-slideshow-controls-bottom'); } else { + this.controlsContainer.classList.add(...this.slideButtonContainerMultiViewClassList); this.controlsContainer.classList.add('dcf-top-50%'); + this.controlsContainer.classList.add('dcf-slideshow-controls-center'); } // Set the classes for the prev and next buttons - this.prevButton.classList.add(...this.slideBtnCoverFlowClassList); - this.nextButton.classList.add(...this.slideBtnCoverFlowClassList); - - } else if (this.layout === 'multi-view') { - - // Set the classes for the button container - this.controlsContainer.classList.add(...['dcf-d-flex', 'dcf-jc-start', 'dcf-ai-center', 'dcf-mt-4']); - - // Set the classes for the prev and next buttons - this.prevButton.classList.add(...this.slideBtnCoverFlowClassList); - this.nextButton.classList.add(...this.slideBtnCoverFlowClassList); + this.prevButton.classList.add(...this.slideBtnMultiViewClassList); + this.nextButton.classList.add(...this.slideBtnMultiViewClassList); - // If we are in a normal layout } else { // Set the classes for the button container @@ -693,26 +697,23 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } /** - * In cover flow layout adjust the left position of the slide deck to "scroll" to image + * In multi view layout adjust the left position of the slide deck to "scroll" to image * @param { Number } newCurrentSlideIndex The new slide which to scroll to * @param { Boolean } jump Do the animation to scroll, or jump right to it and avoid animation */ - #coverFlowSwapSlides(newCurrentSlideIndex, jump = false) { + #multiViewSwapSlides(newCurrentSlideIndex, jump = false) { this.currentSlide = newCurrentSlideIndex; - // Add the active class to start that transition - this.slides[newCurrentSlideIndex].classList.add('active'); - // Remove the active class from all the other slides this.slides.forEach((slideToRemoveClass) => { - if ( - !slideToRemoveClass.isSameNode(this.slides[newCurrentSlideIndex]) - && slideToRemoveClass.classList.contains('active') - ) { - slideToRemoveClass.classList.remove('active'); - } + slideToRemoveClass.classList.remove('dcf-slideshow-active'); + slideToRemoveClass.classList.add('dcf-slideshow-inactive'); }); + // Add the active class to start that transition + this.slides[newCurrentSlideIndex].classList.remove('dcf-slideshow-active'); + this.slides[newCurrentSlideIndex].classList.add('dcf-slideshow-active'); + // "Scroll" to the the new item if (jump) { this.#jumpScrollToItem(this.slides[newCurrentSlideIndex]); @@ -813,7 +814,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> const getMidItemToLeftEdge = (-1 * ( slideOffsetToList + slideMidPoint)); // If we are in multi-view then images snap to edge - if (this.layout === 'multi-view') { + if (this.multiViewSlidePosition === 'left') { // Determine the width (in px) the css var --gap is const dummy = document.createElement('div'); @@ -844,8 +845,8 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> newCurrentSlideIndex = this.slides.length - 1; } - if (this.layout === 'cover-flow' || this.layout === 'multi-view') { - this.#coverFlowSwapSlides(newCurrentSlideIndex); + if (this.layout === 'multi-view') { + this.#multiViewSwapSlides(newCurrentSlideIndex); } else if (this.transition === 'fade') { this.#fadeSlides(newCurrentSlideIndex); } else { @@ -867,8 +868,8 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> newCurrentSlideIndex = 0; } - if (this.layout === 'cover-flow' || this.layout === 'multi-view') { - this.#coverFlowSwapSlides(newCurrentSlideIndex); + if (this.layout === 'multi-view') { + this.#multiViewSwapSlides(newCurrentSlideIndex); } else if (this.transition === 'fade') { this.#fadeSlides(newCurrentSlideIndex); } else { @@ -948,8 +949,8 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> throw new Error(`Slide ${index} does not exist`); } - if (this.layout === 'cover-flow' || this.layout === 'multi-view') { - this.#coverFlowSwapSlides(index); + if (this.layout === 'multi-view') { + this.#multiViewSwapSlides(index); } else { this.#swapSlides(index); } diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index 67cc3166..0aa626ea 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -46,81 +46,55 @@ opacity: 1; // Starting opacity } -.dcf-slideshow[data-layout="cover-flow"] { +.dcf-slideshow[data-layout="multi-view"] { container-type: inline-size; - --item-width: clamp(15rem, 25cqw, 20rem); + --gap: 1rem; --button-padding: 1rem; - --clip-path-inline: 10%; - --clip-path-block: 10%; + --item-width: clamp(15rem, 25cqw, 20rem); --transition-time: 400ms; //!IMPORTANT this is referenced in the JS --transition-easing: cubic-bezier(.25, .46, .45, .94); } -.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck { - padding-inline: calc(50cqw - (var(--item-width) / 2) + 2cqw); // We add +2 to fix some browser rounding issues +.dcf-slideshow[data-layout="multi-view"] .dcf-slide-deck { + gap: var(--gap); } -// /* List item setup */ -.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li { +.dcf-slideshow[data-layout="multi-view"] .dcf-slide-deck li { width: var(--item-width); - clip-path: inset(var(--clip-path-block) var(--clip-path-inline) var(--clip-path-block) var(--clip-path-inline) round var.$roundrect); - transition: - clip-path var(--transition-time) var(--transition-easing), - margin-inline var(--transition-time) var(--transition-easing); -} - -.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li.active { - clip-path: inset(0% 0% 0% 0% round var.$roundrect); - margin-inline: var(--clip-path-inline); -} - -.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li .dcf-slideshow-nonactive-hide { - opacity: 0; - transition: opacity var(--transition-time) var(--transition-easing); -} - -.dcf-slideshow[data-layout="cover-flow"] .dcf-slide-deck li.active .dcf-slideshow-nonactive-hide { - opacity: 1; } -.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls:not(.dcf-slideshow-controls-top):not(.dcf-slideshow-controls-bottom) { +// Center +.dcf-slideshow[data-layout="multi-view"]:not([data-button-position]) .dcf-slideshow-controls, +.dcf-slideshow[data-layout="multi-view"][data-button-position="center"] .dcf-slideshow-controls { width: calc(var(--item-width) + var(--button-padding)); transform: translate(-50%, -50%); } -.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls:not(.dcf-slideshow-controls-top):not(.dcf-slideshow-controls-bottom) .dcf-btn-slide-prev { + +.dcf-slideshow[data-layout="multi-view"]:not([data-button-position]) .dcf-slideshow-controls .dcf-btn-slide-prev, +.dcf-slideshow[data-layout="multi-view"][data-button-position="center"] .dcf-slideshow-controls .dcf-btn-slide-prev { transform: translateX(-100%); } -.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls:not(.dcf-slideshow-controls-top):not(.dcf-slideshow-controls-bottom) .dcf-btn-slide-next { +.dcf-slideshow[data-layout="multi-view"]:not([data-button-position]) .dcf-slideshow-controls .dcf-btn-slide-next, +.dcf-slideshow[data-layout="multi-view"][data-button-position="center"] .dcf-slideshow-controls .dcf-btn-slide-next { transform: translateX(100%); } -.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls.dcf-slideshow-controls-top { + +// Top +.dcf-slideshow[data-layout="multi-view"][data-button-position="top"] .dcf-slideshow-controls { top: var(--button-padding); width: calc(var(--item-width) - (2 * var(--button-padding))); transform: translateX(-50%); } -.dcf-slideshow[data-layout="cover-flow"] .dcf-slideshow-controls.dcf-slideshow-controls-bottom { + +// Bottom +.dcf-slideshow[data-layout="multi-view"][data-button-position="bottom"] .dcf-slideshow-controls { bottom: var(--button-padding); width: calc(var(--item-width) - (2 * var(--button-padding))); transform: translateX(-50%); } -.dcf-slideshow[data-layout="multi-view"] { - container-type: inline-size; - --gap: 1rem; - --item-width: clamp(15rem, 25cqw, 20rem); - --transition-time: 400ms; //!IMPORTANT this is referenced in the JS - --transition-easing: cubic-bezier(.25, .46, .45, .94); -} - -.dcf-slideshow[data-layout="multi-view"] .dcf-slide-deck { - gap: var(--gap); -} - -.dcf-slideshow[data-layout="multi-view"] .dcf-slide-deck li { - width: var(--item-width); -} - -.dcf-slideshow[data-layout="multi-view"] .dcf-slideshow-controls { +// Under +.dcf-slideshow[data-layout="multi-view"][data-button-position="under"] .dcf-slideshow-controls { gap: var(--gap); margin-left: var(--gap); } From ccfadb072452f16f8002b5eced245008e1b658da Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Thu, 25 Jun 2026 11:08:34 -0500 Subject: [PATCH 18/21] Added class list array for under button position --- js/components/dcf-slideshow.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 2d53ca9b..f309db18 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -89,6 +89,13 @@ export default class DCFSlideshow { 'dcf-z-1', ]; + slideButtonContainerMultiViewUnderClassList = [ + 'dcf-d-flex', + 'dcf-jc-start', + 'dcf-ai-center', + 'dcf-mt-4', + ]; + slideClassList = [ 'dcf-mb-0', ]; @@ -205,6 +212,9 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> if ('slideButtonContainerMultiViewClassList' in options && Array.isArray(options.slideButtonContainerMultiViewClassList)) { this.slideButtonContainerMultiViewClassList = options.slideButtonContainerMultiViewClassList; } + if ('slideButtonContainerMultiViewUnderClassList' in options && Array.isArray(options.slideButtonContainerMultiViewUnderClassList)) { + this.slideButtonContainerMultiViewUnderClassList = options.slideButtonContainerMultiViewUnderClassList; + } if ('slideClassList' in options && Array.isArray(options.slideClassList)) { this.slideClassList = options.slideClassList; } @@ -574,7 +584,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.controlsContainer.classList.add(...this.slideButtonContainerMultiViewClassList); this.controlsContainer.classList.add('dcf-slideshow-controls-top'); } else if (this.multiViewButtonPosition === 'under') { - this.controlsContainer.classList.add(...['dcf-d-flex', 'dcf-jc-start', 'dcf-ai-center', 'dcf-mt-4']); + this.controlsContainer.classList.add(...this.slideButtonContainerMultiViewUnderClassList); this.controlsContainer.classList.add('dcf-slideshow-controls-under'); } else if (this.multiViewButtonPosition === 'bottom') { this.controlsContainer.classList.add(...this.slideButtonContainerMultiViewClassList); From 3cf256fb25d267d96b86f0eea37ee2e65557363c Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Thu, 25 Jun 2026 11:12:58 -0500 Subject: [PATCH 19/21] Added right as snap position --- js/components/dcf-slideshow.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index f309db18..3b1ec0e1 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -277,8 +277,8 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> } if ( - this.slideshowContainer.dataset.snap === 'center' || - this.slideshowContainer.dataset.snap === 'left' + this.slideshowContainer.dataset.snap === 'left' || + this.slideshowContainer.dataset.snap === 'right' ) { this.multiViewSlidePosition = this.slideshowContainer.dataset.snap; } @@ -836,6 +836,18 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> this.slideshowContainer.removeChild(dummy); return getMidItemToLeftEdge + slideMidPoint + gapPx; + } else if (this.multiViewSlidePosition === 'right') { + + // Determine the width (in px) the css var --gap is + const dummy = document.createElement('div'); + dummy.style.position = 'absolute'; + dummy.style.visibility = 'hidden'; + dummy.style.width = 'var(--gap)'; + this.slideshowContainer.appendChild(dummy); + const gapPx = dummy.getBoundingClientRect().width; + this.slideshowContainer.removeChild(dummy); + + return getMidItemToLeftEdge + (wrapperMidPoint * 2) - slideMidPoint - gapPx; } return getMidItemToLeftEdge + wrapperMidPoint; From b5be35c6151ab073826e24730bf0980d3f61c45d Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Thu, 25 Jun 2026 11:20:49 -0500 Subject: [PATCH 20/21] Added default value for gap in case it is not set --- js/components/dcf-slideshow.js | 4 ++-- scss/components-js/_slideshows.scss | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index 3b1ec0e1..a9a8e4bb 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -830,7 +830,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> const dummy = document.createElement('div'); dummy.style.position = 'absolute'; dummy.style.visibility = 'hidden'; - dummy.style.width = 'var(--gap)'; + dummy.style.width = 'var(--gap, 1rem)'; this.slideshowContainer.appendChild(dummy); const gapPx = dummy.getBoundingClientRect().width; this.slideshowContainer.removeChild(dummy); @@ -842,7 +842,7 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> const dummy = document.createElement('div'); dummy.style.position = 'absolute'; dummy.style.visibility = 'hidden'; - dummy.style.width = 'var(--gap)'; + dummy.style.width = 'var(--gap, 1rem)'; this.slideshowContainer.appendChild(dummy); const gapPx = dummy.getBoundingClientRect().width; this.slideshowContainer.removeChild(dummy); diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index 0aa626ea..cd7a4a32 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -48,7 +48,7 @@ .dcf-slideshow[data-layout="multi-view"] { container-type: inline-size; - --gap: 1rem; + --gap: 1rem; //!IMPORTANT this is referenced in the JS --button-padding: 1rem; --item-width: clamp(15rem, 25cqw, 20rem); --transition-time: 400ms; //!IMPORTANT this is referenced in the JS From 61a2e5fa208cbefead11fa9eb02cd818eab9f6d7 Mon Sep 17 00:00:00 2001 From: Thomas Neumann Date: Wed, 1 Jul 2026 09:53:24 -0500 Subject: [PATCH 21/21] Styling fixes --- js/components/dcf-slideshow.js | 10 +++++----- scss/components-js/_slideshows.scss | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/js/components/dcf-slideshow.js b/js/components/dcf-slideshow.js index a9a8e4bb..4de997cf 100644 --- a/js/components/dcf-slideshow.js +++ b/js/components/dcf-slideshow.js @@ -716,13 +716,13 @@ width="24" height="24" viewBox="0 0 24 24" focusable="false" aria-hidden="true"> // Remove the active class from all the other slides this.slides.forEach((slideToRemoveClass) => { - slideToRemoveClass.classList.remove('dcf-slideshow-active'); - slideToRemoveClass.classList.add('dcf-slideshow-inactive'); + slideToRemoveClass.classList.remove('dcf-slide-active'); + slideToRemoveClass.classList.add('dcf-slide-inactive'); }); - // Add the active class to start that transition - this.slides[newCurrentSlideIndex].classList.remove('dcf-slideshow-active'); - this.slides[newCurrentSlideIndex].classList.add('dcf-slideshow-active'); + // Add the active class to start that transitions + this.slides[newCurrentSlideIndex].classList.remove('dcf-slide-inactive'); + this.slides[newCurrentSlideIndex].classList.add('dcf-slide-active'); // "Scroll" to the the new item if (jump) { diff --git a/scss/components-js/_slideshows.scss b/scss/components-js/_slideshows.scss index cd7a4a32..208bd64c 100644 --- a/scss/components-js/_slideshows.scss +++ b/scss/components-js/_slideshows.scss @@ -72,11 +72,11 @@ .dcf-slideshow[data-layout="multi-view"]:not([data-button-position]) .dcf-slideshow-controls .dcf-btn-slide-prev, .dcf-slideshow[data-layout="multi-view"][data-button-position="center"] .dcf-slideshow-controls .dcf-btn-slide-prev { - transform: translateX(-100%); + transform: translateX(calc(-100% + calc(var(--clip-path-inline) * 2))); } .dcf-slideshow[data-layout="multi-view"]:not([data-button-position]) .dcf-slideshow-controls .dcf-btn-slide-next, .dcf-slideshow[data-layout="multi-view"][data-button-position="center"] .dcf-slideshow-controls .dcf-btn-slide-next { - transform: translateX(100%); + transform: translateX(calc(100% - calc(var(--clip-path-inline) * 2))); } // Top