@widergy/mobile-ui 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/lib/components/Carousel/README.md +2 -0
- package/lib/components/Carousel/components/CarouselComponent/index.js +165 -29
- package/lib/components/Carousel/components/CarouselComponent/utils.js +2 -0
- package/lib/components/Carousel/propTypes.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [2.3.0](https://github.com/widergy/mobile-ui/compare/v2.2.0...v2.3.0) (2025-12-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* [UGGC-23] carousel autoplay ([#464](https://github.com/widergy/mobile-ui/issues/464)) ([5a81315](https://github.com/widergy/mobile-ui/commit/5a81315a79571e3b833e99b6a96f40ecf4684571))
|
|
7
|
+
|
|
1
8
|
# [2.2.0](https://github.com/widergy/mobile-ui/compare/v2.1.4...v2.2.0) (2025-11-28)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -11,6 +11,8 @@ For example, if `contentHeight` and `contentWidth` are not set, the carousel wil
|
|
|
11
11
|
|
|
12
12
|
| NAME | TYPE | REQUIRED | DESCRIPTION | DEFAULT |
|
|
13
13
|
| --- | --- | --- | --- | --- |
|
|
14
|
+
| autoPlay | bool | No | Whether the carousel should automatically scroll to the next item. | * `false` |
|
|
15
|
+
| autoPlayDelay | number | No | The delay between each auto scroll. | * `3000` |
|
|
14
16
|
| ...[ScrollView props]('https://facebook.github.io/react-native/docs/scrollview#props') | ScrollViewProps | No | Excluded `alwaysBounceHorizontal`, `alwaysBounceVertical`, `automaticallyAdjustContentInsets`, `pagingEnabled`, `scrollEventThrottle` & `scrollsToTop`. | `ScrollView.defaultProps` |
|
|
15
17
|
| horizontal | bool | No* | The orientation of the carousel. | * `true` |
|
|
16
18
|
| contentHeight | number | No* | The `height` of each carousel item. Also the height of the carousel itself, if the prop `horizontal` is `true`. | * `0` |
|
|
@@ -6,23 +6,30 @@ import styles from '../../styles';
|
|
|
6
6
|
import propTypes, { carouselForcedProps } from '../../propTypes';
|
|
7
7
|
import { checkSizeProp, CAROUSEL_MOUNT_DELAY, EXTRA_CAROUSEL_ITEMS } from '../../constants';
|
|
8
8
|
|
|
9
|
+
import { validateIndex } from './utils';
|
|
10
|
+
|
|
9
11
|
class CarouselComponent extends Component {
|
|
10
12
|
constructor(props) {
|
|
11
13
|
super(props);
|
|
12
14
|
this.ref = createRef();
|
|
13
|
-
const { initialIndex
|
|
15
|
+
const { initialIndex, spacing, visibleEdge } = props;
|
|
14
16
|
checkSizeProp(spacing, 'spacing');
|
|
15
17
|
checkSizeProp(visibleEdge, 'visibleEdge');
|
|
16
18
|
this.state = {
|
|
17
|
-
|
|
19
|
+
autoPlayTimeout: null,
|
|
20
|
+
autoScrollRestartTimeout: null,
|
|
21
|
+
currentIndex: validateIndex(initialIndex, 0),
|
|
22
|
+
isAutoScrolling: false,
|
|
18
23
|
opacity: 0,
|
|
19
24
|
opacityTimeout: null,
|
|
25
|
+
resumeAutoPlayTimeout: null,
|
|
20
26
|
timeout: null
|
|
21
27
|
};
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
componentDidMount() {
|
|
25
|
-
const { initialIndex } = this.props;
|
|
31
|
+
const { initialIndex, autoPlay } = this.props;
|
|
32
|
+
const validInitialIndex = validateIndex(initialIndex, 0);
|
|
26
33
|
/**
|
|
27
34
|
* These timeouts are required to scroll to the initial index on mount.
|
|
28
35
|
* They are assigned to state variables because state is asynchronous.
|
|
@@ -30,23 +37,29 @@ class CarouselComponent extends Component {
|
|
|
30
37
|
* We can then cancel the timeouts in case the component unmounts before they were called.
|
|
31
38
|
*/
|
|
32
39
|
const timeout = setTimeout(() => {
|
|
33
|
-
this.scrollToIndex(
|
|
40
|
+
this.scrollToIndex(validInitialIndex);
|
|
41
|
+
if (autoPlay) {
|
|
42
|
+
this.startAutoPlay();
|
|
43
|
+
}
|
|
34
44
|
}, CAROUSEL_MOUNT_DELAY);
|
|
45
|
+
|
|
35
46
|
const opacityTimeout = setTimeout(() => this.setState({ opacity: 1 }), CAROUSEL_MOUNT_DELAY + 10);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
timeout
|
|
39
|
-
});
|
|
47
|
+
|
|
48
|
+
this.setState({ opacityTimeout, timeout });
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
componentWillUnmount() {
|
|
43
|
-
const { opacityTimeout, timeout } =
|
|
52
|
+
const { opacityTimeout, timeout, autoPlayTimeout, resumeAutoPlayTimeout, autoScrollRestartTimeout } =
|
|
53
|
+
this.state;
|
|
44
54
|
/**
|
|
45
55
|
* Here we cancel the timeouts in case they were not executed before the component was unmounted.
|
|
46
56
|
* This avoids memory leaks (because if we don't do it, it will set state to an unmounted component).
|
|
47
57
|
*/
|
|
48
58
|
clearTimeout(opacityTimeout);
|
|
49
59
|
clearTimeout(timeout);
|
|
60
|
+
clearTimeout(autoPlayTimeout);
|
|
61
|
+
clearTimeout(resumeAutoPlayTimeout);
|
|
62
|
+
clearTimeout(autoScrollRestartTimeout);
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
changeCurrentIndex = index => {
|
|
@@ -58,36 +71,145 @@ class CarouselComponent extends Component {
|
|
|
58
71
|
|
|
59
72
|
changeInternalIndex = index => {
|
|
60
73
|
const { currentIndex } = this.state;
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
const { items, loop } = this.props;
|
|
75
|
+
const validIndex = validateIndex(index, currentIndex);
|
|
76
|
+
|
|
77
|
+
if (validIndex !== currentIndex) {
|
|
78
|
+
this.setState({ currentIndex: validIndex }, () => {
|
|
79
|
+
let indexToReport = validIndex;
|
|
80
|
+
if (loop) {
|
|
81
|
+
indexToReport = ((validIndex % items.length) + items.length) % items.length;
|
|
82
|
+
}
|
|
83
|
+
this.changeCurrentIndex(indexToReport);
|
|
84
|
+
});
|
|
63
85
|
}
|
|
64
86
|
};
|
|
65
87
|
|
|
88
|
+
startAutoPlay = () => {
|
|
89
|
+
const { autoPlay, autoPlayDelay, items } = this.props;
|
|
90
|
+
if (!autoPlay || items.length <= 1) return;
|
|
91
|
+
|
|
92
|
+
this.clearAutoPlay();
|
|
93
|
+
const autoPlayTimeout = setTimeout(this.autoPlayNext, autoPlayDelay);
|
|
94
|
+
this.setState({ autoPlayTimeout });
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
clearAutoPlay = () => {
|
|
98
|
+
const { autoPlayTimeout } = this.state;
|
|
99
|
+
if (autoPlayTimeout) {
|
|
100
|
+
clearTimeout(autoPlayTimeout);
|
|
101
|
+
this.setState({ autoPlayTimeout: null });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
autoPlayNext = () => {
|
|
106
|
+
const { currentIndex } = this.state;
|
|
107
|
+
const { items, loop } = this.props;
|
|
108
|
+
|
|
109
|
+
if (!items || items.length <= 1) return;
|
|
110
|
+
|
|
111
|
+
const nextIndex = loop
|
|
112
|
+
? (currentIndex + 1) % items.length
|
|
113
|
+
: currentIndex + 1 >= items.length
|
|
114
|
+
? 0
|
|
115
|
+
: currentIndex + 1;
|
|
116
|
+
|
|
117
|
+
this.setState({ isAutoScrolling: true });
|
|
118
|
+
this.scrollToIndex(nextIndex, true, false, true);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
pauseAutoPlay = () => {
|
|
122
|
+
const { autoPlay } = this.props;
|
|
123
|
+
if (autoPlay) {
|
|
124
|
+
this.clearAutoPlay();
|
|
125
|
+
const { resumeAutoPlayTimeout } = this.state;
|
|
126
|
+
if (resumeAutoPlayTimeout) {
|
|
127
|
+
clearTimeout(resumeAutoPlayTimeout);
|
|
128
|
+
this.setState({ resumeAutoPlayTimeout: null });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
resumeAutoPlay = (delay = 1000) => {
|
|
134
|
+
const { autoPlay } = this.props;
|
|
135
|
+
if (autoPlay) {
|
|
136
|
+
const { resumeAutoPlayTimeout } = this.state;
|
|
137
|
+
if (resumeAutoPlayTimeout) {
|
|
138
|
+
clearTimeout(resumeAutoPlayTimeout);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const newResumeTimeout = setTimeout(() => {
|
|
142
|
+
this.startAutoPlay();
|
|
143
|
+
this.setState({ resumeAutoPlayTimeout: null });
|
|
144
|
+
}, delay);
|
|
145
|
+
|
|
146
|
+
this.setState({ resumeAutoPlayTimeout: newResumeTimeout });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
handleTouchStart = () => this.pauseAutoPlay();
|
|
151
|
+
|
|
152
|
+
handleTouchEnd = () => this.resumeAutoPlay();
|
|
153
|
+
|
|
66
154
|
handleScroll = ({ nativeEvent: { contentOffset } }) => {
|
|
67
155
|
const { horizontal, contentHeight, contentWidth, items, loop, spacing, visibleEdge } = this.props;
|
|
156
|
+
const { isAutoScrolling } = this.state;
|
|
68
157
|
const offset = horizontal ? contentOffset.x : contentOffset.y;
|
|
69
158
|
const size = (horizontal ? contentWidth : contentHeight) - visibleEdge * 2 - spacing;
|
|
70
159
|
const index = Math.round(offset / size);
|
|
71
160
|
const last = items.length - 1;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
161
|
+
const isLast = index <= 1;
|
|
162
|
+
const isFirst = index >= last + 3;
|
|
163
|
+
|
|
164
|
+
if (!isAutoScrolling) {
|
|
165
|
+
this.pauseAutoPlay();
|
|
166
|
+
this.resumeAutoPlay(1500);
|
|
167
|
+
}
|
|
168
|
+
|
|
75
169
|
if (loop) {
|
|
76
|
-
if (isLast)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
170
|
+
if (isLast) {
|
|
171
|
+
this.scrollToIndex(last, false, false, true);
|
|
172
|
+
this.setState({ currentIndex: last }, () => {
|
|
173
|
+
this.changeCurrentIndex(last);
|
|
174
|
+
});
|
|
175
|
+
} else if (isFirst) {
|
|
176
|
+
this.scrollToIndex(0, false, false, true);
|
|
177
|
+
this.setState({ currentIndex: 0 }, () => {
|
|
178
|
+
this.changeCurrentIndex(0);
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
const actualIndex = index - EXTRA_CAROUSEL_ITEMS;
|
|
182
|
+
this.changeInternalIndex(actualIndex);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
this.changeInternalIndex(index);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (isAutoScrolling) {
|
|
189
|
+
this.setState({ isAutoScrolling: false });
|
|
190
|
+
|
|
191
|
+
const { autoScrollRestartTimeout } = this.state;
|
|
192
|
+
if (autoScrollRestartTimeout) {
|
|
193
|
+
clearTimeout(autoScrollRestartTimeout);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const newAutoScrollRestartTimeout = setTimeout(() => {
|
|
197
|
+
this.startAutoPlay();
|
|
198
|
+
this.setState({ autoScrollRestartTimeout: null });
|
|
199
|
+
}, 100);
|
|
200
|
+
this.setState({ autoScrollRestartTimeout: newAutoScrollRestartTimeout });
|
|
201
|
+
}
|
|
80
202
|
};
|
|
81
203
|
|
|
82
204
|
renderCarouselItem = (item, index, array) => {
|
|
83
205
|
const {
|
|
84
206
|
contentHeight,
|
|
85
207
|
contentWidth,
|
|
208
|
+
horizontal,
|
|
209
|
+
itemStyle,
|
|
86
210
|
keyExtractor,
|
|
87
211
|
keyPrefix,
|
|
88
|
-
horizontal,
|
|
89
212
|
loop,
|
|
90
|
-
itemStyle,
|
|
91
213
|
renderItem,
|
|
92
214
|
spacing,
|
|
93
215
|
visibleEdge
|
|
@@ -102,11 +224,19 @@ class CarouselComponent extends Component {
|
|
|
102
224
|
visibleEdge
|
|
103
225
|
};
|
|
104
226
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
227
|
+
let key;
|
|
228
|
+
if (loop) {
|
|
229
|
+
if (index < EXTRA_CAROUSEL_ITEMS) {
|
|
230
|
+
key = `${keyPrefix}loop-start-${index}`;
|
|
231
|
+
} else if (index >= array.length - EXTRA_CAROUSEL_ITEMS) {
|
|
232
|
+
key = `${keyPrefix}loop-end-${index}`;
|
|
233
|
+
} else {
|
|
234
|
+
const originalIndex = index - EXTRA_CAROUSEL_ITEMS;
|
|
235
|
+
key = `${keyPrefix}${keyExtractor(item, originalIndex, array)}`;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
key = `${keyPrefix}${keyExtractor(item, index, array)}`;
|
|
239
|
+
}
|
|
110
240
|
|
|
111
241
|
return (
|
|
112
242
|
<CarouselItem {...itemProps} key={key}>
|
|
@@ -124,19 +254,23 @@ class CarouselComponent extends Component {
|
|
|
124
254
|
return children.map(this.renderCarouselItem);
|
|
125
255
|
};
|
|
126
256
|
|
|
127
|
-
scrollToIndex = (index, animated = false, disableThreshold = false) => {
|
|
257
|
+
scrollToIndex = (index, animated = false, disableThreshold = false, skipOnChange = false) => {
|
|
128
258
|
if (this.ref && this.ref.current) {
|
|
129
|
-
this.changeCurrentIndex(index);
|
|
130
259
|
const { contentHeight, contentWidth, horizontal, items, loop, spacing, visibleEdge } = this.props;
|
|
131
260
|
if (index >= 0 - disableThreshold && index < items.length + disableThreshold) {
|
|
261
|
+
if (!skipOnChange) {
|
|
262
|
+
this.changeCurrentIndex(index);
|
|
263
|
+
}
|
|
132
264
|
const indexToScrollTo = loop ? index + EXTRA_CAROUSEL_ITEMS : index;
|
|
133
265
|
const scrollConfig = horizontal
|
|
134
266
|
? { x: indexToScrollTo * (contentWidth - visibleEdge * 2 - spacing) }
|
|
135
267
|
: { y: indexToScrollTo * (contentHeight - visibleEdge * 2 - spacing) };
|
|
136
268
|
this.ref.current.scrollTo({ ...scrollConfig, animated });
|
|
269
|
+
} else {
|
|
270
|
+
// eslint-disable-next-line no-console
|
|
271
|
+
console.warn('The index specified is not in the item list!', index, items.length);
|
|
137
272
|
}
|
|
138
|
-
|
|
139
|
-
} else console.warn('The index specified is not in the item list!');
|
|
273
|
+
}
|
|
140
274
|
};
|
|
141
275
|
|
|
142
276
|
// eslint-disable-next-line react/no-unused-class-component-methods
|
|
@@ -179,6 +313,8 @@ class CarouselComponent extends Component {
|
|
|
179
313
|
style={[styles.contentStyle, forcedStyles, { opacity }]}
|
|
180
314
|
horizontal={horizontal}
|
|
181
315
|
onScroll={this.handleScroll}
|
|
316
|
+
onTouchStart={this.handleTouchStart}
|
|
317
|
+
onTouchEnd={this.handleTouchEnd}
|
|
182
318
|
overScrollMode={loop ? 'never' : 'auto'}
|
|
183
319
|
pagingEnabled
|
|
184
320
|
ref={this.ref}
|
|
@@ -13,6 +13,8 @@ export const carouselForcedProps = {
|
|
|
13
13
|
|
|
14
14
|
export const carouselDefaultProps = {
|
|
15
15
|
...ScrollView.defaultProps,
|
|
16
|
+
autoPlay: false,
|
|
17
|
+
autoPlayDelay: 3000,
|
|
16
18
|
contentHeight: 0,
|
|
17
19
|
contentWidth: 0,
|
|
18
20
|
horizontal: true, // ScrollView prop
|
|
@@ -31,6 +33,8 @@ export const carouselDefaultProps = {
|
|
|
31
33
|
|
|
32
34
|
const propTypes = {
|
|
33
35
|
...ScrollView.propTypes,
|
|
36
|
+
autoPlay: bool,
|
|
37
|
+
autoPlayDelay: number,
|
|
34
38
|
contentHeight: number,
|
|
35
39
|
contentWidth: number,
|
|
36
40
|
initialIndex: number.isRequired,
|
package/package.json
CHANGED