@faststore/components 2.0.123-alpha.0 → 2.0.132-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/atoms/Button/Button.js +7 -6
- package/dist/atoms/Button/Button.js.map +1 -1
- 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/Alert/Alert.js +2 -4
- package/dist/molecules/Alert/Alert.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 +39 -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/dist/molecules/IconButton/IconButton.js +2 -2
- package/dist/molecules/IconButton/IconButton.js.map +1 -1
- package/dist/molecules/InputField/InputField.js +1 -1
- package/dist/molecules/InputField/InputField.js.map +1 -1
- package/dist/molecules/LinkButton/LinkButton.js +4 -3
- package/dist/molecules/LinkButton/LinkButton.js.map +1 -1
- package/dist/molecules/QuantitySelector/QuantitySelector.js +2 -2
- package/dist/molecules/QuantitySelector/QuantitySelector.js.map +1 -1
- package/dist/molecules/RegionBar/RegionBar.js +2 -3
- package/dist/molecules/RegionBar/RegionBar.js.map +1 -1
- package/dist/molecules/SearchInputField/SearchInputField.js +1 -1
- package/dist/molecules/SearchInputField/SearchInputField.js.map +1 -1
- package/dist/organisms/ImageGallery/ImageGallerySelector.js +2 -2
- package/dist/organisms/ImageGallery/ImageGallerySelector.js.map +1 -1
- package/package.json +2 -2
- package/src/atoms/Button/Button.tsx +13 -11
- 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/Alert/Alert.tsx +2 -6
- package/src/molecules/Carousel/Carousel.tsx +398 -0
- package/src/molecules/Carousel/CarouselBullets.tsx +95 -0
- package/src/molecules/Carousel/CarouselItem.tsx +53 -0
- package/src/molecules/Carousel/index.ts +8 -0
- package/src/molecules/IconButton/IconButton.tsx +2 -0
- package/src/molecules/InputField/InputField.tsx +1 -1
- package/src/molecules/LinkButton/LinkButton.tsx +5 -3
- package/src/molecules/QuantitySelector/QuantitySelector.tsx +2 -0
- package/src/molecules/RegionBar/RegionBar.tsx +1 -2
- package/src/molecules/SearchInputField/SearchInputField.tsx +1 -1
- package/src/organisms/ImageGallery/ImageGallerySelector.tsx +1 -3
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CSSProperties,
|
|
3
|
+
KeyboardEvent,
|
|
4
|
+
PropsWithChildren,
|
|
5
|
+
ReactNode,
|
|
6
|
+
UIEvent,
|
|
7
|
+
} from 'react'
|
|
8
|
+
import React, { useMemo, useRef } from 'react'
|
|
9
|
+
import type { SwipeableProps } from 'react-swipeable'
|
|
10
|
+
|
|
11
|
+
import { Icon, IconButton } from '../..'
|
|
12
|
+
import { useSlider } from '../../hooks'
|
|
13
|
+
import CarouselBullets from './CarouselBullets'
|
|
14
|
+
import CarouselItem from './CarouselItem'
|
|
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
|
+
id={id}
|
|
328
|
+
index={idx}
|
|
329
|
+
key={String(idx)}
|
|
330
|
+
state={sliderState}
|
|
331
|
+
totalItems={childrenCount}
|
|
332
|
+
infiniteMode={infiniteMode}
|
|
333
|
+
isScrollCarousel={isScrollCarousel}
|
|
334
|
+
>
|
|
335
|
+
{currentSlide}
|
|
336
|
+
</CarouselItem>
|
|
337
|
+
))}
|
|
338
|
+
</ul>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
{showNavigationArrows && (
|
|
342
|
+
<div data-fs-carousel-controls>
|
|
343
|
+
<IconButton
|
|
344
|
+
data-fs-carousel-control="left"
|
|
345
|
+
aria-controls={id}
|
|
346
|
+
aria-label="previous"
|
|
347
|
+
icon={
|
|
348
|
+
navigationIcons?.left ?? (
|
|
349
|
+
<Icon name="ArrowLeft" width={20} height={20} weight="bold" />
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
onClick={() => {
|
|
353
|
+
isSlideCarousel && slidePrevious()
|
|
354
|
+
isScrollCarousel &&
|
|
355
|
+
onScrollPagination(sliderState.currentPage - 1, 'previous')
|
|
356
|
+
}}
|
|
357
|
+
/>
|
|
358
|
+
<IconButton
|
|
359
|
+
data-fs-carousel-control="right"
|
|
360
|
+
aria-controls={id}
|
|
361
|
+
aria-label="next"
|
|
362
|
+
icon={
|
|
363
|
+
navigationIcons?.right ?? (
|
|
364
|
+
<Icon name="ArrowRight" width={20} height={20} weight="bold" />
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
onClick={() => {
|
|
368
|
+
isSlideCarousel && slideNext()
|
|
369
|
+
isScrollCarousel &&
|
|
370
|
+
onScrollPagination(sliderState.currentPage + 1, 'next')
|
|
371
|
+
}}
|
|
372
|
+
/>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{showPaginationBullets && (
|
|
377
|
+
<CarouselBullets
|
|
378
|
+
id={id}
|
|
379
|
+
tabIndex={0}
|
|
380
|
+
activeBullet={sliderState.currentPage}
|
|
381
|
+
totalQuantity={pagesCount}
|
|
382
|
+
onKeyDown={handleBulletsKeyDown}
|
|
383
|
+
onClick={async (_, idx) => {
|
|
384
|
+
isSlideCarousel &&
|
|
385
|
+
!sliderState.sliding &&
|
|
386
|
+
slide(idx, sliderDispatch)
|
|
387
|
+
|
|
388
|
+
isScrollCarousel && onScrollPagination(idx)
|
|
389
|
+
}}
|
|
390
|
+
onFocus={(event) => event.currentTarget.focus()}
|
|
391
|
+
ariaControlsGenerator={(idx) => `${id}-carousel-item-${idx}`}
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
</section>
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export default Carousel
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
* ID of the current instance of the component.
|
|
10
|
+
*/
|
|
11
|
+
id: string
|
|
12
|
+
/**
|
|
13
|
+
* Number of bullets that should be rendered.
|
|
14
|
+
*/
|
|
15
|
+
totalQuantity: number
|
|
16
|
+
/**
|
|
17
|
+
* The currently active bullet (zero-indexed).
|
|
18
|
+
*/
|
|
19
|
+
activeBullet: number
|
|
20
|
+
/**
|
|
21
|
+
* Event handler for clicks on each bullet. The handler will be called with
|
|
22
|
+
* the index of the bullet that received the click.
|
|
23
|
+
*/
|
|
24
|
+
onClick: (e: MouseEvent, bulletIdx: number) => void
|
|
25
|
+
/**
|
|
26
|
+
* ID to find this component in testing tools (e.g.: cypress,
|
|
27
|
+
* testing-library, and jest).
|
|
28
|
+
*/
|
|
29
|
+
testId?: string
|
|
30
|
+
/**
|
|
31
|
+
* Function that should be used to generate the aria-label attribute added
|
|
32
|
+
* to each bullet that is inactive. It receives the bullet index as an
|
|
33
|
+
* argument so that it can be interpolated in the generated string.
|
|
34
|
+
*/
|
|
35
|
+
ariaLabelGenerator?: (id: string, index: number, isActive: boolean) => string
|
|
36
|
+
/**
|
|
37
|
+
* Function that should be used to generate the aria-controls attribute added
|
|
38
|
+
* to each bullet. It receives the bullet index as argument and should return a string.
|
|
39
|
+
*/
|
|
40
|
+
ariaControlsGenerator?: (index: number) => string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const defaultAriaLabel = (id: string, idx: number, isActive: boolean) =>
|
|
44
|
+
isActive ? `Current page from ${id}` : `Go to page ${idx + 1} from ${id}`
|
|
45
|
+
|
|
46
|
+
const CarouselBullets = forwardRef<HTMLDivElement, CarouselBulletsProps>(
|
|
47
|
+
function Bullets(
|
|
48
|
+
{
|
|
49
|
+
id,
|
|
50
|
+
totalQuantity,
|
|
51
|
+
activeBullet,
|
|
52
|
+
onClick,
|
|
53
|
+
testId = 'fs-carousel-bullets',
|
|
54
|
+
ariaLabelGenerator = defaultAriaLabel,
|
|
55
|
+
ariaControlsGenerator,
|
|
56
|
+
...otherProps
|
|
57
|
+
},
|
|
58
|
+
ref
|
|
59
|
+
) {
|
|
60
|
+
const bulletIndexes = useMemo(
|
|
61
|
+
() => Array(totalQuantity).fill(0),
|
|
62
|
+
[totalQuantity]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
ref={ref}
|
|
68
|
+
data-fs-carousel-bullets
|
|
69
|
+
data-testid={testId}
|
|
70
|
+
role="tablist"
|
|
71
|
+
{...otherProps}
|
|
72
|
+
>
|
|
73
|
+
{bulletIndexes.map((_, idx) => {
|
|
74
|
+
const isActive = activeBullet === idx
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Button
|
|
78
|
+
key={`${id}-${idx}`}
|
|
79
|
+
role="tab"
|
|
80
|
+
tabIndex={-1}
|
|
81
|
+
data-fs-carousel-bullet
|
|
82
|
+
testId={`${testId}-bullet`}
|
|
83
|
+
onClick={(e) => onClick(e, idx)}
|
|
84
|
+
aria-label={ariaLabelGenerator(id, idx, isActive)}
|
|
85
|
+
aria-controls={ariaControlsGenerator?.(idx)}
|
|
86
|
+
aria-selected={isActive}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
export default CarouselBullets
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CSSProperties, HTMLAttributes, PropsWithChildren } from 'react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { SliderState, useSlideVisibility } 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
|
+
id,
|
|
15
|
+
index,
|
|
16
|
+
state,
|
|
17
|
+
children,
|
|
18
|
+
totalItems,
|
|
19
|
+
infiniteMode,
|
|
20
|
+
isScrollCarousel,
|
|
21
|
+
}: PropsWithChildren<CarouselItemProps>) {
|
|
22
|
+
const { isItemVisible, shouldRenderItem } = useSlideVisibility({
|
|
23
|
+
totalItems,
|
|
24
|
+
currentSlide: state.currentItem,
|
|
25
|
+
itemsPerPage: state.itemsPerPage,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const style =
|
|
29
|
+
((!isScrollCarousel && { width: '100%' }) as CSSProperties) ||
|
|
30
|
+
((isScrollCarousel && {
|
|
31
|
+
maxWidth: '60%',
|
|
32
|
+
display: 'inline-block',
|
|
33
|
+
}) as CSSProperties)
|
|
34
|
+
|
|
35
|
+
const shouldDisplayItem =
|
|
36
|
+
isScrollCarousel || shouldRenderItem(index - Number(infiniteMode))
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<li
|
|
40
|
+
style={style}
|
|
41
|
+
data-fs-carousel-item
|
|
42
|
+
aria-roledescription="slide"
|
|
43
|
+
id={`${id}-carousel-item-${index}`}
|
|
44
|
+
data-fs-carousel-item-visible={
|
|
45
|
+
isItemVisible(index - Number(infiniteMode)) || undefined
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
{shouldDisplayItem ? children : null}
|
|
49
|
+
</li>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
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'
|
|
@@ -26,6 +26,7 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
|
26
26
|
children,
|
|
27
27
|
testId = 'fs-icon-button',
|
|
28
28
|
'aria-label': ariaLabel,
|
|
29
|
+
size = 'regular',
|
|
29
30
|
variant,
|
|
30
31
|
...otherProps
|
|
31
32
|
},
|
|
@@ -40,6 +41,7 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
|
40
41
|
icon={icon}
|
|
41
42
|
aria-label={ariaLabel}
|
|
42
43
|
testId={testId}
|
|
44
|
+
size={size}
|
|
43
45
|
{...otherProps}
|
|
44
46
|
>
|
|
45
47
|
{children}
|
|
@@ -40,9 +40,11 @@ function LinkButton({
|
|
|
40
40
|
data-testid={testId}
|
|
41
41
|
{...otherProps}
|
|
42
42
|
>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
<div data-fs-button-wrapper>
|
|
44
|
+
{React.isValidElement(icon) && iconPosition === 'left' && <span data-fs-button-icon>{icon}</span>}
|
|
45
|
+
{children && <span>{children}</span>}
|
|
46
|
+
{React.isValidElement(icon) && iconPosition === 'right' && <span data-fs-button-icon>{icon}</span>}
|
|
47
|
+
</div>
|
|
46
48
|
</a>
|
|
47
49
|
)
|
|
48
50
|
}
|
|
@@ -94,6 +94,7 @@ const QuantitySelector = ({
|
|
|
94
94
|
disabled={isLeftDisabled || disabled}
|
|
95
95
|
onClick={decrease}
|
|
96
96
|
testId={`${testId}-left-button`}
|
|
97
|
+
size="small"
|
|
97
98
|
/>
|
|
98
99
|
<Input
|
|
99
100
|
data-quantity-selector-input
|
|
@@ -111,6 +112,7 @@ const QuantitySelector = ({
|
|
|
111
112
|
icon={<Icon name="Plus" width={16} height={16} weight="bold" />}
|
|
112
113
|
onClick={increase}
|
|
113
114
|
testId={`${testId}-right-button`}
|
|
115
|
+
size="small"
|
|
114
116
|
/>
|
|
115
117
|
</div>
|
|
116
118
|
)
|
|
@@ -20,7 +20,7 @@ const RegionBar = forwardRef<HTMLDivElement, RegionBarProps>(function RegionBar(
|
|
|
20
20
|
) {
|
|
21
21
|
return (
|
|
22
22
|
<div ref={ref} data-fs-region-bar {...otherProps}>
|
|
23
|
-
<Button onClick={onButtonClick}>
|
|
23
|
+
<Button onClick={onButtonClick} iconPosition="right" icon={<Icon name="CaretRight" />}>
|
|
24
24
|
<Icon name="MapPin" />
|
|
25
25
|
{postalCode ? (
|
|
26
26
|
<>
|
|
@@ -30,7 +30,6 @@ const RegionBar = forwardRef<HTMLDivElement, RegionBarProps>(function RegionBar(
|
|
|
30
30
|
) : (
|
|
31
31
|
<span data-fs-region-bar-message>Set your location</span>
|
|
32
32
|
)}
|
|
33
|
-
<Icon name="CaretRight" />
|
|
34
33
|
</Button>
|
|
35
34
|
</div>
|
|
36
35
|
)
|
|
@@ -88,10 +88,10 @@ const SearchInputField = forwardRef<
|
|
|
88
88
|
{...otherProps}
|
|
89
89
|
/>
|
|
90
90
|
<IconButton
|
|
91
|
-
data-fs-icon-button="null"
|
|
92
91
|
type="submit"
|
|
93
92
|
aria-label="Submit Search"
|
|
94
93
|
icon={buttonIcon ?? <Icon name="MagnifyingGlass" />}
|
|
94
|
+
size="small"
|
|
95
95
|
{...buttonProps}
|
|
96
96
|
/>
|
|
97
97
|
</form>
|
|
@@ -95,7 +95,6 @@ function ImageGallerySelector({
|
|
|
95
95
|
data-fs-image-gallery-selector-control-button
|
|
96
96
|
aria-label={navigationButtonLeftAriaLabel}
|
|
97
97
|
icon={<Icon name="ArrowLeft" />}
|
|
98
|
-
size="small"
|
|
99
98
|
onClick={() =>
|
|
100
99
|
moveScroll(elementsRef.current, -SCROLL_MARGIN_VALUE)
|
|
101
100
|
}
|
|
@@ -105,7 +104,7 @@ function ImageGallerySelector({
|
|
|
105
104
|
<div data-fs-image-gallery-selector-elements ref={elementsRef}>
|
|
106
105
|
{images.map((image, idx) => {
|
|
107
106
|
return (
|
|
108
|
-
<InView
|
|
107
|
+
<InView
|
|
109
108
|
key={idx}
|
|
110
109
|
onChange={(inView) => inViewChange(idx, inView)}>
|
|
111
110
|
<Button
|
|
@@ -134,7 +133,6 @@ function ImageGallerySelector({
|
|
|
134
133
|
data-fs-image-gallery-selector-control-button
|
|
135
134
|
aria-label={navigationButtonRightAriaLabel}
|
|
136
135
|
icon={<Icon name="ArrowLeft" />}
|
|
137
|
-
size="small"
|
|
138
136
|
onClick={() =>
|
|
139
137
|
moveScroll(elementsRef.current, +SCROLL_MARGIN_VALUE)
|
|
140
138
|
}
|