@lynx-example/lynx-ui-swiper 0.0.1 → 0.0.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.
Files changed (62) hide show
  1. package/Basic/index.tsx +109 -0
  2. package/Basic/styles.css +49 -0
  3. package/BasicDynamic/index.tsx +130 -0
  4. package/BasicDynamic/styles.css +47 -0
  5. package/BasicUpdateSize/index.tsx +131 -0
  6. package/BasicUpdateSize/styles.css +47 -0
  7. package/Bounces/index.tsx +73 -0
  8. package/Bounces/styles.css +41 -0
  9. package/CHANGELOG.md +15 -0
  10. package/Common/Button/index.tsx +48 -0
  11. package/Common/Button/styles.css +41 -0
  12. package/Common/Card/index.tsx +21 -0
  13. package/Common/Card/styles.css +21 -0
  14. package/Common/Indicator/index.tsx +24 -0
  15. package/Common/Indicator/styles.css +21 -0
  16. package/Custom/index.tsx +87 -0
  17. package/Custom/styles.css +47 -0
  18. package/CustomScale/index.tsx +116 -0
  19. package/CustomScale/styles.css +41 -0
  20. package/CustomTinder/Card.tsx +24 -0
  21. package/CustomTinder/index.tsx +111 -0
  22. package/CustomTinder/styles.css +61 -0
  23. package/DifferentHeight/Button.tsx +33 -0
  24. package/DifferentHeight/index.tsx +95 -0
  25. package/DifferentHeight/styles.css +50 -0
  26. package/Direction/index.tsx +127 -0
  27. package/Direction/styles.css +41 -0
  28. package/EmptyDataBug/index.tsx +371 -0
  29. package/EmptyDataBug/styles.css +75 -0
  30. package/Indicator/index.tsx +67 -0
  31. package/Indicator/styles.css +41 -0
  32. package/Lazy/index.tsx +83 -0
  33. package/Lazy/styles.css +41 -0
  34. package/Loop/index.tsx +77 -0
  35. package/Loop/styles.css +41 -0
  36. package/RTL/Button.tsx +33 -0
  37. package/RTL/index.tsx +133 -0
  38. package/RTL/styles.css +41 -0
  39. package/RTLCustom/index.tsx +115 -0
  40. package/RTLCustom/styles.css +41 -0
  41. package/RTLLoop/Button.tsx +33 -0
  42. package/RTLLoop/index.tsx +116 -0
  43. package/RTLLoop/styles.css +41 -0
  44. package/RTLLoopLynxRTL/Button.tsx +33 -0
  45. package/RTLLoopLynxRTL/index.tsx +116 -0
  46. package/RTLLoopLynxRTL/styles.css +41 -0
  47. package/WithGap/Button.tsx +33 -0
  48. package/WithGap/index.tsx +118 -0
  49. package/WithGap/styles.css +47 -0
  50. package/dist/SwiperBasic.lynx.bundle +0 -0
  51. package/dist/SwiperBasic.web.bundle +1 -1
  52. package/dist/SwiperBasicDynamic.lynx.bundle +0 -0
  53. package/dist/SwiperBasicDynamic.web.bundle +1 -1
  54. package/dist/SwiperBasicUpdateSize.lynx.bundle +0 -0
  55. package/dist/SwiperBasicUpdateSize.web.bundle +1 -1
  56. package/dist/SwiperCustomTinder.lynx.bundle +0 -0
  57. package/dist/SwiperCustomTinder.web.bundle +1 -1
  58. package/dist/SwiperDifferentHeight.lynx.bundle +0 -0
  59. package/dist/SwiperDifferentHeight.web.bundle +1 -1
  60. package/dist/SwiperEmptyDataBug.lynx.bundle +0 -0
  61. package/dist/SwiperEmptyDataBug.web.bundle +1 -1
  62. package/package.json +5 -3
