@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.
- package/Basic/index.tsx +109 -0
- package/Basic/styles.css +49 -0
- package/BasicDynamic/index.tsx +130 -0
- package/BasicDynamic/styles.css +47 -0
- package/BasicUpdateSize/index.tsx +131 -0
- package/BasicUpdateSize/styles.css +47 -0
- package/Bounces/index.tsx +73 -0
- package/Bounces/styles.css +41 -0
- package/CHANGELOG.md +15 -0
- package/Common/Button/index.tsx +48 -0
- package/Common/Button/styles.css +41 -0
- package/Common/Card/index.tsx +21 -0
- package/Common/Card/styles.css +21 -0
- package/Common/Indicator/index.tsx +24 -0
- package/Common/Indicator/styles.css +21 -0
- package/Custom/index.tsx +87 -0
- package/Custom/styles.css +47 -0
- package/CustomScale/index.tsx +116 -0
- package/CustomScale/styles.css +41 -0
- package/CustomTinder/Card.tsx +24 -0
- package/CustomTinder/index.tsx +111 -0
- package/CustomTinder/styles.css +61 -0
- package/DifferentHeight/Button.tsx +33 -0
- package/DifferentHeight/index.tsx +95 -0
- package/DifferentHeight/styles.css +50 -0
- package/Direction/index.tsx +127 -0
- package/Direction/styles.css +41 -0
- package/EmptyDataBug/index.tsx +371 -0
- package/EmptyDataBug/styles.css +75 -0
- package/Indicator/index.tsx +67 -0
- package/Indicator/styles.css +41 -0
- package/Lazy/index.tsx +83 -0
- package/Lazy/styles.css +41 -0
- package/Loop/index.tsx +77 -0
- package/Loop/styles.css +41 -0
- package/RTL/Button.tsx +33 -0
- package/RTL/index.tsx +133 -0
- package/RTL/styles.css +41 -0
- package/RTLCustom/index.tsx +115 -0
- package/RTLCustom/styles.css +41 -0
- package/RTLLoop/Button.tsx +33 -0
- package/RTLLoop/index.tsx +116 -0
- package/RTLLoop/styles.css +41 -0
- package/RTLLoopLynxRTL/Button.tsx +33 -0
- package/RTLLoopLynxRTL/index.tsx +116 -0
- package/RTLLoopLynxRTL/styles.css +41 -0
- package/WithGap/Button.tsx +33 -0
- package/WithGap/index.tsx +118 -0
- package/WithGap/styles.css +47 -0
- package/dist/SwiperBasic.lynx.bundle +0 -0
- package/dist/SwiperBasic.web.bundle +1 -1
- package/dist/SwiperBasicDynamic.lynx.bundle +0 -0
- package/dist/SwiperBasicDynamic.web.bundle +1 -1
- package/dist/SwiperBasicUpdateSize.lynx.bundle +0 -0
- package/dist/SwiperBasicUpdateSize.web.bundle +1 -1
- package/dist/SwiperCustomTinder.lynx.bundle +0 -0
- package/dist/SwiperCustomTinder.web.bundle +1 -1
- package/dist/SwiperDifferentHeight.lynx.bundle +0 -0
- package/dist/SwiperDifferentHeight.web.bundle +1 -1
- package/dist/SwiperEmptyDataBug.lynx.bundle +0 -0
- package/dist/SwiperEmptyDataBug.web.bundle +1 -1
- 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
|