11'use client' ;
22
3- import { useWidth } from '@/lib/utils/useWidth' ;
43import theme from '@/styles/theme' ;
54import { KeyboardArrowRight } from '@mui/icons-material' ;
65import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft' ;
7- import { Box , Breakpoint , IconButton } from '@mui/material' ;
6+ import { Box , IconButton } from '@mui/material' ;
87import { Carousel as NukaCarousel , useCarousel } from 'nuka-carousel' ;
98
109interface CarouselProps {
1110 items : Array < React . ReactNode > ;
1211 theme : 'primary' | 'secondary' ;
13- showArrows ?: boolean ;
14- arrowPosition ?: 'side' | 'bottom' ;
1512 title ?: string ;
16- slidesPerView ?: { xs : number ; sm : number ; md : number ; lg : number ; xl : number } ;
17- afterSlideHandle ?: ( newSlideIndex : number ) => void ;
1813}
1914
20- const numberSlidesToWidthMap : { [ key : number ] : string } = {
15+ // These are purposely not exactly half or third of the screen width
16+ // because nuka-carousel struggles to calculate the width of slides correctly
17+ // when the parent container has a flex layout. This is a workaround to avoid that issue.
18+ const numSlidesToWidthMap : { [ key : number ] : string } = {
2119 1 : '100%' ,
22- 2 : '50%' ,
23- 3 : '33.33 %' ,
24- 4 : '25%' ,
20+ 2 : '50.01 %' ,
21+ 3 : '33.34 %' ,
22+ 4 : '25.01 %' ,
2523} ;
2624
27- const tabletSlidesToWidthMap : { [ key : number ] : string } = {
28- 1 : '100%' ,
29- 2 : '50%' ,
30- 3 : '25%' ,
25+ export const getSlideWidth = (
26+ numMobileSlides : number ,
27+ numTabletSlides : number ,
28+ numDesktopSlides : number ,
29+ ) => {
30+ return {
31+ width : [
32+ numSlidesToWidthMap [ numMobileSlides || 1 ] ,
33+ numSlidesToWidthMap [ numTabletSlides || 1 ] ,
34+ numSlidesToWidthMap [ numDesktopSlides || 1 ] ,
35+ ] ,
36+ minWidth : [
37+ numSlidesToWidthMap [ numMobileSlides || 1 ] ,
38+ numSlidesToWidthMap [ numTabletSlides || 1 ] ,
39+ numSlidesToWidthMap [ numDesktopSlides || 1 ] ,
40+ ] ,
41+ } ;
3142} ;
3243
3344// Dots and arrows in 1 component because of the design
34- const CustomDots = ( {
35- showArrows = false ,
36- arrowPosition = 'bottom' ,
37- carouselTheme = 'primary' ,
38- } : {
39- showArrows : boolean ;
40- arrowPosition : 'side' | 'bottom' ;
41- carouselTheme : 'primary' | 'secondary' ;
42- } ) => {
45+ const CustomDots = ( { carouselTheme = 'primary' } : { carouselTheme : 'primary' | 'secondary' } ) => {
46+ // totalPages are not calculated correctly in the nuka-carousel so causes a bug.
47+ // In the case that the scroll width is less than 1.5 times the screen width, it will round down the number of pages
48+ // and cause the dot count to be incorrect.
49+ // If you go into useMeasurements hook in nuka-carousel, you can see that it calculates the total pages and rounds down the number of pages.
50+ // This is particularly an issue if you have 1.4 pages, this means the dots will not render!
51+ // Deciding to park this issue for now as it needs a bug report to nuka-carousel.
4352 const { currentPage, totalPages, goBack, goForward, goToPage } = useCarousel ( ) ;
53+ if ( totalPages < 2 ) return < > </ > ;
4454
4555 const getBackground = ( index : number ) =>
4656 currentPage === index
@@ -61,23 +71,21 @@ const CustomDots = ({
6171 display = "flex"
6272 marginTop = { 2 }
6373 >
64- { showArrows && arrowPosition == 'bottom' && (
65- < Box alignContent = "center" >
66- < IconButton
67- onClick = { goBack }
68- sx = { {
69- backgroundColor : theme . palette . primary . dark ,
70- color : theme . palette . common . white ,
71- '&:hover' : {
72- backgroundColor : theme . palette . common . white ,
73- color : theme . palette . primary . dark ,
74- } ,
75- } }
76- >
77- < KeyboardArrowLeft > </ KeyboardArrowLeft >
78- </ IconButton >
79- </ Box >
80- ) }
74+ < Box alignContent = "center" >
75+ < IconButton
76+ onClick = { goBack }
77+ sx = { {
78+ backgroundColor : theme . palette . primary . dark ,
79+ color : theme . palette . common . white ,
80+ '&:hover' : {
81+ backgroundColor : theme . palette . common . white ,
82+ color : theme . palette . primary . dark ,
83+ } ,
84+ } }
85+ >
86+ < KeyboardArrowLeft > </ KeyboardArrowLeft >
87+ </ IconButton >
88+ </ Box >
8189 < Box >
8290 < Box display = "flex" gap = { 1 } alignContent = "center" width = "100%" >
8391 { [ ...Array ( totalPages ) ] . map ( ( _ , index ) => (
@@ -99,59 +107,36 @@ const CustomDots = ({
99107 ) ) }
100108 </ Box >
101109 </ Box >
102- { showArrows && arrowPosition == 'bottom' && (
103- < Box alignContent = "center" >
104- < IconButton
105- onClick = { goForward }
106- sx = { {
107- backgroundColor : theme . palette . primary . dark ,
108- color : theme . palette . common . white ,
109- '&:hover' : {
110- backgroundColor : theme . palette . common . white ,
111- color : theme . palette . primary . dark ,
112- } ,
113- } }
114- >
115- < KeyboardArrowRight > </ KeyboardArrowRight >
116- </ IconButton >
117- </ Box >
118- ) }
110+ < Box alignContent = "center" >
111+ < IconButton
112+ onClick = { goForward }
113+ sx = { {
114+ backgroundColor : theme . palette . primary . dark ,
115+ color : theme . palette . common . white ,
116+ '&:hover' : {
117+ backgroundColor : theme . palette . common . white ,
118+ color : theme . palette . primary . dark ,
119+ } ,
120+ } }
121+ >
122+ < KeyboardArrowRight > </ KeyboardArrowRight >
123+ </ IconButton >
124+ </ Box >
119125 </ Box >
120126 ) ;
121127} ;
122128
123129const Carousel = ( props : CarouselProps ) => {
124- const {
125- items,
126- showArrows = false ,
127- arrowPosition = 'bottom' ,
128- title = 'carousel' ,
129- theme = 'primary' ,
130- slidesPerView = {
131- xs : 1 ,
132- sm : 1 ,
133- md : 1 ,
134- lg : 1 ,
135- xl : 1 ,
136- } ,
137- } = props ;
138- const width = useWidth ( ) ;
139- const currentSlidePerView = slidesPerView [ width ] ;
140- const navigationEnabled = isNavigationEnabled ( width , items . length , slidesPerView ) ;
141- const scrollDistance =
142- currentSlidePerView < 2 || items . length / currentSlidePerView < 2 ? 'slide' : 'screen' ;
130+ const { items, title = 'carousel' , theme = 'primary' } = props ;
131+
143132 return (
144133 < NukaCarousel
145134 id = { title }
146- showArrows = { navigationEnabled && showArrows && arrowPosition == 'side' }
147- showDots = { navigationEnabled }
135+ showDots = { true }
148136 swiping = { true }
149- dots = {
150- < CustomDots showArrows = { showArrows } arrowPosition = { arrowPosition } carouselTheme = { theme } />
151- }
137+ dots = { < CustomDots carouselTheme = { theme } /> }
152138 title = { title }
153- afterSlide = { props . afterSlideHandle }
154- scrollDistance = { scrollDistance }
139+ scrollDistance = { 'screen' }
155140 >
156141 { items }
157142 </ NukaCarousel >
@@ -162,30 +147,41 @@ export default Carousel;
162147// Note that if you use this function, the carousel will be buggy in some screen sizes as it struggles to calculate the width
163148// of slides correctly when the parent container has a flex layout. Avoid using this function if possible.
164149// Use it when you can't set a fixed width for the slides, like in the StoryblokCarousel component.
165- export const getSlideWidth = (
166- numberMobileSlides : number ,
167- numberTabletSlides : number ,
168- numberDesktopSlides : number ,
169- ) => {
170- return {
171- width : [
172- numberSlidesToWidthMap [ numberMobileSlides || 1 ] ,
173- tabletSlidesToWidthMap [ numberTabletSlides || 1 ] ,
174- numberSlidesToWidthMap [ numberDesktopSlides || 1 ] ,
175- ] ,
176- minWidth : [
177- numberSlidesToWidthMap [ numberMobileSlides || 1 ] ,
178- tabletSlidesToWidthMap [ numberTabletSlides || 1 ] ,
179- numberSlidesToWidthMap [ numberDesktopSlides || 1 ] ,
180- ] ,
181- } ;
150+
151+ type CarouselItemContainerProps = {
152+ children : React . ReactNode ;
153+ slidesPerScreen ?: number [ ] ; // [mobile, tablet, desktop]
154+ customPadding ?: number ;
155+ customWidth ?: string | Array < string > ;
182156} ;
183157
184- const isNavigationEnabled = (
185- currentBreakpoint : Breakpoint ,
186- numberOfSlides : number ,
187- slidesPerBreakpoint : Record < Breakpoint , number > ,
188- ) => {
189- const currentSlidesPerBreakpoint = slidesPerBreakpoint [ currentBreakpoint ] ;
190- return currentSlidesPerBreakpoint < numberOfSlides ;
158+ export const CarouselItemContainer = ( {
159+ children,
160+ slidesPerScreen = [ 1 , 2 , 3 ] ,
161+ customPadding = 0.5 ,
162+ customWidth,
163+ } : CarouselItemContainerProps ) => {
164+ return (
165+ < Box
166+ sx = { {
167+ boxSizing : 'border-box' , // Ensure padding is included in width calculation
168+ padding : customPadding ,
169+ display : 'inline-block' ,
170+ ':first-of-type' : {
171+ paddingLeft : [ '0 !important' , '0 !important' , '0 !important' ] ,
172+ } ,
173+ ...( customWidth
174+ ? { minWidth : customWidth , width : customWidth }
175+ : {
176+ ...getSlideWidth (
177+ slidesPerScreen [ 0 ] || 1 ,
178+ slidesPerScreen [ 1 ] || 1 ,
179+ slidesPerScreen [ 2 ] || 1 ,
180+ ) ,
181+ } ) ,
182+ } }
183+ >
184+ { children }
185+ </ Box >
186+ ) ;
191187} ;
0 commit comments