@widergy/mobile-ui 2.2.0 → 2.3.1

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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## [2.3.1](https://github.com/widergy/mobile-ui/compare/v2.3.0...v2.3.1) (2025-12-05)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * [UGOVAES-1094] multiplePicker fixes ([#468](https://github.com/widergy/mobile-ui/issues/468)) ([51a8fc9](https://github.com/widergy/mobile-ui/commit/51a8fc970303bf5f927a5bb592103f091b7181b1))
7
+
8
+ # [2.3.0](https://github.com/widergy/mobile-ui/compare/v2.2.0...v2.3.0) (2025-12-01)
9
+
10
+
11
+ ### Features
12
+
13
+ * [UGGC-23] carousel autoplay ([#464](https://github.com/widergy/mobile-ui/issues/464)) ([5a81315](https://github.com/widergy/mobile-ui/commit/5a81315a79571e3b833e99b6a96f40ecf4684571))
14
+
1
15
  # [2.2.0](https://github.com/widergy/mobile-ui/compare/v2.1.4...v2.2.0) (2025-11-28)
2
16
 
3
17
 
@@ -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: currentIndex, spacing, visibleEdge } = props;
15
+ const { initialIndex, spacing, visibleEdge } = props;
14
16
  checkSizeProp(spacing, 'spacing');
15
17
  checkSizeProp(visibleEdge, 'visibleEdge');
16
18
  this.state = {
17
- currentIndex,
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(initialIndex);
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
- this.setState({
37
- opacityTimeout,
38
- timeout
39
- });
47
+
48
+ this.setState({ opacityTimeout, timeout });
40
49
  }
41
50
 
42
51
  componentWillUnmount() {
43
- const { opacityTimeout, timeout } = this.state;
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
- if (index !== currentIndex) {
62
- this.setState({ currentIndex: index }, () => this.changeCurrentIndex(index));
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 boundary = spacing + visibleEdge;
73
- const isLast = index <= 1 && offset <= size + boundary;
74
- const isFirst = index >= last + 3 && offset >= (last + 3) * size - boundary;
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) this.scrollToIndex(last);
77
- else if (isFirst) this.scrollToIndex(0);
78
- else this.changeInternalIndex(index - EXTRA_CAROUSEL_ITEMS);
79
- } else this.changeInternalIndex(index);
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
- const key = loop
106
- ? index < EXTRA_CAROUSEL_ITEMS || index > array.length - 1
107
- ? `${keyPrefix}${keyExtractor(item, index, array)}`
108
- : `${keyPrefix}${index}`
109
- : `${keyPrefix}${keyExtractor(item, index, array)}`;
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
- // eslint-disable-next-line no-console
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}
@@ -0,0 +1,2 @@
1
+ export const validateIndex = (index, fallback = 0) =>
2
+ typeof index === 'number' && !Number.isNaN(index) ? index : fallback;
@@ -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,
@@ -31,13 +31,6 @@ class FilePicker extends Component {
31
31
  allowedPDFUploadSizes,
32
32
  pdfFormatError
33
33
  } = this.props;
34
-
35
- const getRealFileType = mimeType => {
36
- if (!mimeType || typeof mimeType !== 'string') return null;
37
- const parts = mimeType.split('/');
38
- return parts[1]?.toLowerCase() || null;
39
- };
40
-
41
34
  try {
42
35
  const normalizeTypes = types => {
43
36
  if (!types || types.length === 0) return DEFAULT_ALLOWED_TYPES;
@@ -78,8 +71,7 @@ class FilePicker extends Component {
78
71
  onMaxSizeError(document.size, maxFileByteSize);
79
72
  return;
80
73
  }
81
- const realFileType = getRealFileType(document.type);
82
- const file = !avoidRetrieveFile && (await retrieveFile(document.uri, realFileType));
74
+ const file = !avoidRetrieveFile && (await retrieveFile(document.uri, document.type));
83
75
 
84
76
  if (file && isPDF && !isEmpty(allowedPDFUploadSizes)) {
85
77
  const isWrongFormat = await pdfAspectRatioValidation(file, allowedPDFUploadSizes);
@@ -93,7 +85,7 @@ class FilePicker extends Component {
93
85
  }
94
86
  }
95
87
  if (onChange) {
96
- onChange(avoidRetrieveFile ? { document } : { file: blobToFile(file, realFileType) });
88
+ onChange(avoidRetrieveFile ? { document } : { file: blobToFile(file, document.type) });
97
89
  }
98
90
  this.setState({ fileName: document.name });
99
91
  } catch (err) {
@@ -57,14 +57,7 @@ const ImagePickerComponent = ({
57
57
  onError('No se pudo obtener el archivo');
58
58
  return;
59
59
  }
60
- const getRealFileType = mimeType => {
61
- if (!mimeType || typeof mimeType !== 'string') return null;
62
- const parts = mimeType.split('/');
63
- return parts[1]?.toLowerCase() || null;
64
- };
65
- const realFileType = getRealFileType(item.type);
66
-
67
- const file = !avoidRetrieveFile && (await retrieveFile(item.uri, realFileType));
60
+ const file = !avoidRetrieveFile && (await retrieveFile(item.uri, item.type));
68
61
  if (!avoidRetrieveFile && !file) {
69
62
  onError(response.errorCode);
70
63
  return;
@@ -79,12 +79,6 @@ const MultipleFilePicker = ({
79
79
 
80
80
  const remainingSlots = () => Math.max((maxFiles || 0) - uploadedFiles.length, 0);
81
81
 
82
- const getRealFileType = mimeType => {
83
- if (!mimeType || typeof mimeType !== 'string') return null;
84
- const parts = mimeType.split('/');
85
- return parts[1]?.toLowerCase() || null;
86
- };
87
-
88
82
  const handleAssets = async response => {
89
83
  if (!response || response.didCancel) {
90
84
  return;
@@ -95,17 +89,18 @@ const MultipleFilePicker = ({
95
89
  }
96
90
  closeDrawer();
97
91
  response.assets.forEach(async asset => {
98
- const realFileType = getRealFileType(asset.mimeType);
99
- const file = await retrieveFile(asset.uri, realFileType);
92
+ // We should use mimeType instead of type:
93
+ // asset.type -> "video" | "image".
94
+ const file = await retrieveFile(asset.uri, asset.mimeType);
100
95
  if (!file) {
101
96
  onError(response.errorCode || 'No se pudo obtener el archivo');
102
97
  return;
103
98
  }
104
- if (isFileTypeInvalid({ type: asset.type }, allowedTypes, fileTypeError, onError)) return;
99
+ if (isFileTypeInvalid({ type: asset.mimeType }, allowedTypes, fileTypeError, onError)) return;
105
100
  if (isFileSizeInvaid({ size: file.size }, maxFileByteSize, onMaxSizeError)) return;
106
101
  setNewFile({
107
102
  uploadFile: { name: file.data.name, size: file.data.size },
108
- rawFile: blobToFile(file, realFileType)
103
+ rawFile: blobToFile(file, asset.mimeType)
109
104
  });
110
105
  });
111
106
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@widergy/mobile-ui",
3
3
  "description": "Widergy Mobile Components",
4
4
  "author": "widergy",
5
- "version": "2.2.0",
5
+ "version": "2.3.1",
6
6
  "repository": "https://github.com/widergy/mobile-ui.git",
7
7
  "main": "lib/index.js",
8
8
  "files": [