@via-profit/ui-kit 0.6.0 → 0.8.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/Swiper/Swiper.js CHANGED
@@ -58,44 +58,64 @@ const SwiperTrack_1 = __importDefault(require("./SwiperTrack"));
58
58
  const SwiperSlidesRenderer_1 = __importDefault(require("./SwiperSlidesRenderer"));
59
59
  __exportStar(require("./SwiperSlide"), exports);
60
60
  const defaultSlideLabel = (index, total) => `${index + 1} / ${total}`;
61
+ const prefersReducedMotion = () => typeof window !== 'undefined' &&
62
+ typeof window.matchMedia === 'function' &&
63
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
61
64
  exports.Swiper = React.forwardRef((props, ref) => {
62
- const { children, dragThreshold = 240, snap = true, draggable = true, overrides, initialIndex = 0, infinite = false, onSlideChange, autoplay = false, autoplayInterval = 3000, pauseOnHover = true, threshold = 20, resistance = true, slidesPerView = 1, keyboardControl = true, slideLabel = defaultSlideLabel } = props, restProps = __rest(props, ["children", "dragThreshold", "snap", "draggable", "overrides", "initialIndex", "infinite", "onSlideChange", "autoplay", "autoplayInterval", "pauseOnHover", "threshold", "resistance", "slidesPerView", "keyboardControl", "slideLabel"]);
65
+ const { children, dragThreshold = 240, snap = true, draggable: draggableProp = true, overrides, initialIndex = 0, infinite = false, onSlideChange, autoplay = false, autoplayInterval = 3000, pauseOnHover = true, threshold = 20, resistance = true, slidesPerView: slidesPerViewProp = 1, centered = false, direction = 'horizontal', effect = 'slide', speed = 300, autoScroll, keyboardControl: keyboardControlProp = true, slideLabel = defaultSlideLabel } = props, restProps = __rest(props, ["children", "dragThreshold", "snap", "draggable", "overrides", "initialIndex", "infinite", "onSlideChange", "autoplay", "autoplayInterval", "pauseOnHover", "threshold", "resistance", "slidesPerView", "centered", "direction", "effect", "speed", "autoScroll", "keyboardControl", "slideLabel"]);
66
+ const isFade = effect === 'fade';
67
+ const isTicker = typeof autoScroll === 'number' && autoScroll > 0 && !isFade;
68
+ const isVertical = direction === 'vertical';
69
+ const slidesPerView = isFade ? 1 : Math.max(slidesPerViewProp, 0.1);
70
+ const draggable = draggableProp && !isTicker;
71
+ const keyboardControl = keyboardControlProp && !isTicker;
72
+ // The current slide is moved to the middle of the view by this number of slides
73
+ const centerShift = centered && !isFade ? (slidesPerView - 1) / 2 : 0;
63
74
  // #region Slides
64
- const slides = React.useMemo(() => {
75
+ const { slides, clones } = React.useMemo(() => {
65
76
  // toArray skips null/false and keeps the user keys (prefixed), so the slides are not remounted
66
77
  // when a slide is added to the start
67
78
  const childrenSlides = React.Children.toArray(children).filter(React.isValidElement);
68
- if (infinite) {
69
- if (childrenSlides.length < 2) {
70
- console.error('To infinite loop counts of the slides must be greater than 2');
71
- return childrenSlides;
72
- }
73
- if (childrenSlides.length < slidesPerView + 1) {
74
- console.error(`For infinite loop with slidesPerView=${slidesPerView}, need at least ${slidesPerView + 1} slides`);
75
- return childrenSlides;
76
- }
77
- // Используем стабильные ключи без Date.now()
78
- const headClones = childrenSlides.slice(-slidesPerView).map((slide, i) => React.cloneElement(slide, {
79
- key: `clone-head-${i}`,
80
- 'data-clone': 'head',
81
- }));
82
- const tailClones = childrenSlides.slice(0, slidesPerView).map((slide, i) => React.cloneElement(slide, {
83
- key: `clone-tail-${i}`,
84
- 'data-clone': 'tail',
85
- }));
86
- return [...headClones, ...childrenSlides, ...tailClones];
87
- }
88
- return childrenSlides;
89
- }, [children, infinite, slidesPerView]);
79
+ // The fade effect does not move the track, the loop does not need the copies
80
+ if (!(infinite || isTicker) || isFade) {
81
+ return { slides: childrenSlides, clones: 0 };
82
+ }
83
+ // The copies on each side cover the whole view (also its partially visible slides)
84
+ // and the part of the view in front of the centered slide
85
+ const count = Math.ceil(slidesPerView) + Math.ceil(centerShift);
86
+ if (childrenSlides.length < Math.max(2, count)) {
87
+ console.error(`For infinite loop with slidesPerView=${slidesPerView}${centered ? ' and centered' : ''}, need at least ${Math.max(2, count)} slides`);
88
+ return { slides: childrenSlides, clones: 0 };
89
+ }
90
+ const headClones = childrenSlides.slice(-count).map((slide, i) => React.cloneElement(slide, {
91
+ key: `clone-head-${i}`,
92
+ 'data-clone': 'head',
93
+ }));
94
+ const tailClones = childrenSlides.slice(0, count).map((slide, i) => React.cloneElement(slide, {
95
+ key: `clone-tail-${i}`,
96
+ 'data-clone': 'tail',
97
+ }));
98
+ return { slides: [...headClones, ...childrenSlides, ...tailClones], clones: count };
99
+ }, [children, infinite, isTicker, isFade, slidesPerView, centerShift, centered]);
100
+ // The infinite loop by the copies of the slides (slide effect and ticker)
101
+ const isLoop = clones > 0;
102
+ // The fade effect loops by the index alone
103
+ const isFadeLoop = isFade && infinite && slides.length > 1;
90
104
  const total = slides.length;
91
- const realSlidesCount = infinite ? total - 2 * slidesPerView : total;
92
- const maxIndex = Math.max(0, total - slidesPerView);
105
+ const realSlidesCount = total - 2 * clones;
106
+ // Without the loop the last position shows the last slide at the end of the view
107
+ // (in the middle of the view when centered). With a fractional slidesPerView it is not a whole
108
+ // slide: the index goes up to the next whole number and the track stops at the last position
109
+ const lastPosition = isFade || centerShift > 0 ? Math.max(0, total - 1) : Math.max(0, total - slidesPerView);
110
+ const maxIndex = Math.ceil(lastPosition - 0.001);
111
+ // The index while the track is between the slides (drag, free mode) without the loop
112
+ const maxFreeIndex = Math.floor(lastPosition + 0.001);
93
113
  // #region States
94
114
  const [index, setIndex] = React.useState(() => {
95
- if (!infinite) {
115
+ if (!isLoop) {
96
116
  return Math.max(0, Math.min(initialIndex, maxIndex));
97
117
  }
98
- return initialIndex + slidesPerView;
118
+ return initialIndex + clones;
99
119
  });
100
120
  const [offset, setOffset] = React.useState(0);
101
121
  // The long decelerating transition after a free swipe (snap=false)
@@ -109,8 +129,8 @@ exports.Swiper = React.forwardRef((props, ref) => {
109
129
  const isPaused = isPausedByApi || isHovered || isFocused;
110
130
  // #region Refs
111
131
  const dragging = React.useRef(false);
112
- const startX = React.useRef(0);
113
- const lastX = React.useRef(0);
132
+ const startCoord = React.useRef(0);
133
+ const lastCoord = React.useRef(0);
114
134
  const lastTime = React.useRef(0);
115
135
  const velocity = React.useRef(0);
116
136
  const wrapperRef = React.useRef(null);
@@ -118,12 +138,13 @@ exports.Swiper = React.forwardRef((props, ref) => {
118
138
  const isAnimating = React.useRef(false);
119
139
  const mounted = React.useRef(true);
120
140
  const suppressClick = React.useRef(false);
141
+ const getCoord = React.useCallback((e) => (isVertical ? e.clientY : e.clientX), [isVertical]);
121
142
  // #region Real Index
122
143
  const realIndex = React.useMemo(() => {
123
- if (!infinite) {
144
+ if (!isLoop) {
124
145
  return index;
125
146
  }
126
- const realIndex = index - slidesPerView;
147
+ const realIndex = index - clones;
127
148
  if (realIndex < 0) {
128
149
  return realSlidesCount + realIndex;
129
150
  }
@@ -132,7 +153,7 @@ exports.Swiper = React.forwardRef((props, ref) => {
132
153
  }
133
154
  // In the infinite mode any slide can be the first visible one, so there is no clamp
134
155
  return realIndex;
135
- }, [index, infinite, slidesPerView, realSlidesCount]);
156
+ }, [index, isLoop, clones, realSlidesCount]);
136
157
  const prevRealIndex = React.useRef(realIndex);
137
158
  // #region Effects
138
159
  React.useEffect(() => {
@@ -145,18 +166,18 @@ exports.Swiper = React.forwardRef((props, ref) => {
145
166
  if (!mounted.current)
146
167
  return;
147
168
  // The slides were removed: keep the index inside the allowed range
148
- const lastIndex = infinite ? total - slidesPerView : maxIndex;
169
+ const lastIndex = isLoop ? total - clones : maxIndex;
149
170
  if (index > lastIndex) {
150
- setIndex(infinite ? slidesPerView : maxIndex);
171
+ setIndex(isLoop ? clones : maxIndex);
151
172
  setOffset(0);
152
173
  }
153
- }, [index, infinite, total, slidesPerView, maxIndex]);
174
+ }, [index, isLoop, total, clones, maxIndex]);
154
175
  // #region Normalization
155
176
  const normalizeIndex = React.useCallback(() => {
156
- if (!infinite || !mounted.current)
177
+ if (!isLoop || !mounted.current)
157
178
  return;
158
- const firstRealIndex = slidesPerView;
159
- const lastRealIndex = total - slidesPerView - 1;
179
+ const firstRealIndex = clones;
180
+ const lastRealIndex = total - clones - 1;
160
181
  if (index < firstRealIndex || index > lastRealIndex) {
161
182
  // A clone position shows the same slides as the real position shifted by the slides count.
162
183
  // Not always the first/last real slide: after several fast clicks the index may be deeper in the clones
@@ -176,7 +197,7 @@ exports.Swiper = React.forwardRef((props, ref) => {
176
197
  });
177
198
  });
178
199
  }
179
- }, [index, infinite, slidesPerView, total, realSlidesCount]);
200
+ }, [index, isLoop, clones, total, realSlidesCount]);
180
201
  // #region Navigation API
181
202
  const indexRef = React.useRef(index);
182
203
  React.useEffect(() => {
@@ -185,13 +206,13 @@ exports.Swiper = React.forwardRef((props, ref) => {
185
206
  const goToIndex = React.useCallback((i) => {
186
207
  // In the infinite mode the index is normalized after the transition, so wait for it.
187
208
  // In the regular mode the transition is simply retargeted
188
- if (infinite && isAnimating.current)
209
+ if (isLoop && isAnimating.current)
189
210
  return;
190
211
  let targetIndex = i;
191
- if (infinite) {
192
- targetIndex = i + slidesPerView;
193
- const minIndex = slidesPerView;
194
- const maxIndex = slidesPerView + realSlidesCount - 1;
212
+ if (isLoop) {
213
+ targetIndex = i + clones;
214
+ const minIndex = clones;
215
+ const maxIndex = clones + realSlidesCount - 1;
195
216
  targetIndex = Math.max(minIndex, Math.min(maxIndex, targetIndex));
196
217
  }
197
218
  else {
@@ -205,17 +226,20 @@ exports.Swiper = React.forwardRef((props, ref) => {
205
226
  setOffset(0);
206
227
  setIndex(targetIndex);
207
228
  isAnimating.current = true;
208
- }, [infinite, slidesPerView, realSlidesCount, maxIndex]);
229
+ }, [isLoop, clones, realSlidesCount, maxIndex]);
209
230
  const next = React.useCallback(() => {
210
231
  setDisableAnimation(false);
211
232
  setIsMomentum(false);
212
233
  setOffset(0);
213
234
  setIndex(prev => {
214
235
  let nextIndex;
215
- if (infinite) {
236
+ if (isLoop) {
216
237
  // The last position is the first tail clone: it mirrors the first real slide.
217
238
  // Further positions would show the empty space when slidesPerView > 1
218
- nextIndex = Math.min(prev + 1, total - slidesPerView);
239
+ nextIndex = Math.min(prev + 1, total - clones);
240
+ }
241
+ else if (isFadeLoop) {
242
+ nextIndex = (prev + 1) % total;
219
243
  }
220
244
  else {
221
245
  nextIndex = Math.min(prev + 1, maxIndex);
@@ -227,50 +251,61 @@ exports.Swiper = React.forwardRef((props, ref) => {
227
251
  isAnimating.current = true;
228
252
  return nextIndex;
229
253
  });
230
- }, [infinite, maxIndex, total, slidesPerView]);
254
+ }, [isLoop, isFadeLoop, maxIndex, total, clones]);
231
255
  const prev = React.useCallback(() => {
232
256
  setDisableAnimation(false);
233
257
  setIsMomentum(false);
234
258
  setOffset(0);
235
259
  setIndex(prev => {
236
- const nextIndex = Math.max(prev - 1, 0);
260
+ const nextIndex = isFadeLoop ? (prev - 1 + total) % total : Math.max(prev - 1, 0);
237
261
  if (nextIndex === prev) {
238
262
  return prev;
239
263
  }
240
264
  isAnimating.current = true;
241
265
  return nextIndex;
242
266
  });
243
- }, []);
267
+ }, [isFadeLoop, total]);
244
268
  // #region Position
245
- // The position is the distance in pixels from the start of the track to the left edge of the view.
246
- // The state keeps it as index (whole slides) + offset (pixels), the track is translated by both
247
- const getSlideWidth = React.useCallback(() => { var _a; return (((_a = wrapperRef.current) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0) / slidesPerView; }, [slidesPerView]);
269
+ // The position is the distance in pixels from the start of the track to the start of the view
270
+ // without the centering shift. The state keeps it as index (whole slides) + offset (pixels)
271
+ const getSlideSize = React.useCallback(() => {
272
+ const wrapper = wrapperRef.current;
273
+ const size = (isVertical ? wrapper === null || wrapper === void 0 ? void 0 : wrapper.clientHeight : wrapper === null || wrapper === void 0 ? void 0 : wrapper.clientWidth) || 0;
274
+ return size / slidesPerView;
275
+ }, [isVertical, slidesPerView]);
248
276
  // The last allowed position. In the infinite mode it is the first tail clone
249
- const maxPosition = React.useCallback(() => (infinite ? total - slidesPerView : maxIndex) * getSlideWidth(), [infinite, total, slidesPerView, maxIndex, getSlideWidth]);
277
+ const maxPosition = React.useCallback(() => (isLoop ? total - clones : lastPosition) * getSlideSize(), [isLoop, total, clones, lastPosition, getSlideSize]);
278
+ // The index of the track: without the loop the track stops at the last position
279
+ // even if the index is the next whole number
280
+ const trackIndex = (isLoop ? index : Math.min(index, lastPosition)) - centerShift;
250
281
  // The position on the screen right now, also in the middle of a transition
251
282
  const readPosition = React.useCallback(() => {
252
283
  const track = trackRef.current;
284
+ const size = getSlideSize();
253
285
  if (track && typeof DOMMatrixReadOnly !== 'undefined') {
254
286
  const { transform } = getComputedStyle(track);
255
287
  if (transform && transform !== 'none') {
256
- return -new DOMMatrixReadOnly(transform).m41;
288
+ const matrix = new DOMMatrixReadOnly(transform);
289
+ return -(isVertical ? matrix.m42 : matrix.m41) + centerShift * size;
257
290
  }
258
291
  }
259
- return index * getSlideWidth() - offset;
260
- }, [index, offset, getSlideWidth]);
292
+ return (trackIndex + centerShift) * size - offset;
293
+ }, [trackIndex, centerShift, offset, getSlideSize, isVertical]);
261
294
  // Infinite mode: the same view is shown by the real slides and by the clones,
262
295
  // so the position is moved by the whole slides count when it comes close to the edge of the track
263
296
  const wrapPosition = React.useCallback((position) => {
264
- const width = getSlideWidth();
265
- const span = realSlidesCount * width;
266
- if (!infinite || width === 0)
297
+ const size = getSlideSize();
298
+ const span = realSlidesCount * size;
299
+ if (!isLoop || size === 0)
267
300
  return 0;
268
- if (position < width * 0.5)
301
+ if (position < size * 0.5)
269
302
  return span;
270
- if (position > maxPosition() - width * 0.5)
303
+ if (position > maxPosition() - size * 0.5)
271
304
  return -span;
272
305
  return 0;
273
- }, [infinite, realSlidesCount, getSlideWidth, maxPosition]);
306
+ }, [isLoop, realSlidesCount, getSlideSize, maxPosition]);
307
+ // Without the loop the index between the slides does not go past the last position
308
+ const clampFreeIndex = React.useCallback((i) => (isLoop ? i : Math.max(0, Math.min(maxFreeIndex, i))), [isLoop, maxFreeIndex]);
274
309
  const dragBasePosition = React.useRef(0);
275
310
  const dragAnchorIndex = React.useRef(0);
276
311
  const dragPosition = React.useRef(0);
@@ -281,49 +316,61 @@ exports.Swiper = React.forwardRef((props, ref) => {
281
316
  // Only the main mouse button, the touch or the pen
282
317
  if (e.pointerType === 'mouse' && e.button !== 0)
283
318
  return;
284
- const width = getSlideWidth();
319
+ suppressClick.current = false;
320
+ dragging.current = true;
321
+ startCoord.current = getCoord(e);
322
+ lastCoord.current = getCoord(e);
323
+ lastTime.current = performance.now();
324
+ velocity.current = 0;
325
+ // The capture fails for a pointer that is already gone: the drag still works without it
326
+ try {
327
+ e.target.setPointerCapture(e.pointerId);
328
+ }
329
+ catch (_a) {
330
+ // nothing
331
+ }
332
+ // The fade does not move the track: the slide changes after the release
333
+ if (isFade)
334
+ return;
335
+ const size = getSlideSize();
285
336
  // Grab the track where it is now: a running transition stops under the finger instead of jumping
286
337
  let position = readPosition();
287
338
  position += wrapPosition(position);
288
- const anchor = width > 0 ? Math.round(position / width) : index;
339
+ const anchor = clampFreeIndex(size > 0 ? Math.round(position / size) : index);
289
340
  dragBasePosition.current = position;
290
341
  dragPosition.current = position;
291
342
  dragAnchorIndex.current = anchor;
292
- suppressClick.current = false;
293
- dragging.current = true;
294
343
  isAnimating.current = false;
295
344
  setDisableAnimation(true);
296
345
  setIsMomentum(false);
297
346
  setIsDragging(true);
298
347
  setIndex(anchor);
299
- setOffset(anchor * width - position);
300
- startX.current = e.clientX;
301
- lastX.current = e.clientX;
302
- lastTime.current = performance.now();
303
- velocity.current = 0;
304
- e.target.setPointerCapture(e.pointerId);
305
- }, [draggable, getSlideWidth, readPosition, wrapPosition, index]);
348
+ setOffset(anchor * size - position);
349
+ }, [draggable, isFade, getCoord, getSlideSize, readPosition, wrapPosition, clampFreeIndex, index]);
306
350
  const onPointerMove = React.useCallback((e) => {
307
351
  if (!dragging.current)
308
352
  return;
353
+ const coord = getCoord(e);
309
354
  const now = performance.now();
310
- const dx = e.clientX - lastX.current;
355
+ const delta = coord - lastCoord.current;
311
356
  const dt = now - lastTime.current;
312
357
  if (dt > 0) {
313
- velocity.current = dx / dt;
358
+ velocity.current = delta / dt;
314
359
  }
315
- lastX.current = e.clientX;
360
+ lastCoord.current = coord;
316
361
  lastTime.current = now;
317
- const width = getSlideWidth();
318
- let position = dragBasePosition.current - (e.clientX - startX.current);
362
+ if (isFade)
363
+ return;
364
+ const size = getSlideSize();
365
+ let position = dragBasePosition.current - (coord - startCoord.current);
319
366
  // Endless drag in the infinite mode: move the base together with the position
320
367
  const shift = wrapPosition(position);
321
368
  if (shift !== 0) {
322
369
  dragBasePosition.current += shift;
323
- dragAnchorIndex.current += Math.round(shift / width);
370
+ dragAnchorIndex.current += Math.round(shift / size);
324
371
  position += shift;
325
372
  }
326
- if (!infinite) {
373
+ if (!isLoop) {
327
374
  const max = maxPosition();
328
375
  if (position < 0) {
329
376
  position = resistance ? position * 0.3 : 0;
@@ -335,27 +382,55 @@ exports.Swiper = React.forwardRef((props, ref) => {
335
382
  dragPosition.current = position;
336
383
  // snap: the index stays the same during the drag, the slide changes after the release.
337
384
  // free: the index follows the position, so onSlideChange reports the slide under the view
338
- const nextIndex = snap ? dragAnchorIndex.current : Math.round(position / width);
385
+ const nextIndex = snap
386
+ ? dragAnchorIndex.current
387
+ : clampFreeIndex(Math.round(position / size));
339
388
  setIndex(nextIndex);
340
- setOffset(nextIndex * width - position);
341
- }, [getSlideWidth, wrapPosition, infinite, maxPosition, resistance, snap]);
389
+ setOffset(nextIndex * size - position);
390
+ }, [
391
+ getCoord,
392
+ isFade,
393
+ getSlideSize,
394
+ wrapPosition,
395
+ isLoop,
396
+ maxPosition,
397
+ resistance,
398
+ snap,
399
+ clampFreeIndex,
400
+ ]);
342
401
  const onPointerUp = React.useCallback((e) => {
343
402
  if (!dragging.current)
344
403
  return;
345
- const width = getSlideWidth();
346
- const position = dragPosition.current;
347
- const delta = lastX.current - startX.current;
348
- // The pointer stopped before the release: no inertia
349
- const v = performance.now() - lastTime.current > 100 ? 0 : velocity.current;
350
- const lastIndex = infinite ? total - slidesPerView : maxIndex;
404
+ const delta = lastCoord.current - startCoord.current;
351
405
  // It was a drag, not a click: the click on a link or a button inside the slide must not fire
352
406
  if (Math.abs(delta) > threshold) {
353
407
  suppressClick.current = true;
354
408
  }
409
+ dragging.current = false;
410
+ try {
411
+ e.target.releasePointerCapture(e.pointerId);
412
+ }
413
+ catch (_a) {
414
+ // nothing
415
+ }
416
+ if (isFade) {
417
+ if (delta < -threshold) {
418
+ next();
419
+ }
420
+ else if (delta > threshold) {
421
+ prev();
422
+ }
423
+ return;
424
+ }
425
+ const size = getSlideSize();
426
+ const position = dragPosition.current;
427
+ // The pointer stopped before the release: no inertia
428
+ const v = performance.now() - lastTime.current > 100 ? 0 : velocity.current;
429
+ const lastIndex = isLoop ? total - clones : maxIndex;
355
430
  setDisableAnimation(false);
356
431
  if (snap) {
357
432
  const anchor = dragAnchorIndex.current;
358
- const displacement = position - anchor * width;
433
+ const displacement = position - anchor * size;
359
434
  const velocityFactor = Math.min(Math.abs(v) * 250, 0.65);
360
435
  const dynamicThreshold = Math.max(threshold, Math.min(dragThreshold * (1 - velocityFactor), dragThreshold));
361
436
  let nextIndex = anchor;
@@ -363,37 +438,42 @@ exports.Swiper = React.forwardRef((props, ref) => {
363
438
  // At least one slide, more if the slide was dragged further
364
439
  nextIndex =
365
440
  displacement > 0
366
- ? Math.max(anchor + 1, Math.round(position / width))
367
- : Math.min(anchor - 1, Math.round(position / width));
441
+ ? Math.max(anchor + 1, Math.round(position / size))
442
+ : Math.min(anchor - 1, Math.round(position / size));
368
443
  }
369
444
  nextIndex = Math.max(0, Math.min(lastIndex, nextIndex));
370
445
  setIndex(nextIndex);
371
446
  setOffset(0);
372
447
  // A click without movement does not start a transition, so transitionend never comes
373
448
  // and the flag would block autoplay and goToIndex forever
374
- isAnimating.current = Math.abs(nextIndex * width - position) > 0.5;
449
+ const target = (isLoop ? nextIndex : Math.min(nextIndex, lastPosition)) * size;
450
+ isAnimating.current = Math.abs(target - position) > 0.5;
375
451
  }
376
452
  else {
377
453
  // Free mode: the track keeps moving by inertia and stops where it stops
378
- const projected = Math.max(0, Math.min(lastIndex * width, position - v * 300));
379
- const nextIndex = width > 0 ? Math.round(projected / width) : index;
454
+ const maxProjected = (isLoop ? lastIndex : lastPosition) * size;
455
+ const projected = Math.max(0, Math.min(maxProjected, position - v * 300));
456
+ const nextIndex = clampFreeIndex(size > 0 ? Math.round(projected / size) : index);
380
457
  setIsMomentum(true);
381
458
  setIndex(nextIndex);
382
- setOffset(nextIndex * width - projected);
459
+ setOffset(nextIndex * size - projected);
383
460
  isAnimating.current = Math.abs(projected - position) > 0.5;
384
461
  }
385
- dragging.current = false;
386
462
  setIsDragging(false);
387
- e.target.releasePointerCapture(e.pointerId);
388
463
  }, [
389
- getSlideWidth,
390
- infinite,
464
+ isFade,
465
+ next,
466
+ prev,
467
+ getSlideSize,
468
+ isLoop,
391
469
  total,
392
- slidesPerView,
470
+ clones,
393
471
  maxIndex,
472
+ lastPosition,
394
473
  threshold,
395
474
  snap,
396
475
  dragThreshold,
476
+ clampFreeIndex,
397
477
  index,
398
478
  ]);
399
479
  const onLostPointerCapture = React.useCallback((e) => {
@@ -405,7 +485,7 @@ exports.Swiper = React.forwardRef((props, ref) => {
405
485
  const autoplayStep = React.useRef(() => undefined);
406
486
  autoplayStep.current = () => {
407
487
  // Without the infinite loop the autoplay returns to the first slide instead of stopping at the end
408
- if (!infinite && indexRef.current >= maxIndex) {
488
+ if (!isLoop && !isFadeLoop && indexRef.current >= maxIndex) {
409
489
  goToIndex(0);
410
490
  }
411
491
  else {
@@ -415,7 +495,7 @@ exports.Swiper = React.forwardRef((props, ref) => {
415
495
  const pause = React.useCallback(() => setIsPausedByApi(true), []);
416
496
  const resume = React.useCallback(() => setIsPausedByApi(false), []);
417
497
  React.useEffect(() => {
418
- if (!autoplay || isPaused)
498
+ if (!autoplay || isTicker || isPaused)
419
499
  return undefined;
420
500
  const timer = setInterval(() => {
421
501
  if (!dragging.current && !isAnimating.current && mounted.current) {
@@ -423,13 +503,57 @@ exports.Swiper = React.forwardRef((props, ref) => {
423
503
  }
424
504
  }, autoplayInterval);
425
505
  return () => clearInterval(timer);
426
- }, [autoplay, autoplayInterval, isPaused]);
506
+ }, [autoplay, isTicker, autoplayInterval, isPaused]);
507
+ // #region Ticker
508
+ // The continuous scroll writes the transform directly: 60 renders per second are not needed for it.
509
+ // The position is kept in slides, so it stays in place when the size of the swiper changes
510
+ const tickerPosition = React.useRef(null);
511
+ React.useEffect(() => {
512
+ const track = trackRef.current;
513
+ if (!isTicker || !isLoop || !track)
514
+ return undefined;
515
+ if (isPaused || prefersReducedMotion())
516
+ return undefined;
517
+ let frame = 0;
518
+ let last = performance.now();
519
+ const step = (now) => {
520
+ var _a;
521
+ const size = getSlideSize();
522
+ if (size > 0) {
523
+ let position = (_a = tickerPosition.current) !== null && _a !== void 0 ? _a : clones;
524
+ position += ((autoScroll || 0) * (now - last)) / 1000 / size;
525
+ if (position >= clones + realSlidesCount) {
526
+ position -= realSlidesCount;
527
+ }
528
+ tickerPosition.current = position;
529
+ const translate = -(position - centerShift) * size;
530
+ track.style.transition = 'none';
531
+ track.style.transform = isVertical
532
+ ? `translateY(${translate}px)`
533
+ : `translateX(${translate}px)`;
534
+ }
535
+ last = now;
536
+ frame = requestAnimationFrame(step);
537
+ };
538
+ frame = requestAnimationFrame(step);
539
+ return () => cancelAnimationFrame(frame);
540
+ }, [
541
+ isTicker,
542
+ isLoop,
543
+ isPaused,
544
+ autoScroll,
545
+ getSlideSize,
546
+ realSlidesCount,
547
+ clones,
548
+ centerShift,
549
+ isVertical,
550
+ ]);
427
551
  // #region Hover and focus
428
552
  // The autoplay pauses while the pointer is over the swiper (pauseOnHover)
429
553
  // and always while the focus is inside it, so the keyboard user is not interrupted
430
554
  React.useEffect(() => {
431
555
  const wrapper = wrapperRef.current;
432
- if (!autoplay || !wrapper)
556
+ if (!(autoplay || isTicker) || !wrapper)
433
557
  return undefined;
434
558
  const handleMouseEnter = () => pauseOnHover && setIsHovered(true);
435
559
  const handleMouseLeave = () => setIsHovered(false);
@@ -449,7 +573,7 @@ exports.Swiper = React.forwardRef((props, ref) => {
449
573
  wrapper.removeEventListener('focusin', handleFocusIn);
450
574
  wrapper.removeEventListener('focusout', handleFocusOut);
451
575
  };
452
- }, [autoplay, pauseOnHover]);
576
+ }, [autoplay, isTicker, pauseOnHover]);
453
577
  // #region Keyboard
454
578
  const onKeyDown = React.useCallback((event) => {
455
579
  if (!keyboardControl)
@@ -458,15 +582,15 @@ exports.Swiper = React.forwardRef((props, ref) => {
458
582
  const target = event.target;
459
583
  if (target.closest('input, textarea, select, [contenteditable="true"]'))
460
584
  return;
461
- if (event.key === 'ArrowLeft') {
585
+ if (event.key === (isVertical ? 'ArrowUp' : 'ArrowLeft')) {
462
586
  event.preventDefault();
463
587
  prev();
464
588
  }
465
- if (event.key === 'ArrowRight') {
589
+ if (event.key === (isVertical ? 'ArrowDown' : 'ArrowRight')) {
466
590
  event.preventDefault();
467
591
  next();
468
592
  }
469
- }, [keyboardControl, prev, next]);
593
+ }, [keyboardControl, isVertical, prev, next]);
470
594
  const onClickCapture = React.useCallback((event) => {
471
595
  if (suppressClick.current) {
472
596
  suppressClick.current = false;
@@ -485,14 +609,16 @@ exports.Swiper = React.forwardRef((props, ref) => {
485
609
  const track = trackRef.current;
486
610
  if (!track)
487
611
  return;
612
+ // The fade changes the opacity of the slides, the event bubbles up to the track
613
+ const property = isFade ? 'opacity' : 'transform';
488
614
  const handleTransitionEnd = (e) => {
489
- if (e.propertyName !== 'transform')
615
+ if (e.propertyName !== property)
490
616
  return;
491
617
  isAnimating.current = false;
492
- normalizeIndex(); // <-- вот здесь
618
+ normalizeIndex();
493
619
  };
494
620
  const handleTransitionCancel = (e) => {
495
- if (e.propertyName !== 'transform')
621
+ if (e.propertyName !== property)
496
622
  return;
497
623
  isAnimating.current = false;
498
624
  };
@@ -502,7 +628,7 @@ exports.Swiper = React.forwardRef((props, ref) => {
502
628
  track.removeEventListener('transitionend', handleTransitionEnd);
503
629
  track.removeEventListener('transitioncancel', handleTransitionCancel);
504
630
  };
505
- }, [normalizeIndex]);
631
+ }, [normalizeIndex, isFade]);
506
632
  // #region Mount
507
633
  React.useEffect(() => {
508
634
  mounted.current = true;
@@ -527,34 +653,41 @@ exports.Swiper = React.forwardRef((props, ref) => {
527
653
  Wrapper: (overrides === null || overrides === void 0 ? void 0 : overrides.Wrapper) || SwiperWrapper_1.default,
528
654
  }), [overrides]);
529
655
  const getSlideRealIndex = React.useCallback((domIndex) => {
530
- if (!infinite) {
656
+ if (!isLoop) {
531
657
  return domIndex;
532
658
  }
533
- const totalSlides = realSlidesCount;
534
- if (domIndex < slidesPerView) {
535
- return totalSlides - slidesPerView + domIndex;
659
+ if (domIndex < clones) {
660
+ return realSlidesCount - clones + domIndex;
536
661
  }
537
- if (domIndex >= total - slidesPerView) {
538
- return domIndex - (total - slidesPerView);
662
+ if (domIndex >= total - clones) {
663
+ return domIndex - (total - clones);
539
664
  }
540
- return domIndex - slidesPerView;
541
- }, [infinite, realSlidesCount, slidesPerView, total]);
665
+ return domIndex - clones;
666
+ }, [isLoop, realSlidesCount, clones, total]);
542
667
  // The slides at least partially in the view: they are not inert, so a partially visible slide
543
- // can be clicked. Between the slides (drag, free mode) one more slide is partially visible
544
- const slideWidth = wrapperRef.current ? wrapperRef.current.clientWidth / slidesPerView : 0;
545
- let firstVisibleDomIndex = index;
546
- let visibleCount = slidesPerView;
547
- if (slideWidth > 0 && Math.abs(offset) > 0.5) {
548
- const position = index * slideWidth - offset;
549
- firstVisibleDomIndex = Math.floor(position / slideWidth + 0.001);
550
- const lastVisibleDomIndex = Math.ceil((position + slidesPerView * slideWidth) / slideWidth - 0.001) - 1;
551
- visibleCount = lastVisibleDomIndex - firstVisibleDomIndex + 1;
668
+ // can be clicked. Between the slides (drag, free mode), with a fractional slidesPerView
669
+ // and with the centering one more slide is partially visible
670
+ const wrapper = wrapperRef.current;
671
+ const slideSize = wrapper
672
+ ? (isVertical ? wrapper.clientHeight : wrapper.clientWidth) / slidesPerView
673
+ : 0;
674
+ const viewPosition = slideSize > 0 ? trackIndex - offset / slideSize : trackIndex;
675
+ let firstVisibleDomIndex = Math.floor(viewPosition + 0.001);
676
+ let visibleCount = Math.ceil(viewPosition + slidesPerView - 0.001) - firstVisibleDomIndex;
677
+ if (isFade) {
678
+ firstVisibleDomIndex = index;
679
+ visibleCount = 1;
552
680
  }
553
681
  firstVisibleDomIndex = Math.max(0, Math.min(total - 1, firstVisibleDomIndex));
682
+ // The ticker moves without the state: all the slides are rendered
683
+ if (isTicker) {
684
+ firstVisibleDomIndex = clones;
685
+ visibleCount = realSlidesCount;
686
+ }
554
687
  return (React.createElement(overridesMap.Container, Object.assign({ role: "region", "aria-roledescription": "carousel" }, restProps),
555
- React.createElement(overridesMap.Wrapper, { ref: wrapperRef, draggable: draggable, slidesPerView: slidesPerView, tabIndex: keyboardControl ? 0 : undefined, "aria-live": autoplay && !isPaused ? 'off' : 'polite', onKeyDown: onKeyDown, onClickCapture: onClickCapture, onDragStart: onDragStart, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onLostPointerCapture: onLostPointerCapture },
556
- React.createElement(overridesMap.Track, { ref: trackRef, dragging: isDragging, index: index, offset: offset, disableAnimation: disableAnimation, momentum: isMomentum, slidesPerView: slidesPerView },
557
- React.createElement(SwiperSlidesRenderer_1.default, { slides: slides, slidesPerView: slidesPerView, realIndex: realIndex, firstVisibleIndex: getSlideRealIndex(firstVisibleDomIndex), visibleCount: visibleCount, realSlidesCount: realSlidesCount, getSlideRealIndex: getSlideRealIndex, slideLabel: slideLabel })))));
688
+ React.createElement(overridesMap.Wrapper, { ref: wrapperRef, draggable: draggable, slidesPerView: slidesPerView, vertical: isVertical, tabIndex: keyboardControl ? 0 : undefined, "aria-live": (autoplay || isTicker) && !isPaused ? 'off' : 'polite', onKeyDown: onKeyDown, onClickCapture: onClickCapture, onDragStart: onDragStart, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onLostPointerCapture: onLostPointerCapture },
689
+ React.createElement(overridesMap.Track, { ref: trackRef, dragging: isDragging, index: isFade ? 0 : trackIndex, offset: isFade ? 0 : offset, disableAnimation: disableAnimation, momentum: isMomentum, slidesPerView: slidesPerView, vertical: isVertical, effect: effect, speed: speed },
690
+ React.createElement(SwiperSlidesRenderer_1.default, { slides: slides, slidesPerView: slidesPerView, realIndex: realIndex, firstVisibleIndex: getSlideRealIndex(firstVisibleDomIndex), visibleCount: visibleCount, realSlidesCount: realSlidesCount, getSlideRealIndex: getSlideRealIndex, slideLabel: slideLabel, activeDomIndex: isFade ? index : null })))));
558
691
  });
559
692
  exports.Swiper.displayName = 'Swiper';
560
693
  exports.default = exports.Swiper;