Skip to content

Commit 0e531f9

Browse files
authored
landing page carousel tweaks (#57580)
1 parent 6e83d52 commit 0e531f9

File tree

3 files changed

+58
-4
lines changed

3 files changed

+58
-4
lines changed

src/landings/components/discovery/DiscoveryLanding.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ export const DiscoveryLanding = () => {
2020
<DefaultLayout>
2121
<div>
2222
<LandingHero title={title} intro={intro} heroImage={heroImage} introLinks={introLinks} />
23-
<LandingCarousel flatArticles={flatArticles} recommended={recommended} />
24-
<ArticleGrid flatArticles={flatArticles} />
23+
<div className="container-xl px-3 px-md-6 mt-6 mb-4">
24+
<LandingCarousel flatArticles={flatArticles} recommended={recommended} />
25+
<ArticleGrid flatArticles={flatArticles} />
26+
</div>
2527
</div>
2628
</DefaultLayout>
2729
)

src/landings/components/shared/LandingCarousel.module.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.carousel {
22
margin-top: 3rem;
3+
--carousel-transition-duration: 0.3s;
34
}
45

56
.header {
@@ -47,6 +48,8 @@
4748
display: grid;
4849
gap: 1.5rem;
4950
grid-template-columns: 1fr;
51+
transition: opacity var(--carousel-transition-duration) ease-out;
52+
opacity: 1;
5053

5154
@media (min-width: 768px) {
5255
grid-template-columns: repeat(2, 1fr);
@@ -55,6 +58,10 @@
5558
@media (min-width: 1012px) {
5659
grid-template-columns: repeat(3, 1fr);
5760
}
61+
62+
&.animating {
63+
opacity: 0;
64+
}
5865
}
5966

6067
.articleCard {

src/landings/components/shared/LandingCarousel.tsx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react'
1+
import { useState, useEffect, useRef } from 'react'
22
import { ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react'
33
import { Token } from '@primer/react'
44
import cx from 'classnames'
@@ -53,15 +53,27 @@ export const LandingCarousel = ({
5353
recommended,
5454
}: LandingCarouselProps) => {
5555
const [currentPage, setCurrentPage] = useState(0)
56+
const [isAnimating, setIsAnimating] = useState(false)
5657
const itemsPerView = useResponsiveItemsPerView()
5758
const { t } = useTranslation('discovery_landing')
5859
const headingText = heading || t('recommended')
60+
// Ref to store timeout IDs for cleanup
61+
const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
5962

6063
// Reset to first page when itemsPerView changes (screen size changes)
6164
useEffect(() => {
6265
setCurrentPage(0)
6366
}, [itemsPerView])
6467

68+
// Cleanup timeout on unmount
69+
useEffect(() => {
70+
return () => {
71+
if (animationTimeoutRef.current) {
72+
clearTimeout(animationTimeoutRef.current)
73+
}
74+
}
75+
}, [])
76+
6577
// Helper function to find article data from tocItems
6678
const findArticleData = (articlePath: string) => {
6779
if (typeof articlePath !== 'string') {
@@ -94,11 +106,41 @@ export const LandingCarousel = ({
94106
const totalPages = Math.ceil(totalItems / itemsPerView)
95107

96108
const goToPrevious = () => {
109+
if (currentPage === 0 || isAnimating) return
110+
111+
// Clear any existing timeout
112+
if (animationTimeoutRef.current) {
113+
clearTimeout(animationTimeoutRef.current)
114+
}
115+
116+
setIsAnimating(true)
97117
setCurrentPage((prev) => Math.max(0, prev - 1))
118+
119+
// Set animation state to false after transition completes
120+
// Duration matches CSS custom property --carousel-transition-duration (300ms)
121+
animationTimeoutRef.current = setTimeout(() => {
122+
setIsAnimating(false)
123+
animationTimeoutRef.current = null
124+
}, 300)
98125
}
99126

100127
const goToNext = () => {
128+
if (currentPage >= totalPages - 1 || isAnimating) return
129+
130+
// Clear any existing timeout
131+
if (animationTimeoutRef.current) {
132+
clearTimeout(animationTimeoutRef.current)
133+
}
134+
135+
setIsAnimating(true)
101136
setCurrentPage((prev) => Math.min(totalPages - 1, prev + 1))
137+
138+
// Set animation state to false after transition completes
139+
// Duration matches CSS custom property --carousel-transition-duration (300ms)
140+
animationTimeoutRef.current = setTimeout(() => {
141+
setIsAnimating(false)
142+
animationTimeoutRef.current = null
143+
}, 300)
102144
}
103145

104146
// Calculate the start index based on current page
@@ -136,7 +178,10 @@ export const LandingCarousel = ({
136178
)}
137179
</div>
138180

139-
<div className={styles.itemsGrid} data-testid="carousel-items">
181+
<div
182+
className={cx(styles.itemsGrid, { [styles.animating]: isAnimating })}
183+
data-testid="carousel-items"
184+
>
140185
{visibleItems.map((article: ProcessedArticleItem, index) => (
141186
<div
142187
key={startIndex + index}

0 commit comments

Comments
 (0)