@startupjs-ui/carousel 0.1.3
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/CHANGELOG.md +20 -0
- package/README.mdx +544 -0
- package/index.cssx.styl +67 -0
- package/index.d.ts +41 -0
- package/index.tsx +622 -0
- package/package.json +21 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @startupjs-ui/carousel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
|
|
20
|
+
* **carousel:** refactor Carousel component ([03b066b](https://github.com/startupjs/startupjs-ui/commit/03b066b0343985cce5eb4e023f160b1f267d4110))
|
package/README.mdx
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { useRef, useState } from 'react'
|
|
2
|
+
import { Text } from 'react-native'
|
|
3
|
+
import Carousel, { _PropsJsonSchema as CarouselPropsJsonSchema } from './index'
|
|
4
|
+
import Div from '@startupjs-ui/div'
|
|
5
|
+
import Span from '@startupjs-ui/span'
|
|
6
|
+
import { Sandbox } from '@startupjs-ui/docs'
|
|
7
|
+
|
|
8
|
+
# Carousel
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import { Carousel } from 'startupjs-ui'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Simple example
|
|
15
|
+
|
|
16
|
+
All you need to use is to transfer children in Carousel
|
|
17
|
+
Such an example of a carousel, used on instagram or vk for stories, children have no clear boundaries, such a carousel should be adaptive for all extensions and devices
|
|
18
|
+
|
|
19
|
+
```jsx example noscroll
|
|
20
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
21
|
+
const caseStyle = {
|
|
22
|
+
height: 180,
|
|
23
|
+
width: 200,
|
|
24
|
+
paddingLeft: 4,
|
|
25
|
+
paddingRight: 4
|
|
26
|
+
}
|
|
27
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Carousel style={carouselStyle}>
|
|
31
|
+
<Div style={caseStyle}>
|
|
32
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
33
|
+
</Div>
|
|
34
|
+
<Div style={caseStyle}>
|
|
35
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
36
|
+
</Div>
|
|
37
|
+
<Div style={caseStyle}>
|
|
38
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
39
|
+
</Div>
|
|
40
|
+
<Div style={caseStyle}>
|
|
41
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
42
|
+
</Div>
|
|
43
|
+
</Carousel>
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You can also set different widths for children, the component knows how to handle this case
|
|
48
|
+
|
|
49
|
+
```jsx example noscroll
|
|
50
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
51
|
+
const caseStyle = {
|
|
52
|
+
height: 180,
|
|
53
|
+
width: 200,
|
|
54
|
+
paddingLeft: 4,
|
|
55
|
+
paddingRight: 4
|
|
56
|
+
}
|
|
57
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Carousel style={carouselStyle}>
|
|
61
|
+
<Div style={{ ...caseStyle, width: 120 }}>
|
|
62
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
63
|
+
</Div>
|
|
64
|
+
<Div style={{ ...caseStyle, width: 200 }}>
|
|
65
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
66
|
+
</Div>
|
|
67
|
+
<Div style={{ ...caseStyle, width: 240 }}>
|
|
68
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
69
|
+
</Div>
|
|
70
|
+
<Div style={{ ...caseStyle, width: 150 }}>
|
|
71
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
72
|
+
</Div>
|
|
73
|
+
<Div style={{ ...caseStyle, width: 240 }}>
|
|
74
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
75
|
+
</Div>
|
|
76
|
+
<Div style={{ ...caseStyle, width: 150 }}>
|
|
77
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
78
|
+
</Div>
|
|
79
|
+
</Carousel>
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Adaptability
|
|
84
|
+
|
|
85
|
+
If you need clear boundaries
|
|
86
|
+
There is a property for this - isResponsive (by default - false)
|
|
87
|
+
In conjunction with this property, each children needs to minWidth and maxWidth
|
|
88
|
+
The component itself will calculate how many blocks need to be displayed for a specific resolution
|
|
89
|
+
For example, when minWidth 100 and maxWidth 500, on the desktop extension there will be 2 blocks, on mobile 1
|
|
90
|
+
|
|
91
|
+
```jsx example noscroll
|
|
92
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
93
|
+
const caseStyle = {
|
|
94
|
+
height: 180,
|
|
95
|
+
paddingLeft: 4,
|
|
96
|
+
paddingRight: 4,
|
|
97
|
+
minWidth: 100,
|
|
98
|
+
maxWidth: 500
|
|
99
|
+
}
|
|
100
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Carousel style={carouselStyle} isResponsive={true}>
|
|
104
|
+
<Div style={caseStyle}>
|
|
105
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
106
|
+
</Div>
|
|
107
|
+
<Div style={caseStyle}>
|
|
108
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
109
|
+
</Div>
|
|
110
|
+
<Div style={caseStyle}>
|
|
111
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
112
|
+
</Div>
|
|
113
|
+
<Div style={caseStyle}>
|
|
114
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
115
|
+
</Div>
|
|
116
|
+
</Carousel>
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
To use 1 visible block, children minWidth to be 100%
|
|
121
|
+
|
|
122
|
+
```jsx example noscroll
|
|
123
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
124
|
+
const caseStyle = {
|
|
125
|
+
height: 180,
|
|
126
|
+
paddingLeft: 4,
|
|
127
|
+
paddingRight: 4,
|
|
128
|
+
minWidth: '100%'
|
|
129
|
+
}
|
|
130
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Carousel style={carouselStyle} isResponsive={true}>
|
|
134
|
+
<Div style={caseStyle}>
|
|
135
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
136
|
+
</Div>
|
|
137
|
+
<Div style={caseStyle}>
|
|
138
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
139
|
+
</Div>
|
|
140
|
+
<Div style={caseStyle}>
|
|
141
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
142
|
+
</Div>
|
|
143
|
+
<Div style={caseStyle}>
|
|
144
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
145
|
+
</Div>
|
|
146
|
+
</Carousel>
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Infinite scrolling
|
|
151
|
+
|
|
152
|
+
Set by isEndless property (default is false)
|
|
153
|
+
Without isResponsive property, infinite scrolling works only on arrow clicks
|
|
154
|
+
|
|
155
|
+
```jsx example noscroll
|
|
156
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
157
|
+
const caseStyle = {
|
|
158
|
+
height: 180,
|
|
159
|
+
paddingLeft: 4,
|
|
160
|
+
paddingRight: 4,
|
|
161
|
+
minWidth: 100,
|
|
162
|
+
maxWidth: 500
|
|
163
|
+
}
|
|
164
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<Carousel style={carouselStyle} isEndless={true}>
|
|
168
|
+
<Div style={{ ...caseStyle, width: 400 }}>
|
|
169
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
170
|
+
</Div>
|
|
171
|
+
<Div style={{ ...caseStyle, width: 260 }}>
|
|
172
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
173
|
+
</Div>
|
|
174
|
+
<Div style={{ ...caseStyle, width: 320 }}>
|
|
175
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
176
|
+
</Div>
|
|
177
|
+
<Div style={{ ...caseStyle, width: 200 }}>
|
|
178
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
179
|
+
</Div>
|
|
180
|
+
<Div style={{ ...caseStyle, width: 140 }}>
|
|
181
|
+
<Div style={{ ...itemStyle, backgroundColor: 'green' }} />
|
|
182
|
+
</Div>
|
|
183
|
+
<Div style={{ ...caseStyle, width: 180 }}>
|
|
184
|
+
<Div style={{ ...itemStyle, backgroundColor: 'black' }} />
|
|
185
|
+
</Div>
|
|
186
|
+
</Carousel>
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
With isResponsive property, works also through swipe
|
|
191
|
+
|
|
192
|
+
```jsx example noscroll
|
|
193
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
194
|
+
const caseStyle = {
|
|
195
|
+
height: 180,
|
|
196
|
+
paddingLeft: 4,
|
|
197
|
+
paddingRight: 4,
|
|
198
|
+
minWidth: 100,
|
|
199
|
+
maxWidth: 500
|
|
200
|
+
}
|
|
201
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<Carousel
|
|
205
|
+
style={carouselStyle}
|
|
206
|
+
isResponsive={true}
|
|
207
|
+
isEndless={true}
|
|
208
|
+
hasDots={true}
|
|
209
|
+
>
|
|
210
|
+
<Div style={caseStyle}>
|
|
211
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
212
|
+
</Div>
|
|
213
|
+
<Div style={caseStyle}>
|
|
214
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
215
|
+
</Div>
|
|
216
|
+
<Div style={caseStyle}>
|
|
217
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
218
|
+
</Div>
|
|
219
|
+
<Div style={caseStyle}>
|
|
220
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
221
|
+
</Div>
|
|
222
|
+
</Carousel>
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Swipe
|
|
227
|
+
|
|
228
|
+
The ability to scroll with the mouse or finger is set by isSwipe
|
|
229
|
+
The default is true, and you can always turn it off:
|
|
230
|
+
|
|
231
|
+
```jsx example noscroll
|
|
232
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
233
|
+
const caseStyle = { height: 180, paddingLeft: 4, paddingRight: 4 }
|
|
234
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<Carousel style={carouselStyle} isSwipe={false}>
|
|
238
|
+
<Div style={{ ...caseStyle, width: 400 }}>
|
|
239
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
240
|
+
</Div>
|
|
241
|
+
<Div style={{ ...caseStyle, width: 260 }}>
|
|
242
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
243
|
+
</Div>
|
|
244
|
+
<Div style={{ ...caseStyle, width: 320 }}>
|
|
245
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
246
|
+
</Div>
|
|
247
|
+
<Div style={{ ...caseStyle, width: 200 }}>
|
|
248
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
249
|
+
</Div>
|
|
250
|
+
</Carousel>
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Display arrows and dots
|
|
255
|
+
|
|
256
|
+
Set by properties hasArrows (default is true) and hasDots (default is false)
|
|
257
|
+
Without isResponsive - hasDots always false
|
|
258
|
+
|
|
259
|
+
```jsx example noscroll
|
|
260
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
261
|
+
const caseStyle = { minWidth: '100%', height: 180, paddingLeft: 4, paddingRight: 4 }
|
|
262
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<Carousel
|
|
266
|
+
style={carouselStyle}
|
|
267
|
+
hasArrows={false}
|
|
268
|
+
hasDots={true}
|
|
269
|
+
isResponsive={true}
|
|
270
|
+
>
|
|
271
|
+
<Div style={caseStyle}>
|
|
272
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
273
|
+
</Div>
|
|
274
|
+
<Div style={caseStyle}>
|
|
275
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
276
|
+
</Div>
|
|
277
|
+
<Div style={caseStyle}>
|
|
278
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
279
|
+
</Div>
|
|
280
|
+
<Div style={caseStyle}>
|
|
281
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
282
|
+
</Div>
|
|
283
|
+
</Carousel>
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Auto scroll
|
|
288
|
+
|
|
289
|
+
Set by isLoop property (default is false)
|
|
290
|
+
|
|
291
|
+
```jsx example noscroll
|
|
292
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
293
|
+
const caseStyle = { minWidth: '100%', height: 180, paddingLeft: 4, paddingRight: 4 }
|
|
294
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<Carousel
|
|
298
|
+
style={carouselStyle}
|
|
299
|
+
hasDots={true}
|
|
300
|
+
isLoop={true}
|
|
301
|
+
isEndless={true}
|
|
302
|
+
isResponsive={true}
|
|
303
|
+
>
|
|
304
|
+
<Div style={caseStyle}>
|
|
305
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
306
|
+
</Div>
|
|
307
|
+
<Div style={caseStyle}>
|
|
308
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
309
|
+
</Div>
|
|
310
|
+
<Div style={caseStyle}>
|
|
311
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
312
|
+
</Div>
|
|
313
|
+
<Div style={caseStyle}>
|
|
314
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
315
|
+
</Div>
|
|
316
|
+
</Carousel>
|
|
317
|
+
)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Start value
|
|
321
|
+
|
|
322
|
+
Set by startIndex (Number) property
|
|
323
|
+
|
|
324
|
+
```jsx example noscroll
|
|
325
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
326
|
+
const caseStyle = { minWidth: '100%', height: 180, paddingLeft: 4, paddingRight: 4 }
|
|
327
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<Carousel
|
|
331
|
+
style={carouselStyle}
|
|
332
|
+
hasDots={true}
|
|
333
|
+
isResponsive={true}
|
|
334
|
+
startIndex={2}
|
|
335
|
+
>
|
|
336
|
+
<Div style={caseStyle}>
|
|
337
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
338
|
+
</Div>
|
|
339
|
+
<Div style={caseStyle}>
|
|
340
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
341
|
+
</Div>
|
|
342
|
+
<Div style={caseStyle}>
|
|
343
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
344
|
+
</Div>
|
|
345
|
+
<Div style={caseStyle}>
|
|
346
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
347
|
+
</Div>
|
|
348
|
+
<Div style={caseStyle}>
|
|
349
|
+
<Div style={{ ...itemStyle, backgroundColor: 'green' }} />
|
|
350
|
+
</Div>
|
|
351
|
+
<Div style={caseStyle}>
|
|
352
|
+
<Div style={{ ...itemStyle, backgroundColor: 'black' }} />
|
|
353
|
+
</Div>
|
|
354
|
+
</Carousel>
|
|
355
|
+
)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Styling arrows
|
|
359
|
+
|
|
360
|
+
Set with arrowBackStyle and arrowNextStyle
|
|
361
|
+
|
|
362
|
+
```jsx example noscroll
|
|
363
|
+
const carouselStyle = { width: '80%', alignSelf: 'center' }
|
|
364
|
+
const caseStyle = { width: 200, height: 180, paddingLeft: 4, paddingRight: 4 }
|
|
365
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
366
|
+
const arrowBackStyle = {
|
|
367
|
+
left: -50,
|
|
368
|
+
backgroundColor: 'transparent',
|
|
369
|
+
border: '1px solid #333333',
|
|
370
|
+
color: '#333333'
|
|
371
|
+
}
|
|
372
|
+
const arrowNextStyle = {
|
|
373
|
+
right: -50,
|
|
374
|
+
backgroundColor: 'transparent',
|
|
375
|
+
border: '1px solid #333333',
|
|
376
|
+
color: '#333333'
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<Carousel
|
|
381
|
+
style={carouselStyle}
|
|
382
|
+
arrowBackStyle={arrowBackStyle}
|
|
383
|
+
arrowNextStyle={arrowNextStyle}
|
|
384
|
+
>
|
|
385
|
+
<Div style={caseStyle}>
|
|
386
|
+
<Div style={{ ...itemStyle, backgroundColor: 'blue' }} />
|
|
387
|
+
</Div>
|
|
388
|
+
<Div style={caseStyle}>
|
|
389
|
+
<Div style={{ ...itemStyle, backgroundColor: 'red' }} />
|
|
390
|
+
</Div>
|
|
391
|
+
<Div style={caseStyle}>
|
|
392
|
+
<Div style={{ ...itemStyle, backgroundColor: 'yellow' }} />
|
|
393
|
+
</Div>
|
|
394
|
+
<Div style={caseStyle}>
|
|
395
|
+
<Div style={{ ...itemStyle, backgroundColor: 'gray' }} />
|
|
396
|
+
</Div>
|
|
397
|
+
<Div style={caseStyle}>
|
|
398
|
+
<Div style={{ ...itemStyle, backgroundColor: 'green' }} />
|
|
399
|
+
</Div>
|
|
400
|
+
<Div style={caseStyle}>
|
|
401
|
+
<Div style={{ ...itemStyle, backgroundColor: 'black' }} />
|
|
402
|
+
</Div>
|
|
403
|
+
</Carousel>
|
|
404
|
+
)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
If you need full customization of arrows or points, can be transferred ref, it will contain functions to switch: toBack, toNext, toIndex
|
|
408
|
+
|
|
409
|
+
```jsx example noscroll
|
|
410
|
+
const ref = useRef()
|
|
411
|
+
const [slide, setSlide] = useState(0)
|
|
412
|
+
const carouselStyle = { width: '100%', marginLeft: 4, marginRight: 4 }
|
|
413
|
+
const caseStyle = {
|
|
414
|
+
minWidth: '100%',
|
|
415
|
+
width: 200,
|
|
416
|
+
height: 180,
|
|
417
|
+
paddingLeft: 4,
|
|
418
|
+
paddingRight: 4
|
|
419
|
+
}
|
|
420
|
+
const itemStyle = { width: '100%', height: '100%' }
|
|
421
|
+
const arrowStyle = {
|
|
422
|
+
backgroundColor: '#333',
|
|
423
|
+
alignItems: 'center',
|
|
424
|
+
justifyContent: 'center',
|
|
425
|
+
height: 20,
|
|
426
|
+
width: 20,
|
|
427
|
+
paddingBottom: 2
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const dotStyle = {
|
|
431
|
+
backgroundColor: '#333',
|
|
432
|
+
width: 10,
|
|
433
|
+
height: 10,
|
|
434
|
+
border: '2px solid #333',
|
|
435
|
+
marginLeft: 2,
|
|
436
|
+
marginRight: 2,
|
|
437
|
+
borderRadius: 30
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const data = [
|
|
441
|
+
{ backgroundColor: 'blue' },
|
|
442
|
+
{ backgroundColor: 'red' },
|
|
443
|
+
{ backgroundColor: 'yellow' },
|
|
444
|
+
{ backgroundColor: 'gray' },
|
|
445
|
+
{ backgroundColor: 'green' },
|
|
446
|
+
{ backgroundColor: 'black' }
|
|
447
|
+
]
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<Div style={{ overflow: 'hidden' }}>
|
|
451
|
+
<Div row style={{ marginBottom: 4 }}>
|
|
452
|
+
<Div style={arrowStyle} onPress={()=> ref.current.toBack()}>
|
|
453
|
+
<Text style={{ color: '#fff' }}>{'<'}</Text>
|
|
454
|
+
</Div>
|
|
455
|
+
<Div style={arrowStyle} onPress={()=> ref.current.toNext()}>
|
|
456
|
+
<Text style={{ color: '#fff' }}>{'>'}</Text>
|
|
457
|
+
</Div>
|
|
458
|
+
</Div>
|
|
459
|
+
<Carousel
|
|
460
|
+
ref={ref}
|
|
461
|
+
style={{ marginLeft: -4, marginRight: -4 }}
|
|
462
|
+
hasArrows={false}
|
|
463
|
+
isResponsive={true}
|
|
464
|
+
onChange={v => setSlide(v)}
|
|
465
|
+
>
|
|
466
|
+
{
|
|
467
|
+
data.map((item, index) => (
|
|
468
|
+
<Div key={index} style={caseStyle}>
|
|
469
|
+
<Div style={{ ...itemStyle, ...item }} />
|
|
470
|
+
</Div>
|
|
471
|
+
))
|
|
472
|
+
}
|
|
473
|
+
</Carousel>
|
|
474
|
+
<Div row style={{ alignSelf: 'center', marginTop: 8 }}>
|
|
475
|
+
{
|
|
476
|
+
data.map((item, index) => (
|
|
477
|
+
<Div
|
|
478
|
+
key={index}
|
|
479
|
+
style={{
|
|
480
|
+
...dotStyle,
|
|
481
|
+
...(slide === index ? { backgroundColor: '#fff' } : {})
|
|
482
|
+
}}
|
|
483
|
+
onPress={()=> ref.current.toIndex(index)}
|
|
484
|
+
/>
|
|
485
|
+
))
|
|
486
|
+
}
|
|
487
|
+
</Div>
|
|
488
|
+
</Div>
|
|
489
|
+
)
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Vertical mode
|
|
493
|
+
|
|
494
|
+
```jsx example
|
|
495
|
+
const carouselStyle = { marginLeft: 4, marginRight: 4 }
|
|
496
|
+
const caseStyle = {
|
|
497
|
+
height: 56,
|
|
498
|
+
width: 72,
|
|
499
|
+
paddingLeft: 4,
|
|
500
|
+
paddingRight: 4
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const itemStyle = {
|
|
504
|
+
width: '100%',
|
|
505
|
+
height: '100%',
|
|
506
|
+
justifyContent: 'center',
|
|
507
|
+
alignItems: 'center'
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const textStyle = { color: '#fff' }
|
|
511
|
+
|
|
512
|
+
const data = new Array(24).fill(true).map(Number)
|
|
513
|
+
|
|
514
|
+
return (
|
|
515
|
+
<Carousel
|
|
516
|
+
style={{ height: 240, width: 72 }}
|
|
517
|
+
variant='vertical'
|
|
518
|
+
isEndless
|
|
519
|
+
hasArrows
|
|
520
|
+
>
|
|
521
|
+
{
|
|
522
|
+
data.map((item, index) => (
|
|
523
|
+
<Div key={index} style={caseStyle}>
|
|
524
|
+
<Div style={itemStyle}>
|
|
525
|
+
<Span style={textStyle}>{index + 1}</Span>
|
|
526
|
+
</Div>
|
|
527
|
+
</Div>
|
|
528
|
+
))
|
|
529
|
+
}
|
|
530
|
+
</Carousel>
|
|
531
|
+
)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## Sandbox
|
|
535
|
+
|
|
536
|
+
<Sandbox
|
|
537
|
+
Component={Carousel}
|
|
538
|
+
propsJsonSchema={CarouselPropsJsonSchema}
|
|
539
|
+
props={{
|
|
540
|
+
isResponsive: true,
|
|
541
|
+
hasDots: true,
|
|
542
|
+
style: { height: 180, width: '100%' }
|
|
543
|
+
}}
|
|
544
|
+
/>
|
package/index.cssx.styl
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.wrapper
|
|
2
|
+
flex-grow 1
|
|
3
|
+
flex-shrink 1
|
|
4
|
+
flex-direction column
|
|
5
|
+
|
|
6
|
+
.carousel
|
|
7
|
+
flex-grow 1
|
|
8
|
+
flex-shrink 1
|
|
9
|
+
flex-direction row
|
|
10
|
+
|
|
11
|
+
&.vertical
|
|
12
|
+
flex-direction column
|
|
13
|
+
|
|
14
|
+
.arrow
|
|
15
|
+
background-color var(--Carousel-arrowWrapperBg)
|
|
16
|
+
align-self center
|
|
17
|
+
cursor pointer
|
|
18
|
+
position absolute
|
|
19
|
+
user-select none
|
|
20
|
+
z-index 999
|
|
21
|
+
border-radius 1u
|
|
22
|
+
padding .5u
|
|
23
|
+
|
|
24
|
+
&.vertical
|
|
25
|
+
position relative
|
|
26
|
+
|
|
27
|
+
.arrowNext
|
|
28
|
+
right 3u
|
|
29
|
+
|
|
30
|
+
&.vertical
|
|
31
|
+
right 0
|
|
32
|
+
|
|
33
|
+
.arrowBack
|
|
34
|
+
left 3u
|
|
35
|
+
|
|
36
|
+
&.vertical
|
|
37
|
+
left 0
|
|
38
|
+
|
|
39
|
+
.root
|
|
40
|
+
overflow hidden
|
|
41
|
+
flex-direction row
|
|
42
|
+
flex-grow 1
|
|
43
|
+
flex-shrink 1
|
|
44
|
+
|
|
45
|
+
&.vertical
|
|
46
|
+
flex-direction column
|
|
47
|
+
|
|
48
|
+
.case
|
|
49
|
+
flex-direction row
|
|
50
|
+
|
|
51
|
+
&.vertical
|
|
52
|
+
flex-direction column
|
|
53
|
+
|
|
54
|
+
.dots
|
|
55
|
+
align-self center
|
|
56
|
+
margin 2u 0
|
|
57
|
+
|
|
58
|
+
.dot
|
|
59
|
+
height 1u
|
|
60
|
+
width 2u
|
|
61
|
+
background-color var(--color-bg-main-subtle)
|
|
62
|
+
border-radius .3u
|
|
63
|
+
margin 0 .4u
|
|
64
|
+
cursor pointer
|
|
65
|
+
|
|
66
|
+
.dotActive
|
|
67
|
+
background-color var(--color-bg-primary)
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
|
|
3
|
+
|
|
4
|
+
import React, { type Ref } from 'react';
|
|
5
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
6
|
+
import './index.cssx.styl';
|
|
7
|
+
export declare const _PropsJsonSchema: {};
|
|
8
|
+
export interface CarouselProps {
|
|
9
|
+
/** Custom styles applied to the outer wrapper */
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
/** Styles for the back arrow container and icon */
|
|
12
|
+
arrowBackStyle?: StyleProp<any>;
|
|
13
|
+
/** Styles for the next arrow container and icon */
|
|
14
|
+
arrowNextStyle?: StyleProp<any>;
|
|
15
|
+
/** Initial active slide index @default 0 */
|
|
16
|
+
startIndex?: number;
|
|
17
|
+
/** Direction of movement @default 'horizontal' */
|
|
18
|
+
variant?: 'horizontal' | 'vertical';
|
|
19
|
+
/** Enable swipe gestures @default true */
|
|
20
|
+
isSwipe?: boolean;
|
|
21
|
+
/** Auto-scroll slides in a loop @default false */
|
|
22
|
+
isLoop?: boolean;
|
|
23
|
+
/** Enable endless carousel mode @default false */
|
|
24
|
+
isEndless?: boolean;
|
|
25
|
+
/** Adjust slide sizes based on available space @default false */
|
|
26
|
+
isResponsive?: boolean;
|
|
27
|
+
/** Show navigation arrows @default true */
|
|
28
|
+
hasArrows?: boolean;
|
|
29
|
+
/** Show navigation dots (responsive only) @default false */
|
|
30
|
+
hasDots?: boolean;
|
|
31
|
+
/** Animation duration in ms @default 300 */
|
|
32
|
+
duration?: number;
|
|
33
|
+
/** Slides to render inside the carousel */
|
|
34
|
+
children?: any[];
|
|
35
|
+
/** Callback fired when active slide changes */
|
|
36
|
+
onChange?: (index: number) => void;
|
|
37
|
+
/** Ref exposing active child and navigation helpers */
|
|
38
|
+
ref?: Ref<any>;
|
|
39
|
+
}
|
|
40
|
+
declare const _default: React.ComponentType<CarouselProps>;
|
|
41
|
+
export default _default;
|
package/index.tsx
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useImperativeHandle,
|
|
3
|
+
useEffect,
|
|
4
|
+
useState,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type Ref
|
|
9
|
+
} from 'react'
|
|
10
|
+
import {
|
|
11
|
+
Animated,
|
|
12
|
+
PanResponder,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
View,
|
|
15
|
+
type StyleProp,
|
|
16
|
+
type ViewStyle
|
|
17
|
+
} from 'react-native'
|
|
18
|
+
import { pug, observer } from 'startupjs'
|
|
19
|
+
import { themed } from '@startupjs-ui/core'
|
|
20
|
+
import Div from '@startupjs-ui/div'
|
|
21
|
+
import Icon from '@startupjs-ui/icon'
|
|
22
|
+
import { faAngleLeft } from '@fortawesome/free-solid-svg-icons/faAngleLeft'
|
|
23
|
+
import { faAngleRight } from '@fortawesome/free-solid-svg-icons/faAngleRight'
|
|
24
|
+
import { faAngleUp } from '@fortawesome/free-solid-svg-icons/faAngleUp'
|
|
25
|
+
import { faAngleDown } from '@fortawesome/free-solid-svg-icons/faAngleDown'
|
|
26
|
+
import './index.cssx.styl'
|
|
27
|
+
|
|
28
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
29
|
+
export const _PropsJsonSchema = {/* CarouselProps */}
|
|
30
|
+
|
|
31
|
+
export interface CarouselProps {
|
|
32
|
+
/** Custom styles applied to the outer wrapper */
|
|
33
|
+
style?: StyleProp<ViewStyle>
|
|
34
|
+
/** Styles for the back arrow container and icon */
|
|
35
|
+
arrowBackStyle?: StyleProp<any>
|
|
36
|
+
/** Styles for the next arrow container and icon */
|
|
37
|
+
arrowNextStyle?: StyleProp<any>
|
|
38
|
+
/** Initial active slide index @default 0 */
|
|
39
|
+
startIndex?: number
|
|
40
|
+
/** Direction of movement @default 'horizontal' */
|
|
41
|
+
variant?: 'horizontal' | 'vertical'
|
|
42
|
+
/** Enable swipe gestures @default true */
|
|
43
|
+
isSwipe?: boolean
|
|
44
|
+
/** Auto-scroll slides in a loop @default false */
|
|
45
|
+
isLoop?: boolean
|
|
46
|
+
/** Enable endless carousel mode @default false */
|
|
47
|
+
isEndless?: boolean
|
|
48
|
+
/** Adjust slide sizes based on available space @default false */
|
|
49
|
+
isResponsive?: boolean
|
|
50
|
+
/** Show navigation arrows @default true */
|
|
51
|
+
hasArrows?: boolean
|
|
52
|
+
/** Show navigation dots (responsive only) @default false */
|
|
53
|
+
hasDots?: boolean
|
|
54
|
+
/** Animation duration in ms @default 300 */
|
|
55
|
+
duration?: number
|
|
56
|
+
/** Slides to render inside the carousel */
|
|
57
|
+
children?: any[]
|
|
58
|
+
/** Callback fired when active slide changes */
|
|
59
|
+
onChange?: (index: number) => void
|
|
60
|
+
/** Ref exposing active child and navigation helpers */
|
|
61
|
+
ref?: Ref<any>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Carousel ({
|
|
65
|
+
style,
|
|
66
|
+
arrowBackStyle = {},
|
|
67
|
+
arrowNextStyle = {},
|
|
68
|
+
startIndex = 0,
|
|
69
|
+
variant = 'horizontal',
|
|
70
|
+
isSwipe = true,
|
|
71
|
+
isLoop = false,
|
|
72
|
+
isEndless = false,
|
|
73
|
+
isResponsive = false,
|
|
74
|
+
hasArrows = true,
|
|
75
|
+
hasDots = false,
|
|
76
|
+
duration = 300,
|
|
77
|
+
children = [],
|
|
78
|
+
onChange,
|
|
79
|
+
ref
|
|
80
|
+
}: CarouselProps): ReactNode {
|
|
81
|
+
arrowBackStyle = StyleSheet.flatten(arrowBackStyle)
|
|
82
|
+
arrowNextStyle = StyleSheet.flatten(arrowNextStyle)
|
|
83
|
+
const backArrowColor = arrowBackStyle?.color || '#eeeeee'
|
|
84
|
+
const nextArrowColor = arrowNextStyle?.color || '#eeeeee'
|
|
85
|
+
|
|
86
|
+
const refRoot = useRef<any>(null)
|
|
87
|
+
const refCase = useRef<any>(null)
|
|
88
|
+
const refTimeout = useRef<any>(null)
|
|
89
|
+
const childrenInfo = useRef<any[]>([])
|
|
90
|
+
const coardName = variant === 'horizontal' ? 'x' : 'y'
|
|
91
|
+
const sideName = variant === 'horizontal' ? 'width' : 'height'
|
|
92
|
+
|
|
93
|
+
const childrenRefs = useMemo(() => {
|
|
94
|
+
const _refs: Record<number, any> = {}
|
|
95
|
+
children.forEach((c, i) => {
|
|
96
|
+
_refs[i] = React.createRef()
|
|
97
|
+
})
|
|
98
|
+
return _refs
|
|
99
|
+
}, [children])
|
|
100
|
+
|
|
101
|
+
const validChildren = useMemo(() => {
|
|
102
|
+
return getValidChildren({ children, isEndless, isResponsive, childrenRefs })
|
|
103
|
+
}, [children, childrenRefs, isEndless, isResponsive])
|
|
104
|
+
|
|
105
|
+
const [activeIndex, setActiveIndex] = useState(startIndex)
|
|
106
|
+
const [rootInfo, setRootInfo] = useState<Record<string, number>>({})
|
|
107
|
+
const [caseInfo, setCaseInfo] = useState<Record<string, number>>({})
|
|
108
|
+
const [animateTranslate, setAnimateTranslate] = useState(new Animated.Value(0))
|
|
109
|
+
const [caseStyle, setCaseStyle] = useState<Record<string, any>>({})
|
|
110
|
+
|
|
111
|
+
const [startDrag, setStartDrag] = useState<any>(null)
|
|
112
|
+
const [endDrag, setEndDrag] = useState<any>(null)
|
|
113
|
+
const [isRender, setIsRender] = useState(false)
|
|
114
|
+
const [isAnimate, setIsAnimate] = useState(false)
|
|
115
|
+
|
|
116
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
117
|
+
useImperativeHandle(ref, () => {
|
|
118
|
+
return new Proxy(refRoot.current, {
|
|
119
|
+
get (target: any, prop: any) {
|
|
120
|
+
const actualChildIndex = activeIndex % children.length
|
|
121
|
+
const activeRef = childrenRefs[actualChildIndex].current
|
|
122
|
+
if (prop === 'getChildByIndex') return getChildByIndex
|
|
123
|
+
if (prop === 'activeChild') return activeRef
|
|
124
|
+
if (prop === 'element') return refRoot.current
|
|
125
|
+
if (prop === 'toBack') return onBack
|
|
126
|
+
if (prop === 'toNext') return onNext
|
|
127
|
+
if (prop === 'toIndex') return toIndex
|
|
128
|
+
if (target?.[prop]) return target[prop]
|
|
129
|
+
return activeRef?.[prop]
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}, [
|
|
133
|
+
activeIndex,
|
|
134
|
+
rootInfo,
|
|
135
|
+
caseInfo,
|
|
136
|
+
isAnimate,
|
|
137
|
+
isRender
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (isRender && isLoop && !isAnimate) {
|
|
143
|
+
refTimeout.current = setTimeout(onNext, 3000)
|
|
144
|
+
} else {
|
|
145
|
+
clearTimeout(refTimeout.current)
|
|
146
|
+
}
|
|
147
|
+
}, [isRender, isLoop, isAnimate, activeIndex])
|
|
148
|
+
|
|
149
|
+
function getChildByIndex (index: number) {
|
|
150
|
+
return childrenRefs[index].current
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function onLayoutChild ({ nativeEvent }: any, index: number) {
|
|
154
|
+
childrenInfo.current[index] = {
|
|
155
|
+
width: Math.round(nativeEvent.layout.width),
|
|
156
|
+
height: Math.round(nativeEvent.layout.height),
|
|
157
|
+
x: Math.round(nativeEvent.layout.x),
|
|
158
|
+
y: Math.round(nativeEvent.layout.y)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (childrenInfo.current.length === validChildren.length && !isRender) {
|
|
162
|
+
const isChildNotExists = childrenInfo.current.includes(undefined)
|
|
163
|
+
if (isChildNotExists) return
|
|
164
|
+
|
|
165
|
+
if (isEndless && isResponsive) {
|
|
166
|
+
setActiveIndex(children.length + startIndex)
|
|
167
|
+
|
|
168
|
+
const coard = childrenInfo.current[children.length + startIndex] &&
|
|
169
|
+
-childrenInfo.current[children.length + startIndex][coardName]
|
|
170
|
+
animateTranslate.setValue(coard || 0)
|
|
171
|
+
} else {
|
|
172
|
+
setActiveIndex(startIndex)
|
|
173
|
+
|
|
174
|
+
const coard = childrenInfo.current[startIndex] &&
|
|
175
|
+
-childrenInfo.current[startIndex][coardName]
|
|
176
|
+
animateTranslate.setValue(coard || 0)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
setIsRender(true)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function toIndex (index: number) {
|
|
184
|
+
setIsAnimate(true)
|
|
185
|
+
|
|
186
|
+
if (isEndless && isResponsive) {
|
|
187
|
+
index = index + children.length
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let activeElement: any = childrenInfo.current[index]
|
|
191
|
+
activeElement.index = index
|
|
192
|
+
|
|
193
|
+
let toValue = -activeElement[coardName]
|
|
194
|
+
if (caseInfo[sideName] - activeElement[coardName] < rootInfo[sideName]) {
|
|
195
|
+
activeElement = getClosest({
|
|
196
|
+
childrenInfo: childrenInfo.current,
|
|
197
|
+
newPosition: caseInfo[sideName] - rootInfo[sideName],
|
|
198
|
+
coardName,
|
|
199
|
+
sideName
|
|
200
|
+
})
|
|
201
|
+
toValue = rootInfo[sideName] - caseInfo[sideName]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
setActiveIndex(activeElement.index)
|
|
205
|
+
onChange && onChange(activeElement.index % children.length)
|
|
206
|
+
Animated.timing(animateTranslate, { toValue, duration, useNativeDriver: false }).start(() => {
|
|
207
|
+
setIsAnimate(false)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function onBack () {
|
|
212
|
+
if ((activeIndex === 0 &&
|
|
213
|
+
(animateTranslate as any)._value === 0 &&
|
|
214
|
+
!isEndless) || isAnimate) return
|
|
215
|
+
|
|
216
|
+
setIsAnimate(true)
|
|
217
|
+
|
|
218
|
+
let activeElement: any = childrenInfo.current[activeIndex - 1]
|
|
219
|
+
if (isResponsive) {
|
|
220
|
+
activeElement.index = activeIndex - 1
|
|
221
|
+
} else {
|
|
222
|
+
const newPosition = childrenInfo.current[activeIndex][coardName] +
|
|
223
|
+
childrenInfo.current[activeIndex][sideName] - rootInfo[sideName]
|
|
224
|
+
|
|
225
|
+
activeElement = getClosest({
|
|
226
|
+
childrenInfo: childrenInfo.current,
|
|
227
|
+
coardName,
|
|
228
|
+
sideName,
|
|
229
|
+
newPosition
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let toValue = -activeElement[coardName]
|
|
234
|
+
|
|
235
|
+
if (isEndless && !isResponsive && (activeIndex === 0)) {
|
|
236
|
+
activeElement = getClosest({
|
|
237
|
+
childrenInfo: childrenInfo.current,
|
|
238
|
+
newPosition: caseInfo[sideName] - rootInfo[sideName],
|
|
239
|
+
coardName,
|
|
240
|
+
sideName
|
|
241
|
+
})
|
|
242
|
+
toValue = rootInfo[sideName] - caseInfo[sideName]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (isEndless && isResponsive && (activeIndex - 1 < children.length)) {
|
|
246
|
+
const index = activeElement.index * 2 + 1
|
|
247
|
+
setActiveIndex(index)
|
|
248
|
+
onChange && onChange(index % children.length)
|
|
249
|
+
} else {
|
|
250
|
+
setActiveIndex(activeElement.index)
|
|
251
|
+
onChange && onChange(activeElement.index % children.length)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
Animated.timing(animateTranslate, { toValue, duration, useNativeDriver: false }).start(() => {
|
|
255
|
+
if (isEndless && isResponsive && activeIndex - 1 < children.length) {
|
|
256
|
+
animateTranslate.setValue(-childrenInfo.current[activeElement.index * 2 + 1][coardName])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
setIsAnimate(false)
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function onNext () {
|
|
264
|
+
if (isAnimate) return
|
|
265
|
+
if (!childrenInfo.current[activeIndex + 1] && !isEndless) return
|
|
266
|
+
if ((caseInfo[sideName] - childrenInfo.current[activeIndex][coardName] <= rootInfo[sideName]) && !isEndless) {
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
setIsAnimate(true)
|
|
271
|
+
|
|
272
|
+
let activeElement: any = childrenInfo.current[activeIndex + 1]
|
|
273
|
+
if (isResponsive) {
|
|
274
|
+
activeElement.index = activeIndex + 1
|
|
275
|
+
} else {
|
|
276
|
+
if (childrenInfo.current[activeIndex][coardName] + childrenInfo.current[activeIndex][sideName] > rootInfo[sideName]) {
|
|
277
|
+
activeElement.index = activeIndex + 1
|
|
278
|
+
} else {
|
|
279
|
+
activeElement = getClosest({
|
|
280
|
+
childrenInfo: childrenInfo.current,
|
|
281
|
+
newPosition: childrenInfo.current[activeIndex][coardName] + rootInfo[sideName],
|
|
282
|
+
coardName,
|
|
283
|
+
sideName
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let toValue = -activeElement[coardName]
|
|
289
|
+
if (caseInfo[sideName] - activeElement[coardName] < rootInfo[sideName]) {
|
|
290
|
+
activeElement = getClosest({
|
|
291
|
+
childrenInfo: childrenInfo.current,
|
|
292
|
+
newPosition: caseInfo[sideName] - rootInfo[sideName],
|
|
293
|
+
coardName,
|
|
294
|
+
sideName
|
|
295
|
+
})
|
|
296
|
+
toValue = -(caseInfo[sideName] - rootInfo[sideName])
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (isEndless && !isResponsive && ((animateTranslate as any)._value === -(caseInfo[sideName] - rootInfo[sideName]))) {
|
|
300
|
+
activeElement = childrenInfo.current[0]
|
|
301
|
+
activeElement.index = 0
|
|
302
|
+
toValue = activeElement[coardName]
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (isEndless && isResponsive && activeIndex + 1 >= (children.length * 2)) {
|
|
306
|
+
setActiveIndex(children.length)
|
|
307
|
+
onChange && onChange(0)
|
|
308
|
+
} else {
|
|
309
|
+
setActiveIndex(activeElement.index)
|
|
310
|
+
onChange && onChange(activeElement.index % children.length)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
Animated.timing(animateTranslate, { toValue, duration, useNativeDriver: false }).start(() => {
|
|
314
|
+
if (isEndless && isResponsive && activeIndex + 1 >= (children.length * 2)) {
|
|
315
|
+
animateTranslate.setValue(-childrenInfo.current[children.length][coardName])
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
setIsAnimate(false)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
if (!endDrag) return
|
|
325
|
+
if (startDrag === endDrag) return
|
|
326
|
+
setIsAnimate(true)
|
|
327
|
+
|
|
328
|
+
let _endDrag = endDrag
|
|
329
|
+
if (-endDrag > caseInfo[sideName]) {
|
|
330
|
+
_endDrag = -caseInfo[sideName]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let activeElement: any = getClosest({
|
|
334
|
+
childrenInfo: childrenInfo.current,
|
|
335
|
+
newPosition: -_endDrag,
|
|
336
|
+
coardName,
|
|
337
|
+
sideName
|
|
338
|
+
})
|
|
339
|
+
const side = (startDrag > _endDrag) ? 'next' : 'back'
|
|
340
|
+
|
|
341
|
+
if (activeElement.index === activeIndex) {
|
|
342
|
+
if (side === 'next') {
|
|
343
|
+
if (!childrenInfo.current[activeIndex + 1]) {
|
|
344
|
+
activeElement = childrenInfo.current[activeIndex]
|
|
345
|
+
} else {
|
|
346
|
+
activeElement = childrenInfo.current[activeIndex + 1]
|
|
347
|
+
activeElement.index = activeIndex + 1
|
|
348
|
+
}
|
|
349
|
+
} else if (activeIndex - 1 >= 0) {
|
|
350
|
+
activeElement = childrenInfo.current[activeIndex - 1]
|
|
351
|
+
activeElement.index = activeIndex - 1
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let toValue = -activeElement[coardName]
|
|
356
|
+
if (caseInfo[sideName] - activeElement[coardName] < rootInfo[sideName]) {
|
|
357
|
+
activeElement = getClosest({
|
|
358
|
+
childrenInfo: childrenInfo.current,
|
|
359
|
+
newPosition: caseInfo[sideName] - rootInfo[sideName],
|
|
360
|
+
coardName,
|
|
361
|
+
sideName
|
|
362
|
+
})
|
|
363
|
+
toValue = -(caseInfo[sideName] - rootInfo[sideName])
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (side === 'back' && isEndless && isResponsive && (activeIndex - 1 < children.length)) {
|
|
367
|
+
const index = activeElement.index * 2 + 1
|
|
368
|
+
setActiveIndex(index)
|
|
369
|
+
onChange && onChange(index % children.length)
|
|
370
|
+
} else if (side === 'next' && isEndless && isResponsive && (activeIndex + 1 >= (children.length * 2))) {
|
|
371
|
+
setActiveIndex(children.length)
|
|
372
|
+
onChange && onChange(0)
|
|
373
|
+
} else {
|
|
374
|
+
setActiveIndex(activeElement.index)
|
|
375
|
+
onChange && onChange(activeElement.index % children.length)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
Animated.timing(animateTranslate, { toValue, duration, useNativeDriver: false }).start(() => {
|
|
379
|
+
if (side === 'back' && isEndless && isResponsive && activeIndex - 1 < children.length) {
|
|
380
|
+
animateTranslate.setValue(-childrenInfo.current[activeElement.index * 2 + 1][coardName])
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (side === 'next' && isEndless && isResponsive && (activeIndex + 1 >= (children.length * 2))) {
|
|
384
|
+
animateTranslate.setValue(-childrenInfo.current[children.length][coardName])
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
setIsAnimate(false)
|
|
388
|
+
})
|
|
389
|
+
}, [endDrag])
|
|
390
|
+
|
|
391
|
+
const panResponder = useMemo(() => {
|
|
392
|
+
if (isSwipe && !isAnimate) {
|
|
393
|
+
return PanResponder.create({
|
|
394
|
+
onStartShouldSetPanResponder: () => true,
|
|
395
|
+
onMoveShouldSetPanResponderCapture: (_, gestureState) => {
|
|
396
|
+
if (!(gestureState.dx === 0 && gestureState.dy === 0)) {
|
|
397
|
+
setCaseStyle({ pointerEvents: 'box-only' })
|
|
398
|
+
return true
|
|
399
|
+
} else {
|
|
400
|
+
return false
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
onPanResponderTerminationRequest: () => false,
|
|
404
|
+
onShouldBlockNativeResponder: () => false,
|
|
405
|
+
onPanResponderGrant: e => {
|
|
406
|
+
setAnimateTranslate(animateTranslate => {
|
|
407
|
+
setStartDrag((animateTranslate as any)._value)
|
|
408
|
+
return animateTranslate
|
|
409
|
+
})
|
|
410
|
+
},
|
|
411
|
+
onPanResponderMove: (e, gesture) => {
|
|
412
|
+
const delta = (gesture as any)['d' + coardName]
|
|
413
|
+
setStartDrag((startDrag: any) => {
|
|
414
|
+
animateTranslate.setValue(startDrag + delta)
|
|
415
|
+
return startDrag
|
|
416
|
+
})
|
|
417
|
+
},
|
|
418
|
+
onPanResponderEnd: () => {
|
|
419
|
+
setAnimateTranslate(animateTranslate => {
|
|
420
|
+
setEndDrag((animateTranslate as any)._value)
|
|
421
|
+
setCaseStyle({})
|
|
422
|
+
return animateTranslate
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return { panHandlers: {} }
|
|
429
|
+
}, [animateTranslate, coardName, isAnimate, isSwipe])
|
|
430
|
+
|
|
431
|
+
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
432
|
+
const renderChildren = React.Children.toArray(validChildren).map((child: any, index, arr) => {
|
|
433
|
+
const _style = StyleSheet.flatten([child.props.style])
|
|
434
|
+
|
|
435
|
+
const sideCapitalLetter = sideName[0].toUpperCase() + sideName.slice(1)
|
|
436
|
+
|
|
437
|
+
if (isResponsive) {
|
|
438
|
+
if ((!_style['min' + sideCapitalLetter] || !_style['max' + sideCapitalLetter]) && _style['min' + sideCapitalLetter] !== '100%') {
|
|
439
|
+
console.error('isResponsive need minWidth and maxWidth in children')
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!rootInfo[sideName]) return null
|
|
444
|
+
|
|
445
|
+
let step = 1
|
|
446
|
+
let _side
|
|
447
|
+
|
|
448
|
+
if (_style['min' + sideCapitalLetter] === '100%') {
|
|
449
|
+
_style['min' + sideCapitalLetter] = rootInfo[sideName]
|
|
450
|
+
_style['max' + sideCapitalLetter] = rootInfo[sideName]
|
|
451
|
+
_side = rootInfo[sideName]
|
|
452
|
+
step = -1
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
while (step !== -1) {
|
|
456
|
+
_side = rootInfo[sideName] / step
|
|
457
|
+
|
|
458
|
+
if (isNaN(_side)) break
|
|
459
|
+
if (step > 10) {
|
|
460
|
+
console.error('no valid minSide/maxSide')
|
|
461
|
+
break
|
|
462
|
+
}
|
|
463
|
+
if ((_side >= _style['min' + sideCapitalLetter]) && (_side <= _style['max' + sideCapitalLetter])) {
|
|
464
|
+
break
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
step++
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (_side && !isNaN(_side)) _style[sideName] = _side
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
child = React.cloneElement(child, { style: _style })
|
|
474
|
+
|
|
475
|
+
return pug`
|
|
476
|
+
View(
|
|
477
|
+
key=index
|
|
478
|
+
onLayout=e=> onLayoutChild(e, index)
|
|
479
|
+
)= child
|
|
480
|
+
`
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
function onLayoutRoot ({ nativeEvent }: any) {
|
|
484
|
+
setRootInfo({
|
|
485
|
+
width: Math.round(nativeEvent.layout.width),
|
|
486
|
+
height: Math.round(nativeEvent.layout.height)
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function onLayoutCase ({ nativeEvent }: any) {
|
|
491
|
+
setCaseInfo({
|
|
492
|
+
width: Math.round(nativeEvent.layout.width),
|
|
493
|
+
height: Math.round(nativeEvent.layout.height)
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const dots = getDotsArray({
|
|
498
|
+
children,
|
|
499
|
+
childrenInfo: childrenInfo.current,
|
|
500
|
+
coardName,
|
|
501
|
+
sideName,
|
|
502
|
+
rootInfo,
|
|
503
|
+
caseInfo,
|
|
504
|
+
isRender,
|
|
505
|
+
isEndless,
|
|
506
|
+
isResponsive
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
return pug`
|
|
510
|
+
Div.wrapper(style=[style, { opacity: isRender ? 1 : 0 }])
|
|
511
|
+
Div.carousel(styleName=variant)
|
|
512
|
+
if hasArrows
|
|
513
|
+
Div.arrow.arrowBack(
|
|
514
|
+
style=arrowBackStyle
|
|
515
|
+
styleName=variant
|
|
516
|
+
onPress=onBack
|
|
517
|
+
)
|
|
518
|
+
Icon.icon(
|
|
519
|
+
style={ color: backArrowColor }
|
|
520
|
+
icon=variant === 'vertical' ? faAngleUp : faAngleLeft
|
|
521
|
+
size='xxl'
|
|
522
|
+
)
|
|
523
|
+
View.root(
|
|
524
|
+
ref=refRoot
|
|
525
|
+
styleName=variant
|
|
526
|
+
onLayout=onLayoutRoot
|
|
527
|
+
)
|
|
528
|
+
Animated.View.case(
|
|
529
|
+
ref=refCase
|
|
530
|
+
...panResponder.panHandlers
|
|
531
|
+
style=[
|
|
532
|
+
{
|
|
533
|
+
transform: [{
|
|
534
|
+
['translate' + coardName.toUpperCase()]: animateTranslate
|
|
535
|
+
}]
|
|
536
|
+
},
|
|
537
|
+
caseStyle
|
|
538
|
+
]
|
|
539
|
+
styleName=variant
|
|
540
|
+
onLayout=onLayoutCase
|
|
541
|
+
)= renderChildren
|
|
542
|
+
if hasArrows
|
|
543
|
+
Div.arrow.arrowNext(
|
|
544
|
+
style=arrowNextStyle
|
|
545
|
+
styleName=variant
|
|
546
|
+
onPress=onNext
|
|
547
|
+
)
|
|
548
|
+
Icon.icon(
|
|
549
|
+
style={ color: nextArrowColor }
|
|
550
|
+
icon=variant === 'vertical' ? faAngleDown : faAngleRight
|
|
551
|
+
size='xxl'
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if hasDots && isResponsive
|
|
555
|
+
Div.dots(row)
|
|
556
|
+
each _, index in dots
|
|
557
|
+
Div.dot(
|
|
558
|
+
key=index
|
|
559
|
+
onPress=()=> toIndex(index)
|
|
560
|
+
styleName={ dotActive: activeIndex === (isEndless ? (index + children.length) : index) }
|
|
561
|
+
)
|
|
562
|
+
`
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function getClosest ({
|
|
566
|
+
childrenInfo,
|
|
567
|
+
newPosition,
|
|
568
|
+
coardName,
|
|
569
|
+
sideName
|
|
570
|
+
}: any) {
|
|
571
|
+
let activeElement: any = {}
|
|
572
|
+
|
|
573
|
+
for (let index = 0; index < childrenInfo.length; index++) {
|
|
574
|
+
const item = childrenInfo[index]
|
|
575
|
+
|
|
576
|
+
if (newPosition < item[coardName] && index === 0) {
|
|
577
|
+
activeElement = { ...item, index }
|
|
578
|
+
break
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (newPosition >= item[coardName] && newPosition <= (item[coardName] + item[sideName])) {
|
|
582
|
+
activeElement = { ...item, index }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return activeElement
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function getDotsArray ({
|
|
590
|
+
children,
|
|
591
|
+
childrenInfo,
|
|
592
|
+
coardName,
|
|
593
|
+
sideName,
|
|
594
|
+
rootInfo,
|
|
595
|
+
caseInfo,
|
|
596
|
+
isRender,
|
|
597
|
+
isEndless,
|
|
598
|
+
isResponsive
|
|
599
|
+
}: any) {
|
|
600
|
+
if (!isRender) return []
|
|
601
|
+
if (isEndless && isResponsive) return childrenInfo.slice(children.length * 2)
|
|
602
|
+
|
|
603
|
+
return childrenInfo.filter((item: any) => {
|
|
604
|
+
if (!item) return false
|
|
605
|
+
return Math.round(item[coardName]) <= (caseInfo[sideName] - rootInfo[sideName])
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function getValidChildren ({ children, isEndless, isResponsive, childrenRefs }: any) {
|
|
610
|
+
const childrenWithRefs = React.Children.toArray(children)
|
|
611
|
+
.map((child: any, index: number) => {
|
|
612
|
+
const childRef = childrenRefs[index]
|
|
613
|
+
return React.cloneElement(child, { ref: childRef })
|
|
614
|
+
})
|
|
615
|
+
if (isEndless && isResponsive) {
|
|
616
|
+
return [...children, ...childrenWithRefs, ...children]
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return childrenWithRefs
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export default observer(themed('Carousel', Carousel))
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@startupjs-ui/carousel",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"main": "index.tsx",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@startupjs-ui/core": "^0.1.3",
|
|
12
|
+
"@startupjs-ui/div": "^0.1.3",
|
|
13
|
+
"@startupjs-ui/icon": "^0.1.3"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"react": "*",
|
|
17
|
+
"react-native": "*",
|
|
18
|
+
"startupjs": "*"
|
|
19
|
+
},
|
|
20
|
+
"gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
|
|
21
|
+
}
|