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