Skip to content

Commit 3e08dd5

Browse files
Add autoplay to viz carousel
1 parent ef48c3d commit 3e08dd5

File tree

4 files changed

+181
-34
lines changed

4 files changed

+181
-34
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@undp/design-system-react",
3-
"version": "1.4.3",
3+
"version": "1.5.0",
44
"main": "./dist/index.cjs",
55
"module": "./dist/index.js",
66
"browser": "./dist/index.umd.js",

src/components/ui/vizCarousel.tsx

Lines changed: 176 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import React, { ReactNode, useState } from 'react';
1+
import {
2+
CSSProperties,
3+
forwardRef,
4+
HTMLAttributes,
5+
ReactNode,
6+
useEffect,
7+
useRef,
8+
useState,
9+
} from 'react';
210
import { cva, VariantProps } from 'class-variance-authority';
3-
import { ChevronLeft, ChevronRight } from 'lucide-react';
11+
import { ChevronLeft, ChevronRight, PauseIcon, PlayIcon } from 'lucide-react';
412

513
import { P } from './typography';
614

@@ -34,40 +42,58 @@ const vizContainerVariants = cva('flex box-border grow shrink-0', {
3442
defaultVariants: { vizWidth: 'base' },
3543
});
3644

37-
interface CardProps
38-
extends React.HTMLAttributes<HTMLDivElement>,
39-
VariantProps<typeof cardVariants> {
40-
vizStyle?: React.CSSProperties;
45+
interface CardProps extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> {
4146
slides: {
4247
content: ReactNode;
4348
viz: ReactNode;
4449
}[];
45-
contentStyle?: React.CSSProperties;
46-
contentClassName?: string;
47-
vizClassName?: string;
4850
slideNo?: boolean;
51+
autoScroll?: boolean | number;
52+
classNames?: {
53+
content?: string;
54+
viz?: string;
55+
arrowButton?: string;
56+
arrows?: string;
57+
playPauseButton?: string;
58+
playPauseIcon?: string;
59+
progressBar?: string;
60+
progressBarBg?: string;
61+
};
62+
styles?: {
63+
content?: CSSProperties;
64+
viz?: CSSProperties;
65+
arrowButton?: CSSProperties;
66+
arrows?: CSSProperties;
67+
playPauseButton?: CSSProperties;
68+
playPauseIcon?: CSSProperties;
69+
progressBar?: CSSProperties;
70+
progressBarBg?: CSSProperties;
71+
};
4972
}
5073

51-
const VizCarousel = React.forwardRef<HTMLDivElement, CardProps>(
74+
const VizCarousel = forwardRef<HTMLDivElement, CardProps>(
5275
(
5376
{
5477
className,
5578
vizWidth,
5679
slides,
57-
vizStyle,
58-
contentStyle,
59-
contentClassName,
60-
vizClassName,
80+
styles,
81+
classNames,
6182
slideNo = true,
83+
autoScroll = false,
6284
...props
6385
},
6486
ref,
6587
) => {
66-
const WrapperRef = React.useRef<HTMLDivElement>(null);
67-
const slideRefs = React.useRef<(HTMLDivElement | null)[]>([]);
88+
const WrapperRef = useRef<HTMLDivElement>(null);
89+
const slideRefs = useRef<(HTMLDivElement | null)[]>([]);
90+
const intervalRef = useRef<NodeJS.Timeout | null>(null);
91+
const [paused, setPaused] = useState(false);
92+
const [mouseOver, setMouseOver] = useState(false);
6893
const [slide, setSlide] = useState(1);
69-
70-
React.useEffect(() => {
94+
const [progress, setProgress] = useState(0);
95+
const progressInterval = 50;
96+
useEffect(() => {
7197
const observer = new IntersectionObserver(
7298
entries => {
7399
const visible = entries
@@ -97,8 +123,73 @@ const VizCarousel = React.forwardRef<HTMLDivElement, CardProps>(
97123
};
98124
}, []);
99125

126+
useEffect(() => {
127+
if (!autoScroll) return;
128+
const interval = typeof autoScroll === 'number' ? autoScroll : 5000;
129+
let currentProgress = progress;
130+
if (intervalRef.current) {
131+
clearInterval(intervalRef.current);
132+
}
133+
if (!paused && !mouseOver) {
134+
intervalRef.current = setInterval(() => {
135+
currentProgress += (progressInterval / interval) * 100;
136+
137+
if (currentProgress >= 100) {
138+
currentProgress = 0;
139+
setProgress(0);
140+
if (!WrapperRef.current) return;
141+
const parentWithDir = WrapperRef.current.closest('[dir]');
142+
const isRTL = parentWithDir?.getAttribute('dir') === 'rtl';
143+
const scrollBy = isRTL ? -280 : 280;
144+
145+
if (slide === slides.length) {
146+
WrapperRef.current.scrollTo({
147+
left: isRTL ? WrapperRef.current.scrollWidth : 0,
148+
behavior: 'smooth',
149+
});
150+
setSlide(1);
151+
} else {
152+
WrapperRef.current.scrollBy({
153+
left: scrollBy,
154+
behavior: 'smooth',
155+
});
156+
setSlide(prev => prev + 1);
157+
}
158+
} else {
159+
setProgress(currentProgress);
160+
}
161+
}, progressInterval);
162+
}
163+
return () => {
164+
if (intervalRef.current) {
165+
clearInterval(intervalRef.current);
166+
}
167+
};
168+
}, [autoScroll, mouseOver, paused, slide, slides.length, progress]);
169+
100170
return (
101-
<div ref={ref}>
171+
<div
172+
ref={ref}
173+
onMouseEnter={() => setMouseOver(true)}
174+
onMouseLeave={() => setMouseOver(false)}
175+
>
176+
{autoScroll ? (
177+
<div
178+
className={cn(
179+
'w-full h-4 bg-primary-gray-300 dark:bg-primary-gray-600 rounded-full overflow-hidden mb-4',
180+
classNames?.progressBarBg,
181+
)}
182+
style={styles?.progressBarBg}
183+
>
184+
<div
185+
className={cn(
186+
'h-full bg-accent-yellow transition-all duration-100 ease-linear',
187+
classNames?.progressBar,
188+
)}
189+
style={{ ...styles?.progressBar, width: `${progress}%` }}
190+
/>
191+
</div>
192+
) : null}
102193
<div
103194
ref={WrapperRef}
104195
className={cn(
@@ -116,13 +207,20 @@ const VizCarousel = React.forwardRef<HTMLDivElement, CardProps>(
116207
className={`flex box-border flex-wrap w-full shrink-0 snap-start ${vizWidth === 'full' ? 'flex-col items-start' : 'flex-row items-stretch'}`}
117208
>
118209
<div
119-
style={contentStyle}
120-
className={cn(cardVariants({ vizWidth }), contentClassName)}
210+
style={styles?.content}
211+
className={cn(cardVariants({ vizWidth }), classNames?.content)}
121212
>
122213
<div className='min-w-80 grow sm:grow-0'>{d.content}</div>
123214
<div className={`flex ${slideNo ? 'gap-2' : 'gap-3'} items-center shrink-0`}>
124215
<div
125-
className={`rounded-full pr-1 w-9 h-9 md:w-12 md:h-12 border-0 flex items-center justify-center rtl:rotate-180 ${slide === 1 ? 'bg-primary-gray-400 dark:bg-primary-gray-550 cursor-not-allowed' : 'cursor-pointer bg-primary-gray-700 dark:bg-primary-gray-100 hover:bg-primary-gray-600 dark:hover:bg-primary-gray-200'}`}
216+
style={styles?.arrowButton}
217+
className={cn(
218+
`rounded-full pr-1 w-9 h-9 md:w-12 md:h-12 border-0 flex items-center justify-center rtl:rotate-180`,
219+
slide === 1
220+
? 'bg-primary-gray-400 dark:bg-primary-gray-550 cursor-not-allowed'
221+
: 'cursor-pointer bg-primary-gray-700 dark:bg-primary-gray-100 hover:bg-primary-gray-600 dark:hover:bg-primary-gray-200',
222+
classNames?.arrowButton,
223+
)}
126224
onClick={() => {
127225
if (WrapperRef.current && slide !== 1) {
128226
const parentWithDir = WrapperRef.current.closest('[dir]');
@@ -132,15 +230,28 @@ const VizCarousel = React.forwardRef<HTMLDivElement, CardProps>(
132230
}
133231
}}
134232
>
135-
<ChevronLeft className='w-6 h-6 text-primary-white dark:text-primary-gray-700' />
233+
<ChevronLeft
234+
style={styles?.arrows}
235+
className={cn(
236+
'w-6 h-6 text-primary-white dark:text-primary-gray-700',
237+
classNames?.arrows,
238+
)}
239+
/>
136240
</div>
137241
{slideNo ? (
138242
<P marginBottom='none' className='px-2! shrink-0'>
139243
{slide} / {slides.length}
140244
</P>
141245
) : null}
142246
<div
143-
className={`rounded-full pl-1 w-9 h-9 md:w-12 md:h-12 border-0 flex items-center justify-center rtl:rotate-180 ${slide === slides.length ? 'bg-primary-gray-400 dark:bg-primary-gray-550 cursor-not-allowed' : 'cursor-pointer bg-primary-gray-700 dark:bg-primary-gray-100 hover:bg-primary-gray-600 dark:hover:bg-primary-gray-200'}`}
247+
className={cn(
248+
`rounded-full pl-1 w-9 h-9 md:w-12 md:h-12 border-0 flex items-center justify-center rtl:rotate-180`,
249+
slide === slides.length
250+
? 'bg-primary-gray-400 dark:bg-primary-gray-550 cursor-not-allowed'
251+
: 'cursor-pointer bg-primary-gray-700 dark:bg-primary-gray-100 hover:bg-primary-gray-600 dark:hover:bg-primary-gray-200',
252+
classNames?.arrowButton,
253+
)}
254+
style={styles?.arrowButton}
144255
onClick={() => {
145256
if (WrapperRef.current && slide !== slides.length) {
146257
const parentWithDir = WrapperRef.current.closest('[dir]');
@@ -150,13 +261,51 @@ const VizCarousel = React.forwardRef<HTMLDivElement, CardProps>(
150261
}
151262
}}
152263
>
153-
<ChevronRight className='w-6 h-6 text-primary-white dark:text-primary-gray-700' />
264+
<ChevronRight
265+
style={styles?.arrows}
266+
className={cn(
267+
'w-6 h-6 text-primary-white dark:text-primary-gray-700',
268+
classNames?.arrows,
269+
)}
270+
/>
154271
</div>
272+
{autoScroll ? (
273+
<div
274+
style={styles?.playPauseButton}
275+
className={cn(
276+
'rounded-full pl-1 w-9 h-9 md:w-12 md:h-12 border-2 border-primary-gray-600 dark:border-primary-white flex items-center justify-center rtl:rotate-180 cursor-pointer bg-transparent hover:bg-primary-gray-100 dark:hover:bg-primary-gray-600',
277+
classNames?.playPauseButton,
278+
)}
279+
onClick={() => {
280+
setPaused(!paused);
281+
}}
282+
>
283+
{paused ? (
284+
<PlayIcon
285+
style={styles?.playPauseIcon}
286+
strokeWidth={2}
287+
className={cn(
288+
'w-6 h-6 text-primary-gray-700 dark:text-primary-white',
289+
classNames?.playPauseIcon,
290+
)}
291+
/>
292+
) : (
293+
<PauseIcon
294+
strokeWidth={2}
295+
style={styles?.playPauseIcon}
296+
className={cn(
297+
'w-6 h-6 text-primary-gray-700 dark:text-primary-white',
298+
classNames?.playPauseIcon,
299+
)}
300+
/>
301+
)}
302+
</div>
303+
) : null}
155304
</div>
156305
</div>
157306
<div
158-
style={vizStyle}
159-
className={cn(vizContainerVariants({ vizWidth }), vizClassName)}
307+
style={styles?.viz}
308+
className={cn(vizContainerVariants({ vizWidth }), classNames?.viz)}
160309
>
161310
{d.viz}
162311
</div>

src/stories/VizCarousel/VizCarousel.stories.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ const meta: Meta<PagePropsAndCustomArgs> = {
1515
options: ['xs', 'sm', 'base', 'lg', 'xl', 'full'],
1616
defaultValue: { summary: 'sm' },
1717
},
18-
vizClassName: { control: { type: 'text' } },
19-
contentClassName: { control: { type: 'text' } },
20-
contentStyle: { control: { type: 'object' } },
21-
vizStyle: { control: { type: 'object' } },
18+
classNames: { control: { type: 'object' } },
19+
styles: { control: { type: 'object' } },
2220
slideNo: { control: { type: 'boolean' } },
2321
},
2422
args: {

0 commit comments

Comments
 (0)