@faststore/components 2.0.123-alpha.0 → 2.0.128-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useSlideVisibility.d.ts +9 -0
- package/dist/hooks/useSlideVisibility.js +29 -0
- package/dist/hooks/useSlideVisibility.js.map +1 -0
- package/dist/hooks/useSlider.d.ts +64 -0
- package/dist/hooks/useSlider.js +103 -0
- package/dist/hooks/useSlider.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/molecules/Carousel/Carousel.d.ts +54 -0
- package/dist/molecules/Carousel/Carousel.js +184 -0
- package/dist/molecules/Carousel/Carousel.js.map +1 -0
- package/dist/molecules/Carousel/CarouselBullets.d.ts +35 -0
- package/dist/molecules/Carousel/CarouselBullets.js +12 -0
- package/dist/molecules/Carousel/CarouselBullets.js.map +1 -0
- package/dist/molecules/Carousel/CarouselItem.d.ts +11 -0
- package/dist/molecules/Carousel/CarouselItem.js +18 -0
- package/dist/molecules/Carousel/CarouselItem.js.map +1 -0
- package/dist/molecules/Carousel/index.d.ts +6 -0
- package/dist/molecules/Carousel/index.js +4 -0
- package/dist/molecules/Carousel/index.js.map +1 -0
- package/package.json +2 -2
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useSlideVisibility.ts +59 -0
- package/src/hooks/useSlider.ts +209 -0
- package/src/index.ts +12 -0
- package/src/molecules/Carousel/Carousel.tsx +396 -0
- package/src/molecules/Carousel/CarouselBullets.tsx +90 -0
- package/src/molecules/Carousel/CarouselItem.tsx +52 -0
- package/src/molecules/Carousel/index.ts +8 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UIEvent,
|
|
3
|
+
ReactNode,
|
|
4
|
+
CSSProperties,
|
|
5
|
+
KeyboardEvent,
|
|
6
|
+
PropsWithChildren,
|
|
7
|
+
} from 'react'
|
|
8
|
+
import React, { useMemo, useRef } from 'react'
|
|
9
|
+
import type { SwipeableProps } from 'react-swipeable'
|
|
10
|
+
|
|
11
|
+
import CarouselItem from './CarouselItem'
|
|
12
|
+
import { useSlider } from '../../hooks'
|
|
13
|
+
import CarouselBullets from './CarouselBullets'
|
|
14
|
+
import { IconButton, Icon } from '../..'
|
|
15
|
+
|
|
16
|
+
const createTransformValues = (infinite: boolean, totalItems: number) => {
|
|
17
|
+
const transformMap: Record<number, number> = {}
|
|
18
|
+
const slideWidth = 100 / totalItems
|
|
19
|
+
|
|
20
|
+
for (let idx = 0; idx < totalItems; ++idx) {
|
|
21
|
+
const currIdx = infinite ? idx - 1 : idx
|
|
22
|
+
const transformValue = -(slideWidth * idx)
|
|
23
|
+
|
|
24
|
+
transformMap[currIdx] = transformValue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return transformMap
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CarouselProps extends SwipeableProps {
|
|
31
|
+
/**
|
|
32
|
+
* ID of the current instance of the component.
|
|
33
|
+
*/
|
|
34
|
+
id?: string
|
|
35
|
+
/**
|
|
36
|
+
* ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
|
|
37
|
+
*/
|
|
38
|
+
testId?: string
|
|
39
|
+
/**
|
|
40
|
+
* Returns the value of element's class content attribute.
|
|
41
|
+
*/
|
|
42
|
+
className?: string
|
|
43
|
+
/**
|
|
44
|
+
* Whether or not the Carousel is infinite slide/scroll. Only for the `slide` variant.
|
|
45
|
+
* @default true
|
|
46
|
+
*/
|
|
47
|
+
infiniteMode?: boolean
|
|
48
|
+
/**
|
|
49
|
+
* Specifies which navigation elements should be visible.
|
|
50
|
+
* @default complete
|
|
51
|
+
*/
|
|
52
|
+
controls?: 'complete' | 'navigationArrows' | 'paginationBullets'
|
|
53
|
+
/**
|
|
54
|
+
* Specifies the slide transition. Only for the `slide` variant
|
|
55
|
+
*/
|
|
56
|
+
transition?: {
|
|
57
|
+
duration: number
|
|
58
|
+
property: string
|
|
59
|
+
delay?: number
|
|
60
|
+
timing?: string
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Specifies the number of items per page.
|
|
64
|
+
* @default 1
|
|
65
|
+
*/
|
|
66
|
+
itemsPerPage?: number
|
|
67
|
+
/**
|
|
68
|
+
* Specifies the Carousel track variant.
|
|
69
|
+
* @default slide
|
|
70
|
+
*/
|
|
71
|
+
variant?: 'slide' | 'scroll'
|
|
72
|
+
/**
|
|
73
|
+
* Specifies the navigation icons.
|
|
74
|
+
*/
|
|
75
|
+
navigationIcons?: {
|
|
76
|
+
left?: ReactNode
|
|
77
|
+
right?: ReactNode
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function Carousel({
|
|
82
|
+
infiniteMode = true,
|
|
83
|
+
controls = 'complete',
|
|
84
|
+
testId = 'fs-carousel',
|
|
85
|
+
transition = {
|
|
86
|
+
duration: 400,
|
|
87
|
+
property: 'transform',
|
|
88
|
+
},
|
|
89
|
+
children,
|
|
90
|
+
className,
|
|
91
|
+
id = 'fs-carousel',
|
|
92
|
+
variant = 'slide',
|
|
93
|
+
itemsPerPage = 1,
|
|
94
|
+
navigationIcons = undefined,
|
|
95
|
+
...swipeableConfigOverrides
|
|
96
|
+
}: PropsWithChildren<CarouselProps>) {
|
|
97
|
+
const carouselTrackRef = useRef<HTMLUListElement>(null)
|
|
98
|
+
const isSlideCarousel = variant === 'slide'
|
|
99
|
+
const isScrollCarousel = variant === 'scroll'
|
|
100
|
+
const childrenArray = React.Children.toArray(children)
|
|
101
|
+
const childrenCount = childrenArray.length
|
|
102
|
+
const numberOfSlides = infiniteMode ? childrenCount + 2 : childrenCount
|
|
103
|
+
const slidingTransition = `${transition.property} ${transition.duration}ms ${
|
|
104
|
+
transition.timing ?? ''
|
|
105
|
+
} ${transition.delay ?? ''}`
|
|
106
|
+
|
|
107
|
+
const { handlers, slide, sliderState, sliderDispatch } = useSlider({
|
|
108
|
+
itemsPerPage,
|
|
109
|
+
infiniteMode,
|
|
110
|
+
totalItems: childrenCount,
|
|
111
|
+
shouldSlideOnSwipe: isSlideCarousel,
|
|
112
|
+
...swipeableConfigOverrides,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const pagesCount = Math.ceil(childrenCount / sliderState.itemsPerPage)
|
|
116
|
+
|
|
117
|
+
const showNavigationArrows =
|
|
118
|
+
pagesCount !== 1 &&
|
|
119
|
+
(controls === 'complete' || controls === 'navigationArrows')
|
|
120
|
+
|
|
121
|
+
const showPaginationBullets =
|
|
122
|
+
pagesCount !== 1 &&
|
|
123
|
+
(controls === 'complete' || controls === 'paginationBullets')
|
|
124
|
+
|
|
125
|
+
const transformValues = useMemo(
|
|
126
|
+
() => createTransformValues(infiniteMode, numberOfSlides),
|
|
127
|
+
[numberOfSlides, infiniteMode]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const postRenderedSlides =
|
|
131
|
+
infiniteMode && children ? childrenArray.slice(0, 1) : []
|
|
132
|
+
|
|
133
|
+
const preRenderedSlides =
|
|
134
|
+
infiniteMode && children ? childrenArray.slice(childrenCount - 1) : []
|
|
135
|
+
|
|
136
|
+
const slides = preRenderedSlides.concat(
|
|
137
|
+
(children as any) ?? [],
|
|
138
|
+
postRenderedSlides
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const slideCarouselTrackStyle: CSSProperties = useMemo(
|
|
142
|
+
() => ({
|
|
143
|
+
display: 'flex',
|
|
144
|
+
width: `${numberOfSlides * 100}%`,
|
|
145
|
+
transition: sliderState.sliding ? slidingTransition : undefined,
|
|
146
|
+
transform: `translate3d(${
|
|
147
|
+
transformValues[sliderState.currentPage]
|
|
148
|
+
}%, 0, 0)`,
|
|
149
|
+
}),
|
|
150
|
+
[
|
|
151
|
+
numberOfSlides,
|
|
152
|
+
transformValues,
|
|
153
|
+
slidingTransition,
|
|
154
|
+
sliderState.sliding,
|
|
155
|
+
sliderState.currentPage,
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const scrollCarouselTrackStyle: CSSProperties = useMemo(
|
|
160
|
+
() => ({
|
|
161
|
+
width: '100%',
|
|
162
|
+
display: 'block',
|
|
163
|
+
overflowX: 'scroll',
|
|
164
|
+
whiteSpace: 'nowrap',
|
|
165
|
+
}),
|
|
166
|
+
[]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const carouselTrackStyle =
|
|
170
|
+
((isSlideCarousel && slideCarouselTrackStyle) as CSSProperties) ||
|
|
171
|
+
((isScrollCarousel && scrollCarouselTrackStyle) as CSSProperties)
|
|
172
|
+
|
|
173
|
+
const slidePrevious = () => {
|
|
174
|
+
if (
|
|
175
|
+
sliderState.sliding ||
|
|
176
|
+
(!infiniteMode && sliderState.currentPage === 0)
|
|
177
|
+
) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
slide('previous', sliderDispatch)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const slideNext = () => {
|
|
185
|
+
if (
|
|
186
|
+
sliderState.sliding ||
|
|
187
|
+
(!infiniteMode && sliderState.currentPage === childrenCount - 1)
|
|
188
|
+
) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
slide('next', sliderDispatch)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const onScrollTrack = (event: UIEvent) => {
|
|
196
|
+
if (isSlideCarousel || itemsPerPage > 1) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const itemWidth = Number(event.currentTarget.firstElementChild?.scrollWidth)
|
|
201
|
+
const scrollOffset = event.currentTarget?.scrollLeft
|
|
202
|
+
const formatter = scrollOffset > itemWidth / 2 ? Math.round : Math.floor
|
|
203
|
+
const page = formatter(scrollOffset / itemWidth)
|
|
204
|
+
|
|
205
|
+
slide(page, sliderDispatch)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const onTransitionTrackEnd = () => {
|
|
209
|
+
sliderDispatch({
|
|
210
|
+
type: 'STOP_SLIDE',
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
if (infiniteMode && sliderState.currentItem >= childrenCount) {
|
|
214
|
+
sliderDispatch({
|
|
215
|
+
type: 'GO_TO_PAGE',
|
|
216
|
+
payload: {
|
|
217
|
+
pageIndex: 0,
|
|
218
|
+
shouldSlide: false,
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (infiniteMode && sliderState.currentItem < 0) {
|
|
224
|
+
sliderDispatch({
|
|
225
|
+
type: 'GO_TO_PAGE',
|
|
226
|
+
payload: {
|
|
227
|
+
pageIndex: sliderState.totalPages - 1,
|
|
228
|
+
shouldSlide: false,
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const onScrollPagination = async (
|
|
235
|
+
index: number,
|
|
236
|
+
slideDirection?: 'previous' | 'next'
|
|
237
|
+
) => {
|
|
238
|
+
if (slideDirection === 'previous' && sliderState.currentPage === 0) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (
|
|
243
|
+
slideDirection === 'next' &&
|
|
244
|
+
sliderState.currentPage === sliderState.totalPages - 1
|
|
245
|
+
) {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let scrollOffset
|
|
250
|
+
const carouselItemsWidth = Number(
|
|
251
|
+
carouselTrackRef.current?.firstElementChild?.clientWidth
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if (itemsPerPage > 1) {
|
|
255
|
+
scrollOffset = index * carouselItemsWidth * itemsPerPage
|
|
256
|
+
} else {
|
|
257
|
+
scrollOffset = index * carouselItemsWidth - carouselItemsWidth * 0.125
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
carouselTrackRef.current?.scrollTo({
|
|
261
|
+
left: scrollOffset,
|
|
262
|
+
behavior: 'smooth',
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
slide(index, sliderDispatch)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// accessible behavior for tablist
|
|
269
|
+
const handleBulletsKeyDown = (event: KeyboardEvent) => {
|
|
270
|
+
switch (event.key) {
|
|
271
|
+
case 'ArrowLeft': {
|
|
272
|
+
isSlideCarousel && slidePrevious()
|
|
273
|
+
isScrollCarousel &&
|
|
274
|
+
onScrollPagination(sliderState.currentPage - 1, 'previous')
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case 'ArrowRight': {
|
|
279
|
+
isSlideCarousel && slideNext()
|
|
280
|
+
isScrollCarousel &&
|
|
281
|
+
onScrollPagination(sliderState.currentPage + 1, 'next')
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'Home': {
|
|
286
|
+
slide(0, sliderDispatch)
|
|
287
|
+
break
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'End': {
|
|
291
|
+
slide(childrenCount - 1, sliderDispatch)
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
default:
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<section
|
|
301
|
+
id={id}
|
|
302
|
+
data-fs-carousel
|
|
303
|
+
className={className}
|
|
304
|
+
data-testid={testId}
|
|
305
|
+
aria-label="carousel"
|
|
306
|
+
aria-roledescription="carousel"
|
|
307
|
+
>
|
|
308
|
+
<div
|
|
309
|
+
data-fs-carousel-track-container
|
|
310
|
+
style={{
|
|
311
|
+
width: '100%',
|
|
312
|
+
overflow: 'hidden',
|
|
313
|
+
display: isScrollCarousel ? 'block' : undefined,
|
|
314
|
+
}}
|
|
315
|
+
{...handlers}
|
|
316
|
+
>
|
|
317
|
+
<ul
|
|
318
|
+
aria-live="polite"
|
|
319
|
+
ref={carouselTrackRef}
|
|
320
|
+
style={carouselTrackStyle}
|
|
321
|
+
data-fs-carousel-track
|
|
322
|
+
onScroll={onScrollTrack}
|
|
323
|
+
onTransitionEnd={onTransitionTrackEnd}
|
|
324
|
+
>
|
|
325
|
+
{slides.map((currentSlide, idx) => (
|
|
326
|
+
<CarouselItem
|
|
327
|
+
index={idx}
|
|
328
|
+
key={String(idx)}
|
|
329
|
+
state={sliderState}
|
|
330
|
+
totalItems={childrenCount}
|
|
331
|
+
infiniteMode={infiniteMode}
|
|
332
|
+
isScrollCarousel={isScrollCarousel}
|
|
333
|
+
>
|
|
334
|
+
{currentSlide}
|
|
335
|
+
</CarouselItem>
|
|
336
|
+
))}
|
|
337
|
+
</ul>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{showNavigationArrows && (
|
|
341
|
+
<div data-fs-carousel-controls>
|
|
342
|
+
<IconButton
|
|
343
|
+
data-fs-carousel-control="left"
|
|
344
|
+
aria-controls={id}
|
|
345
|
+
aria-label="previous"
|
|
346
|
+
icon={
|
|
347
|
+
navigationIcons?.left ?? (
|
|
348
|
+
<Icon name="ArrowLeft" width={20} height={20} weight="bold" />
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
onClick={() => {
|
|
352
|
+
isSlideCarousel && slidePrevious()
|
|
353
|
+
isScrollCarousel &&
|
|
354
|
+
onScrollPagination(sliderState.currentPage - 1, 'previous')
|
|
355
|
+
}}
|
|
356
|
+
/>
|
|
357
|
+
<IconButton
|
|
358
|
+
data-fs-carousel-control="right"
|
|
359
|
+
aria-controls={id}
|
|
360
|
+
aria-label="next"
|
|
361
|
+
icon={
|
|
362
|
+
navigationIcons?.right ?? (
|
|
363
|
+
<Icon name="ArrowRight" width={20} height={20} weight="bold" />
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
onClick={() => {
|
|
367
|
+
isSlideCarousel && slideNext()
|
|
368
|
+
isScrollCarousel &&
|
|
369
|
+
onScrollPagination(sliderState.currentPage + 1, 'next')
|
|
370
|
+
}}
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
373
|
+
)}
|
|
374
|
+
|
|
375
|
+
{showPaginationBullets && (
|
|
376
|
+
<CarouselBullets
|
|
377
|
+
tabIndex={0}
|
|
378
|
+
activeBullet={sliderState.currentPage}
|
|
379
|
+
totalQuantity={pagesCount}
|
|
380
|
+
onKeyDown={handleBulletsKeyDown}
|
|
381
|
+
onClick={async (_, idx) => {
|
|
382
|
+
isSlideCarousel &&
|
|
383
|
+
!sliderState.sliding &&
|
|
384
|
+
slide(idx, sliderDispatch)
|
|
385
|
+
|
|
386
|
+
isScrollCarousel && onScrollPagination(idx)
|
|
387
|
+
}}
|
|
388
|
+
onFocus={(event) => event.currentTarget.focus()}
|
|
389
|
+
ariaControlsGenerator={(idx) => `carousel-item-${idx}`}
|
|
390
|
+
/>
|
|
391
|
+
)}
|
|
392
|
+
</section>
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export default Carousel
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { HTMLAttributes, MouseEvent } from 'react'
|
|
2
|
+
import React, { forwardRef, useMemo } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button } from '../..'
|
|
5
|
+
|
|
6
|
+
export interface CarouselBulletsProps
|
|
7
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, 'onClick' | 'role'> {
|
|
8
|
+
/**
|
|
9
|
+
* Number of bullets that should be rendered.
|
|
10
|
+
*/
|
|
11
|
+
totalQuantity: number
|
|
12
|
+
/**
|
|
13
|
+
* The currently active bullet (zero-indexed).
|
|
14
|
+
*/
|
|
15
|
+
activeBullet: number
|
|
16
|
+
/**
|
|
17
|
+
* Event handler for clicks on each bullet. The handler will be called with
|
|
18
|
+
* the index of the bullet that received the click.
|
|
19
|
+
*/
|
|
20
|
+
onClick: (e: MouseEvent, bulletIdx: number) => void
|
|
21
|
+
/**
|
|
22
|
+
* ID to find this component in testing tools (e.g.: cypress,
|
|
23
|
+
* testing-library, and jest).
|
|
24
|
+
*/
|
|
25
|
+
testId?: string
|
|
26
|
+
/**
|
|
27
|
+
* Function that should be used to generate the aria-label attribute added
|
|
28
|
+
* to each bullet that is inactive. It receives the bullet index as an
|
|
29
|
+
* argument so that it can be interpolated in the generated string.
|
|
30
|
+
*/
|
|
31
|
+
ariaLabelGenerator?: (index: number, isActive: boolean) => string
|
|
32
|
+
/**
|
|
33
|
+
* Function that should be used to generate the aria-controls attribute added
|
|
34
|
+
* to each bullet. It receives the bullet index as argument and should return a string.
|
|
35
|
+
*/
|
|
36
|
+
ariaControlsGenerator?: (index: number) => string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const defaultAriaLabel = (idx: number, isActive: boolean) =>
|
|
40
|
+
isActive ? 'Current page' : `Go to page ${idx + 1}`
|
|
41
|
+
|
|
42
|
+
const CarouselBullets = forwardRef<HTMLDivElement, CarouselBulletsProps>(
|
|
43
|
+
function Bullets(
|
|
44
|
+
{
|
|
45
|
+
totalQuantity,
|
|
46
|
+
activeBullet,
|
|
47
|
+
onClick,
|
|
48
|
+
testId = 'fs-carousel-bullets',
|
|
49
|
+
ariaLabelGenerator = defaultAriaLabel,
|
|
50
|
+
ariaControlsGenerator,
|
|
51
|
+
...otherProps
|
|
52
|
+
},
|
|
53
|
+
ref
|
|
54
|
+
) {
|
|
55
|
+
const bulletIndexes = useMemo(
|
|
56
|
+
() => Array(totalQuantity).fill(0),
|
|
57
|
+
[totalQuantity]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
ref={ref}
|
|
63
|
+
data-fs-carousel-bullets
|
|
64
|
+
data-testid={testId}
|
|
65
|
+
role="tablist"
|
|
66
|
+
{...otherProps}
|
|
67
|
+
>
|
|
68
|
+
{bulletIndexes.map((_, idx) => {
|
|
69
|
+
const isActive = activeBullet === idx
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Button
|
|
73
|
+
key={idx}
|
|
74
|
+
role="tab"
|
|
75
|
+
tabIndex={-1}
|
|
76
|
+
data-fs-carousel-bullet
|
|
77
|
+
testId={`${testId}-bullet`}
|
|
78
|
+
onClick={(e) => onClick(e, idx)}
|
|
79
|
+
aria-label={ariaLabelGenerator(idx, isActive)}
|
|
80
|
+
aria-controls={ariaControlsGenerator?.(idx)}
|
|
81
|
+
aria-selected={isActive}
|
|
82
|
+
/>
|
|
83
|
+
)
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
export default CarouselBullets
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { CSSProperties, PropsWithChildren, HTMLAttributes } from 'react'
|
|
3
|
+
import { useSlideVisibility, SliderState } from '../../hooks'
|
|
4
|
+
|
|
5
|
+
export interface CarouselItemProps extends HTMLAttributes<HTMLLIElement> {
|
|
6
|
+
index: number
|
|
7
|
+
totalItems: number
|
|
8
|
+
state: SliderState
|
|
9
|
+
infiniteMode: boolean
|
|
10
|
+
isScrollCarousel: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function CarouselItem({
|
|
14
|
+
index,
|
|
15
|
+
state,
|
|
16
|
+
children,
|
|
17
|
+
totalItems,
|
|
18
|
+
infiniteMode,
|
|
19
|
+
isScrollCarousel,
|
|
20
|
+
}: PropsWithChildren<CarouselItemProps>) {
|
|
21
|
+
const { isItemVisible, shouldRenderItem } = useSlideVisibility({
|
|
22
|
+
totalItems,
|
|
23
|
+
currentSlide: state.currentItem,
|
|
24
|
+
itemsPerPage: state.itemsPerPage,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const style =
|
|
28
|
+
((!isScrollCarousel && { width: '100%' }) as CSSProperties) ||
|
|
29
|
+
((isScrollCarousel && {
|
|
30
|
+
maxWidth: '60%',
|
|
31
|
+
display: 'inline-block',
|
|
32
|
+
}) as CSSProperties)
|
|
33
|
+
|
|
34
|
+
const shouldDisplayItem =
|
|
35
|
+
isScrollCarousel || shouldRenderItem(index - Number(infiniteMode))
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<li
|
|
39
|
+
style={style}
|
|
40
|
+
data-fs-carousel-item
|
|
41
|
+
aria-roledescription="slide"
|
|
42
|
+
id={`carousel-item-${index}`}
|
|
43
|
+
data-fs-carousel-item-visible={
|
|
44
|
+
isItemVisible(index - Number(infiniteMode)) || undefined
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
{shouldDisplayItem ? children : null}
|
|
48
|
+
</li>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default CarouselItem
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default } from './Carousel'
|
|
2
|
+
export type { CarouselProps } from './Carousel'
|
|
3
|
+
|
|
4
|
+
export { default as CarouselItem } from './CarouselItem'
|
|
5
|
+
export type { CarouselItemProps } from './CarouselItem'
|
|
6
|
+
|
|
7
|
+
export { default as CarouselBullets } from './CarouselBullets'
|
|
8
|
+
export type { CarouselBulletsProps } from './CarouselBullets'
|