@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.
Files changed (33) hide show
  1. package/dist/hooks/index.d.ts +3 -0
  2. package/dist/hooks/index.js +2 -0
  3. package/dist/hooks/index.js.map +1 -1
  4. package/dist/hooks/useSlideVisibility.d.ts +9 -0
  5. package/dist/hooks/useSlideVisibility.js +29 -0
  6. package/dist/hooks/useSlideVisibility.js.map +1 -0
  7. package/dist/hooks/useSlider.d.ts +64 -0
  8. package/dist/hooks/useSlider.js +103 -0
  9. package/dist/hooks/useSlider.js.map +1 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/molecules/Carousel/Carousel.d.ts +54 -0
  14. package/dist/molecules/Carousel/Carousel.js +184 -0
  15. package/dist/molecules/Carousel/Carousel.js.map +1 -0
  16. package/dist/molecules/Carousel/CarouselBullets.d.ts +35 -0
  17. package/dist/molecules/Carousel/CarouselBullets.js +12 -0
  18. package/dist/molecules/Carousel/CarouselBullets.js.map +1 -0
  19. package/dist/molecules/Carousel/CarouselItem.d.ts +11 -0
  20. package/dist/molecules/Carousel/CarouselItem.js +18 -0
  21. package/dist/molecules/Carousel/CarouselItem.js.map +1 -0
  22. package/dist/molecules/Carousel/index.d.ts +6 -0
  23. package/dist/molecules/Carousel/index.js +4 -0
  24. package/dist/molecules/Carousel/index.js.map +1 -0
  25. package/package.json +2 -2
  26. package/src/hooks/index.ts +10 -0
  27. package/src/hooks/useSlideVisibility.ts +59 -0
  28. package/src/hooks/useSlider.ts +209 -0
  29. package/src/index.ts +12 -0
  30. package/src/molecules/Carousel/Carousel.tsx +396 -0
  31. package/src/molecules/Carousel/CarouselBullets.tsx +90 -0
  32. package/src/molecules/Carousel/CarouselItem.tsx +52 -0
  33. 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'