@@ -0,0 +1,95 @@
1
+ // Copyright 2026 The Lynx Authors. All rights reserved.
2
+ // Licensed under the Apache License Version 2.0 that can be found in the
3
+ // LICENSE file in the root directory of this source tree.
4
+
5
+ import { root, useRef, useState } from '@lynx-js/react'
6
+
7
+ import { Swiper, SwiperItem } from '@lynx-js/lynx-ui'
8
+ import type { SwiperRef } from '@lynx-js/lynx-ui'
9
+
10
+ import { Button } from '../Common/Button'
11
+ import { Card } from '../Common/Card'
12
+ import { Indicator } from '../Common/Indicator'
13
+
14
+ import './styles.css'
15
+
16
+ const itemArr: number[] = [1, 2, 3, 4, 5]
17
+
18
+ const itemWidths = [250, 350, 400]
19
+ const itemHeights = [200, 300, 350]
20
+ const alignArr: ['start', 'center', 'end'] = ['start', 'center', 'end']
21
+
22
+ const INITIAL_INDEX = 2
23
+
24
+ function SwiperEntry(): JSX.Element {
25
+ const [itemWidthsIndex, _setItemWidthsIndex] = useState<number>(0)
26
+ const [alignIndex, _setAlignIndex] = useState<number>(1)
27
+ const [currentIndex, setCurrentIndex] = useState<number>(INITIAL_INDEX)
28
+ const swiperRef = useRef<SwiperRef>(null)
29
+
30
+ return (
31
+ <view class='container lunaris-dark'>
32
+ <view class='top-area' />
33
+ <view class='content-area'>
34
+ <Swiper
35
+ ref={swiperRef}
36
+ data={itemArr}
37
+ itemWidth={itemWidths[itemWidthsIndex] ?? 0}
38
+ containerWidth={lynx.__globalProps.screenWidth - 32
39
+ || SystemInfo.pixelWidth / SystemInfo.pixelRatio - 32}
40
+ duration={500}
41
+ initialIndex={INITIAL_INDEX}
42
+ onChange={setCurrentIndex}
43
+ mode='normal'
44
+ modeConfig={{
45
+ align: alignArr[alignIndex],
46
+ spaceBetween: 16,
47
+ }}
48
+ autoPlay={false}
49
+ style={{
50
+ overflow: 'visible',
51
+ }}
52
+ trackStyle={{
53
+ alignItems: 'end',
54
+ }}
55
+ >
56
+ {({ index, realIndex }) => (
57
+ <SwiperItem index={index} key={realIndex} realIndex={realIndex}>
58
+ <Card
59
+ index={realIndex}
60
+ style={{
61
+ height: `${itemHeights[realIndex % itemHeights.length]}px`,
62
+ }}
63
+ />
64
+ </SwiperItem>
65
+ )}
66
+ </Swiper>
67
+ <Indicator
68
+ current={currentIndex}
69
+ count={itemArr.length}
70
+ />
71
+ </view>
72
+ <view class='operation'>
73
+ <Button
74
+ onClick={() => {
75
+ swiperRef.current?.swipePrev()
76
+ }}
77
+ className='expand'
78
+ text='SwipePrev'
79
+ />
80
+ <Button
81
+ onClick={() => {
82
+ swiperRef.current?.swipeNext()
83
+ }}
84
+ className='expand'
85
+ type='primary'
86
+ text='SwipeNext'
87
+ />
88
+ </view>
89
+ </view>
90
+ )
91
+ }
92
+
93
+ root.render(<SwiperEntry />)
94
+
95
+ export default SwiperEntry
@@ -0,0 +1,50 @@
1
+ @import "@lynx-js/luna-styles/index.css";
2
+
3
+ .container {
4
+ background-color: var(--canvas);
5
+ display: flex;
6
+ width: 100%;
7
+ height: 100%;
8
+ flex-direction: column;
9
+ justify-content: center;
10
+ }
11
+
12
+ .expand {
13
+ flex: 1;
14
+ }
15
+
16
+ .top-area {
17
+ height: 54px;
18
+ width: 100%;
19
+ }
20
+
21
+ .content-area {
22
+ display: flex;
23
+ background: linear-gradient(
24
+ 0deg,
25
+ var(--gradient-a),
26
+ var(--gradient-b),
27
+ );
28
+ padding-top: 96px;
29
+ flex-direction: column;
30
+ justify-content: center;
31
+ align-items: center;
32
+ row-gap: 24px;
33
+ padding-bottom: 12px;
34
+ }
35
+
36
+ .operation {
37
+ display: flex;
38
+ flex-direction: row;
39
+ gap: 16px;
40
+ padding: 32px 16px;
41
+ margin-bottom: 64px;
42
+ }
43
+
44
+ .sub-operation {
45
+ display: flex;
46
+ flex: 1;
47
+ flex-direction: column;
48
+ gap: 8px;
49
+ padding: 0 16px 72px 16px;
50
+ }
@@ -0,0 +1,127 @@
1
+ // Copyright 2026 The Lynx Authors. All rights reserved.
2
+ // Licensed under the Apache License Version 2.0 that can be found in the
3
+ // LICENSE file in the root directory of this source tree.
4
+
5
+ import { root } from '@lynx-js/react'
6
+
7
+ import { Swiper, SwiperItem } from '@lynx-js/lynx-ui'
8
+
9
+ import './styles.css'
10
+
11
+ const colorsArr: string[] = ['red', 'green', 'yellow', 'purple']
12
+
13
+ function SwiperEntry() {
14
+ return (
15
+ <scroll-view scroll-y style='height: 230px'>
16
+ <view>
17
+ <text>Test Swiper in scroll-view</text>
18
+ </view>
19
+ <Swiper
20
+ data={colorsArr}
21
+ itemWidth={250}
22
+ itemHeight={200}
23
+ loop={false}
24
+ duration={500}
25
+ initialIndex={0}
26
+ mode='normal'
27
+ modeConfig={{
28
+ align: 'start',
29
+ }}
30
+ bounceConfig={{
31
+ enable: true,
32
+ startBounceItemWidth: 0,
33
+ endBounceItem: (
34
+ <view style='display: linear; linear-orientation: vertical; height: 100%; width: 30px;'>
35
+ <text>12345</text>
36
+ </view>
37
+ ),
38
+ onEndBounceItemBounce: ({ type }) => {
39
+ console.log('onBounce result', type)
40
+ },
41
+ }}
42
+ experimentalHorizontalSwipeOnly={true}
43
+ >
44
+ {({ item, index, realIndex }) => (
45
+ <SwiperItem index={index} key={realIndex} realIndex={realIndex}>
46
+ <view
47
+ class='block-view'
48
+ style={{
49
+ width: '100%',
50
+ height: '100%',
51
+ backgroundColor: item,
52
+ display: 'flex',
53
+ justifyContent: 'center',
54
+ alignItems: 'center',
55
+ }}
56
+ >
57
+ <view
58
+ style={{
59
+ backgroundColor: 'white',
60
+ width: '4px',
61
+ height: '4px',
62
+ }}
63
+ >
64
+ </view>
65
+ </view>
66
+ <text class='image-text'>Number.{index}</text>
67
+ </SwiperItem>
68
+ )}
69
+ </Swiper>
70
+ <Swiper
71
+ data={colorsArr}
72
+ itemWidth={250}
73
+ itemHeight={200}
74
+ loop={false}
75
+ duration={500}
76
+ initialIndex={0}
77
+ mode='normal'
78
+ modeConfig={{
79
+ align: 'start',
80
+ }}
81
+ bounceConfig={{
82
+ enable: true,
83
+ startBounceItemWidth: 0,
84
+ endBounceItem: (
85
+ <view style='display: linear; linear-orientation: vertical; height: 100%; width: 30px;'>
86
+ <text>12345</text>
87
+ </view>
88
+ ),
89
+ onEndBounceItemBounce: ({ type }) => {
90
+ console.log('onBounce result', type)
91
+ },
92
+ }}
93
+ experimentalHorizontalSwipeOnly={true}
94
+ >
95
+ {({ item, index, realIndex }) => (
96
+ <SwiperItem index={index} key={realIndex} realIndex={realIndex}>
97
+ <view
98
+ class='block-view'
99
+ style={{
100
+ width: '100%',
101
+ height: '100%',
102
+ backgroundColor: item,
103
+ display: 'flex',
104
+ justifyContent: 'center',
105
+ alignItems: 'center',
106
+ }}
107
+ >
108
+ <view
109
+ style={{
110
+ backgroundColor: 'white',
111
+ width: '4px',
112
+ height: '4px',
113
+ }}
114
+ >
115
+ </view>
116
+ </view>
117
+ <text class='image-text'>Number.{index}</text>
118
+ </SwiperItem>
119
+ )}
120
+ </Swiper>
121
+ </scroll-view>
122
+ )
123
+ }
124
+
125
+ root.render(<SwiperEntry />)
126
+
127
+ export default SwiperEntry
@@ -0,0 +1,41 @@
1
+ .image {
2
+ width: 100%;
3
+ height: 100%;
4
+ }
5
+
6
+ .image-text {
7
+ position: absolute;
8
+ }
9
+
10
+ .container {
11
+ width: 100%;
12
+ height: 100%;
13
+ }
14
+
15
+ .title-container {
16
+ margin-top: 10px;
17
+ margin-bottom: 5px;
18
+ }
19
+
20
+ .title-text {
21
+ font-size: 16px;
22
+ }
23
+
24
+ .button {
25
+ border: 1px solid;
26
+ border-radius: 4px;
27
+ width: 150px;
28
+ background-color: red;
29
+ margin: 4px 8px;
30
+ padding: 4px 8px;
31
+ }
32
+
33
+ .button-text {
34
+ color: white;
35
+ }
36
+
37
+ .operation {
38
+ display: flex;
39
+ align-items: center;
40
+ flex-wrap: wrap;
41
+ }
@@ -0,0 +1,371 @@
1
+ // Copyright 2026 The Lynx Authors. All rights reserved.
2
+ // Licensed under the Apache License Version 2.0 that can be found in the
3
+ // LICENSE file in the root directory of this source tree.
4
+
5
+ /**
6
+ * This example reproduces the "calcBounceOffset: invalid offset" error.
7
+ *
8
+ * The bug occurs when:
9
+ * 1. Swiper is initialized with empty data (data=[])
10
+ * 2. autoPlay is enabled
11
+ * 3. Data is updated later
12
+ *
13
+ * Root cause: getCurrentIndex() returns NaN when dataCount is 0
14
+ * because (x % 0) = NaN in JavaScript
15
+ */
16
+
17
+ import { root, useEffect, useRef, useState } from '@lynx-js/react'
18
+
19
+ import { Swiper, SwiperItem } from '@lynx-js/lynx-ui'
20
+ import type { SwiperRef } from '@lynx-js/lynx-ui'
21
+
22
+ import { Button } from '../Common/Button'
23
+ import { Card } from '../Common/Card'
24
+
25
+ import './styles.css'
26
+
27
+ const DEFAULT_DATA: number[] = [1, 2, 3, 4, 5]
28
+
29
+ // Container padding (16px) + Section padding (16px) = 32px on each side = 64px total
30
+ const CONTAINER_PADDING = 64
31
+ const getContainerWidth = () => {
32
+ const screenWidth = lynx.__globalProps.screenWidth
33
+ ?? SystemInfo.pixelWidth / SystemInfo.pixelRatio
34
+ return screenWidth - CONTAINER_PADDING
35
+ }
36
+
37
+ /**
38
+ * Scenario 1: Empty data with autoPlay enabled
39
+ * REPRODUCES: Swipe the empty swiper before data loads (within 1 second)
40
+ * Result: Swiper shows wrong position (index -1, -2 loop placeholders instead of starting at 0)
41
+ */
42
+ function EmptyDataWithAutoPlay(): JSX.Element {
43
+ const [data, setData] = useState<number[]>([])
44
+ const [currentIndex, setCurrentIndex] = useState<number>(0)
45
+ const [onChangeLog, setOnChangeLog] = useState<string[]>([])
46
+ const swiperRef = useRef<SwiperRef>(null)
47
+
48
+ const handleChange = (index: number) => {
49
+ console.log('[Scenario 1] onChange:', index, 'isNaN:', isNaN(index))
50
+ setCurrentIndex(index)
51
+ setOnChangeLog(
52
+ prev => [
53
+ ...prev.slice(-4),
54
+ `onChange(${index}) ${isNaN(index) ? '⚠️ NaN!' : ''}`,
55
+ ],
56
+ )
57
+ }
58
+
59
+ const handleSwipeStop = (index: number) => {
60
+ console.log('[Scenario 1] onSwipeStop:', index, 'isNaN:', isNaN(index))
61
+ setOnChangeLog(
62
+ prev => [
63
+ ...prev.slice(-4),
64
+ `onSwipeStop(${index}) ${isNaN(index) ? '⚠️ NaN!' : ''}`,
65
+ ],
66
+ )
67
+ }
68
+
69
+ useEffect(() => {
70
+ // Simulate async data loading
71
+ // User should SWIPE before this timer fires to reproduce the bug
72
+ setTimeout(() => {
73
+ console.log('[Scenario 1] Loading data after 1 second...')
74
+ setData(DEFAULT_DATA)
75
+ }, 1000)
76
+ }, [])
77
+
78
+ return (
79
+ <view class='section'>
80
+ <text class='title'>
81
+ Scenario 1: Empty Data + Swipe Before Load (REPRODUCES)
82
+ </text>
83
+ <text class='description'>
84
+ ⚠️ To reproduce: SWIPE the empty area within 1 second (before data
85
+ loads). Result: Swiper shows wrong position (loop placeholders -1, -2
86
+ visible instead of index 0)
87
+ </text>
88
+ <Swiper
89
+ ref={swiperRef}
90
+ data={data}
91
+ containerWidth={getContainerWidth()}
92
+ itemWidth={300}
93
+ itemHeight={200}
94
+ duration={500}
95
+ initialIndex={0}
96
+ onChange={handleChange}
97
+ onSwipeStop={handleSwipeStop}
98
+ mode='normal'
99
+ loop={true}
100
+ autoPlay={true}
101
+ autoPlayInterval={2000}
102
+ modeConfig={{ align: 'center' }}
103
+ >
104
+ {({ index, realIndex }) => (
105
+ <SwiperItem index={index} key={realIndex} realIndex={realIndex}>
106
+ <Card index={realIndex} style={{ height: '200px' }} />
107
+ </SwiperItem>
108
+ )}
109
+ </Swiper>
110
+ <view class='info'>
111
+ <text>
112
+ Current Index: {currentIndex} {isNaN(currentIndex) ? '⚠️ NaN!' : ''}
113
+ </text>
114
+ <text>Data Length: {data.length}</text>
115
+ <text class='log-title'>Callback Log:</text>
116
+ {onChangeLog.map((log, i) => <text key={i} class='log-item'>{log}
117
+ </text>)}
118
+ </view>
119
+ </view>
120
+ )
121
+ }
122
+
123
+ /**
124
+ * Scenario 2: Empty data with swipe gesture
125
+ * REPRODUCES: SWIPE the empty area, then click "Load Data"
126
+ * Note: Just clicking SwipeNext button doesn't reproduce - must use swipe gesture
127
+ * Observe: Visual shows wrong position, but callbacks report correct values
128
+ */
129
+ function EmptyDataWithManualSwipe(): JSX.Element {
130
+ const [data, setData] = useState<number[]>([])
131
+ const [currentIndex, setCurrentIndex] = useState<number>(0)
132
+ const [onChangeLog, setOnChangeLog] = useState<string[]>([])
133
+ const [swipeStartCount, setSwipeStartCount] = useState<number>(0)
134
+ const [swipeStopCount, setSwipeStopCount] = useState<number>(0)
135
+ const swiperRef = useRef<SwiperRef>(null)
136
+
137
+ const handleChange = (index: number) => {
138
+ console.log('[Scenario 2] onChange:', index, 'isNaN:', isNaN(index))
139
+ setCurrentIndex(index)
140
+ setOnChangeLog(
141
+ prev => [
142
+ ...prev.slice(-4),
143
+ `onChange(${index}) ${isNaN(index) ? '⚠️ NaN!' : ''}`,
144
+ ],
145
+ )
146
+ }
147
+
148
+ const handleSwipeStart = () => {
149
+ console.log('[Scenario 2] onSwipeStart')
150
+ setSwipeStartCount(prev => prev + 1)
151
+ }
152
+
153
+ const handleSwipeStop = (index: number) => {
154
+ console.log('[Scenario 2] onSwipeStop:', index, 'isNaN:', isNaN(index))
155
+ setSwipeStopCount(prev => prev + 1)
156
+ setOnChangeLog(
157
+ prev => [
158
+ ...prev.slice(-4),
159
+ `onSwipeStop(${index}) ${isNaN(index) ? '⚠️ NaN!' : ''}`,
160
+ ],
161
+ )
162
+ }
163
+
164
+ return (
165
+ <view class='section'>
166
+ <text class='title'>
167
+ Scenario 2: Empty Data + Swipe Gesture (REPRODUCES)
168
+ </text>
169
+ <text class='description'>
170
+ ⚠️ Steps to reproduce: 1) SWIPE the empty area (not just button click)
171
+ 2) Click "Load Data". Note: Callbacks report correct values, but visual
172
+ is wrong.
173
+ </text>
174
+ <Swiper
175
+ ref={swiperRef}
176
+ data={data}
177
+ containerWidth={getContainerWidth()}
178
+ itemWidth={300}
179
+ itemHeight={200}
180
+ duration={500}
181
+ mode='normal'
182
+ loop={true}
183
+ autoPlay={false}
184
+ modeConfig={{ align: 'center' }}
185
+ onChange={handleChange}
186
+ onSwipeStart={handleSwipeStart}
187
+ onSwipeStop={handleSwipeStop}
188
+ >
189
+ {({ index, realIndex }) => (
190
+ <SwiperItem index={index} key={realIndex} realIndex={realIndex}>
191
+ <Card index={realIndex} style={{ height: '200px' }} />
192
+ </SwiperItem>
193
+ )}
194
+ </Swiper>
195
+ <view class='buttons'>
196
+ <Button
197
+ onClick={() => {
198
+ console.log('[Scenario 2] Calling swipeNext() on empty swiper')
199
+ swiperRef.current?.swipeNext()
200
+ }}
201
+ text='1. SwipeNext (empty)'
202
+ />
203
+ <Button
204
+ onClick={() => {
205
+ console.log('[Scenario 2] Loading data...')
206
+ setData(DEFAULT_DATA)
207
+ }}
208
+ text='2. Load Data'
209
+ type='primary'
210
+ />
211
+ <Button
212
+ onClick={() => {
213
+ setData([])
214
+ setOnChangeLog([])
215
+ setSwipeStartCount(0)
216
+ setSwipeStopCount(0)
217
+ }}
218
+ text='Reset'
219
+ />
220
+ </view>
221
+ <view class='info'>
222
+ <text>
223
+ Current Index: {currentIndex} {isNaN(currentIndex) ? '⚠️ NaN!' : ''}
224
+ </text>
225
+ <text>Data Length: {data.length}</text>
226
+ <text>SwipeStart Count: {swipeStartCount}</text>
227
+ <text>SwipeStop Count: {swipeStopCount}</text>
228
+ <text class='log-title'>Callback Log:</text>
229
+ {onChangeLog.map((log, i) => <text key={i} class='log-item'>{log}
230
+ </text>)}
231
+ </view>
232
+ </view>
233
+ )
234
+ }
235
+
236
+ /**
237
+ * Scenario 3: Data becomes empty during use
238
+ */
239
+ function DataBecomesEmpty(): JSX.Element {
240
+ const [data, setData] = useState<number[]>(DEFAULT_DATA)
241
+ const [_currentIndex, setCurrentIndex] = useState<number>(0)
242
+ const swiperRef = useRef<SwiperRef>(null)
243
+
244
+ return (
245
+ <view class='section'>
246
+ <text class='title'>
247
+ Scenario 3: Data Becomes Empty During Use (Does NOT reproduce)
248
+ </text>
249
+ <text class='description'>
250
+ Starts with data, then data becomes empty, then data returns.
251
+ </text>
252
+ <Swiper
253
+ ref={swiperRef}
254
+ data={data}
255
+ containerWidth={getContainerWidth()}
256
+ itemWidth={300}
257
+ itemHeight={200}
258
+ duration={500}
259
+ onChange={setCurrentIndex}
260
+ mode='normal'
261
+ loop={true}
262
+ autoPlay={true}
263
+ autoPlayInterval={3000}
264
+ modeConfig={{ align: 'center' }}
265
+ >
266
+ {({ index, realIndex }) => (
267
+ <SwiperItem index={index} key={realIndex} realIndex={realIndex}>
268
+ <Card index={realIndex} style={{ height: '200px' }} />
269
+ </SwiperItem>
270
+ )}
271
+ </Swiper>
272
+ <view class='buttons'>
273
+ <Button
274
+ onClick={() => {
275
+ console.log('Clearing data...')
276
+ setData([])
277
+ // After a short delay, restore data - this can trigger the bug
278
+ setTimeout(() => {
279
+ console.log('Restoring data...')
280
+ setData(DEFAULT_DATA)
281
+ }, 500)
282
+ }}
283
+ text='Clear then Restore'
284
+ type='primary'
285
+ />
286
+ </view>
287
+ <view class='info'>
288
+ <text>Data Length: {data.length}</text>
289
+ </view>
290
+ </view>
291
+ )
292
+ }
293
+
294
+ /**
295
+ * Scenario 4: Reset with corrupted prevIndexRef
296
+ */
297
+ function ResetWithCorruptedIndex(): JSX.Element {
298
+ const [data, setData] = useState<number[]>([])
299
+ const [swiperKey, setSwiperKey] = useState<string>('key-1')
300
+ const swiperRef = useRef<SwiperRef>(null)
301
+
302
+ useEffect(() => {
303
+ // First, let swiper initialize with empty data
304
+ // Then load data
305
+ setTimeout(() => {
306
+ setData(DEFAULT_DATA)
307
+ }, 500)
308
+ }, [])
309
+
310
+ return (
311
+ <view class='section'>
312
+ <text class='title'>
313
+ Scenario 4: Reset After Empty Init (Does NOT reproduce)
314
+ </text>
315
+ <text class='description'>
316
+ Swiper resets (via swiperKey change) after being initialized empty. The
317
+ prevIndexRef may contain NaN from the empty state.
318
+ </text>
319
+ <Swiper
320
+ ref={swiperRef}
321
+ swiperKey={swiperKey}
322
+ data={data}
323
+ containerWidth={getContainerWidth()}
324
+ itemWidth={300}
325
+ itemHeight={200}
326
+ duration={500}
327
+ mode='normal'
328
+ loop={true}
329
+ autoPlay={false}
330
+ modeConfig={{ align: 'center' }}
331
+ >
332
+ {({ index, realIndex }) => (
333
+ <SwiperItem index={index} key={realIndex} realIndex={realIndex}>
334
+ <Card index={realIndex} style={{ height: '200px' }} />
335
+ </SwiperItem>
336
+ )}
337
+ </Swiper>
338
+ <view class='buttons'>
339
+ <Button
340
+ onClick={() => {
341
+ // Change swiperKey to trigger reset
342
+ // This may use the corrupted prevIndexRef
343
+ setSwiperKey(`key-${Date.now()}`)
344
+ }}
345
+ text='Reset Swiper'
346
+ type='primary'
347
+ />
348
+ </view>
349
+ </view>
350
+ )
351
+ }
352
+
353
+ function EmptyDataBugExample(): JSX.Element {
354
+ return (
355
+ <scroll-view class='container' scroll-orientation='vertical'>
356
+ <text class='main-title'>Empty Data Bug Reproduction</text>
357
+ <text class='main-description'>
358
+ These examples demonstrate the "calcBounceOffset: invalid offset" error
359
+ that occurs when Swiper is initialized with empty data.
360
+ </text>
361
+ <EmptyDataWithAutoPlay />
362
+ <EmptyDataWithManualSwipe />
363
+ <DataBecomesEmpty />
364
+ <ResetWithCorruptedIndex />
365
+ </scroll-view>
366
+ )
367
+ }
368
+
369
+ root.render(<EmptyDataBugExample />)
370
+
371
+ export default EmptyDataBugExample