@yoursurprise/slider 2.2.0-beta.4 → 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.
@@ -1,20 +1,36 @@
1
1
  import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import '@testing-library/jest-dom';
4
- import { Slider, SliderTypes } from './Slider';
5
- import React from 'react';
6
- import API = SliderTypes.API;
7
-
8
- const renderSliderWithDimensions = (clientWidth = 1000, scrollWidth = 2000, scrollLeft = 0) => {
4
+ import type { SliderTypes } from './Slider';
5
+ import { Orientation, Slider } from './Slider';
6
+ import React, { ComponentType } from 'react';
7
+
8
+ type SliderOptions = typeof Slider extends ComponentType<infer T> ? Omit<T, 'children'> : never;
9
+
10
+ const renderSliderWithDimensions = ({
11
+ clientWidth = 1000,
12
+ scrollWidth = 2000,
13
+ scrollLeft = 0,
14
+ scrollTop = 0,
15
+ scrollHeight = 2000,
16
+ clientHeight = 1000,
17
+ }, sliderOptions: SliderOptions = {}) => {
9
18
  Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: clientWidth });
10
19
  Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: scrollWidth });
20
+ Object.defineProperty(HTMLElement.prototype, 'scrollHeight', { configurable: true, value: scrollHeight });
21
+ Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, value: clientHeight });
11
22
  Object.defineProperty(HTMLElement.prototype, 'scrollLeft', {
12
23
  configurable: true,
13
24
  value: scrollLeft,
14
25
  writable: true,
15
26
  });
27
+ Object.defineProperty(HTMLElement.prototype, 'scrollTop', {
28
+ configurable: true,
29
+ value: scrollTop,
30
+ writable: true,
31
+ });
16
32
 
17
- render(<Slider>
33
+ render(<Slider {...sliderOptions}>
18
34
  <span key={1}/>
19
35
  <span key={2}/>
20
36
  <span key={3}/>
@@ -22,7 +38,7 @@ const renderSliderWithDimensions = (clientWidth = 1000, scrollWidth = 2000, scro
22
38
  </Slider>);
23
39
  };
24
40
 
25
- describe('UpsellSlider', () => {
41
+ describe('Slider', () => {
26
42
  let mockIntersectionObserver: jest.Mock<IntersectionObserver, ConstructorParameters<typeof IntersectionObserver>>;
27
43
  let observeSpy: jest.Mock;
28
44
  let disconnectSpy: jest.Mock;
@@ -72,16 +88,53 @@ describe('UpsellSlider', () => {
72
88
  expect(observeSpy).toHaveBeenCalledTimes(children.length);
73
89
  });
74
90
 
91
+ it('does not set the container scrollable if the scroll area does not exceed the container width', () => {
92
+ renderSliderWithDimensions({
93
+ clientWidth: 200,
94
+ scrollWidth: 200,
95
+ clientHeight: 200,
96
+ scrollHeight: 200,
97
+ });
98
+
99
+ expect(screen.getByRole('list')).not.toHaveClass('slider__wrapper--is-scrollable');
100
+ });
101
+
102
+ it('does not set the container scrollable if the scroll area does not exceed the container height', () => {
103
+ renderSliderWithDimensions({
104
+ clientWidth: 200,
105
+ scrollWidth: 200,
106
+ clientHeight: 200,
107
+ scrollHeight: 200,
108
+ }, {
109
+ orientation: Orientation.VERTICAL,
110
+ });
111
+
112
+
113
+ expect(screen.getByRole('list')).not.toHaveClass('slider__wrapper--is-scrollable');
114
+ });
115
+
75
116
  it('sets the container scrollable if the scroll area exceeds the container width', () => {
76
- renderSliderWithDimensions(500, 1000);
117
+ renderSliderWithDimensions({
118
+ clientWidth: 500,
119
+ scrollWidth: 1000,
120
+ clientHeight: 200,
121
+ scrollHeight: 200,
122
+ });
77
123
 
78
- expect(screen.getByRole('list')).toHaveClass('is-scrollable');
124
+ expect(screen.getByRole('list')).toHaveClass('slider__wrapper--is-scrollable');
79
125
  });
80
126
 
81
- it('does not set the container scrollable if the scroll area does not exceed the container width', () => {
82
- renderSliderWithDimensions(500, 400);
127
+ it('sets the container scrollable if the scroll area exceeds the container height', () => {
128
+ renderSliderWithDimensions({
129
+ clientWidth: 200,
130
+ scrollWidth: 200,
131
+ clientHeight: 500,
132
+ scrollHeight: 1000,
133
+ }, {
134
+ orientation: Orientation.VERTICAL,
135
+ });
83
136
 
84
- expect(screen.getByRole('list')).not.toHaveClass('is-scrollable');
137
+ expect(screen.getByRole('list')).toHaveClass('slider__wrapper--is-scrollable');
85
138
  });
86
139
 
87
140
  it('disconnects the intersection observer on re-render', () => {
@@ -96,7 +149,7 @@ describe('UpsellSlider', () => {
96
149
 
97
150
  describe('controls', () => {
98
151
  it('sets controls visibility initially', () => {
99
- renderSliderWithDimensions();
152
+ renderSliderWithDimensions({});
100
153
 
101
154
  const nextButton = screen.getByLabelText('Next slide');
102
155
  const prevButton = screen.getByLabelText('Previous slide');
@@ -121,18 +174,29 @@ describe('UpsellSlider', () => {
121
174
  });
122
175
 
123
176
  it('allows scrolling by dragging with the mouse', () => {
124
- renderSliderWithDimensions();
177
+ renderSliderWithDimensions({});
125
178
 
126
179
  const scrollElement = screen.getByRole('list');
127
180
 
128
- // eslint-disable-next-line testing-library/prefer-user-event
129
- fireEvent.mouseDown(scrollElement);
130
- // eslint-disable-next-line testing-library/prefer-user-event
131
- fireEvent.mouseMove(scrollElement, { clientX: 100, clientY: 0 });
132
- // eslint-disable-next-line testing-library/prefer-user-event
133
- fireEvent.mouseUp(scrollElement);
181
+ act(() => fireEvent.mouseDown(scrollElement));
182
+ act(() => fireEvent.mouseMove(scrollElement, { clientX: 100, clientY: 0 }));
183
+ act(() => fireEvent.mouseUp(scrollElement));
134
184
 
135
185
  expect(scrollElement.scrollLeft).toBe(-100);
186
+ expect(scrollElement.scrollTop).toBe(0);
187
+ });
188
+
189
+ it('allows vertical scrolling by dragging with the mouse', () => {
190
+ renderSliderWithDimensions({}, { orientation: Orientation.VERTICAL });
191
+
192
+ const scrollElement = screen.getByRole('list');
193
+
194
+ act(() => fireEvent.mouseDown(scrollElement));
195
+ act(() => fireEvent.mouseMove(scrollElement, { clientX: 0, clientY: 100 }));
196
+ act(() => fireEvent.mouseUp(scrollElement));
197
+
198
+ expect(scrollElement.scrollTop).toBe(-100);
199
+ expect(scrollElement.scrollLeft).toBe(0);
136
200
  });
137
201
 
138
202
  it('registers click when not scrolling', async () => {
@@ -144,17 +208,15 @@ describe('UpsellSlider', () => {
144
208
 
145
209
  const scrollElement = screen.getByRole('list');
146
210
 
147
- // eslint-disable-next-line testing-library/prefer-user-event
148
- fireEvent.mouseDown(scrollElement);
149
- // eslint-disable-next-line testing-library/prefer-user-event
150
- fireEvent.mouseMove(scrollElement, { clientX: 100, clientY: 0 });
151
- // eslint-disable-next-line testing-library/prefer-user-event
152
- fireEvent.mouseUp(scrollElement);
211
+ act(() => fireEvent.mouseDown(scrollElement));
212
+ act(() => fireEvent.mouseMove(scrollElement, { clientX: 100, clientY: 0 }));
213
+ act(() => fireEvent.mouseUp(scrollElement));
153
214
 
154
- // This click is normally triggered when releasing the mouse after scrolling
155
- await userEvent.click(scrollElement);
156
-
157
- await userEvent.click(screen.getByTestId('1'));
215
+ await act(async () => {
216
+ // This click is normally triggered when releasing the mouse after scrolling
217
+ await userEvent.click(scrollElement);
218
+ await userEvent.click(screen.getByTestId('1'));
219
+ });
158
220
 
159
221
  await waitFor(() => {
160
222
  expect(clickSpy).toHaveBeenCalledTimes(1);
@@ -164,7 +226,7 @@ describe('UpsellSlider', () => {
164
226
 
165
227
  describe('sliding', () => {
166
228
  it('scrolls to the next slide', async () => {
167
- renderSliderWithDimensions();
229
+ renderSliderWithDimensions({});
168
230
 
169
231
  const intersectionObserverInstance = getIntersectionObserverInstance();
170
232
  const [intersectionCallback] = intersectionObserverInstance;
@@ -192,13 +254,45 @@ describe('UpsellSlider', () => {
192
254
  expect(scrollToSpy).toHaveBeenCalledWith({
193
255
  behavior: 'smooth',
194
256
  left: 200,
195
- top: 0,
257
+ });
258
+ });
259
+ });
260
+
261
+ it('scrolls to the next slide vertically', async () => {
262
+ renderSliderWithDimensions({}, { orientation: Orientation.VERTICAL });
263
+
264
+ const intersectionObserverInstance = getIntersectionObserverInstance();
265
+ const [intersectionCallback] = intersectionObserverInstance;
266
+
267
+ const slides = screen.getAllByRole('listitem');
268
+ const nextButton = screen.getByLabelText('Next slide');
269
+
270
+ slides.forEach((child, i) => {
271
+ Object.defineProperty(child, 'clientHeight', { value: 100 * (i + 1) });
272
+ Object.defineProperty(child, 'offsetTop', { value: 100 * (i + 1) });
273
+ });
274
+
275
+ act(() => {
276
+ intersectionCallback([
277
+ { intersectionRatio: 1, target: slides[0] } as unknown as IntersectionObserverEntry,
278
+ { intersectionRatio: 0.5, target: slides[1] } as unknown as IntersectionObserverEntry,
279
+ { intersectionRatio: 0, target: slides[2] } as unknown as IntersectionObserverEntry,
280
+ { intersectionRatio: 0, target: slides[3] } as unknown as IntersectionObserverEntry,
281
+ ], mockIntersectionObserver.mock.instances[0]);
282
+ });
283
+
284
+ await userEvent.click(nextButton);
285
+
286
+ await waitFor(() => {
287
+ expect(scrollToSpy).toHaveBeenCalledWith({
288
+ behavior: 'smooth',
289
+ top: 200,
196
290
  });
197
291
  });
198
292
  });
199
293
 
200
294
  it('scrolls to the previous slide', async () => {
201
- renderSliderWithDimensions();
295
+ renderSliderWithDimensions({});
202
296
 
203
297
  const intersectionObserverInstance = getIntersectionObserverInstance();
204
298
  const [intersectionCallback] = intersectionObserverInstance;
@@ -226,7 +320,39 @@ describe('UpsellSlider', () => {
226
320
  expect(scrollToSpy).toHaveBeenCalledWith({
227
321
  behavior: 'smooth',
228
322
  left: -600,
229
- top: 0,
323
+ });
324
+ });
325
+ });
326
+
327
+ it('scrolls to the previous slide vertically', async () => {
328
+ renderSliderWithDimensions({}, { orientation: Orientation.VERTICAL });
329
+
330
+ const intersectionObserverInstance = getIntersectionObserverInstance();
331
+ const [intersectionCallback] = intersectionObserverInstance;
332
+
333
+ const slides = screen.getAllByRole('listitem');
334
+ const prevButton = screen.getByLabelText('Previous slide');
335
+
336
+ slides.forEach((child, i) => {
337
+ Object.defineProperty(child, 'clientHeight', { value: 100 * (i + 1) });
338
+ Object.defineProperty(child, 'offsetTop', { value: 100 * (i + 1) });
339
+ });
340
+
341
+ act(() => {
342
+ intersectionCallback([
343
+ { intersectionRatio: 0, target: slides[0] } as unknown as IntersectionObserverEntry,
344
+ { intersectionRatio: 0, target: slides[1] } as unknown as IntersectionObserverEntry,
345
+ { intersectionRatio: 1, target: slides[2] } as unknown as IntersectionObserverEntry,
346
+ { intersectionRatio: 0.5, target: slides[3] } as unknown as IntersectionObserverEntry,
347
+ ], mockIntersectionObserver.mock.instances[0]);
348
+ });
349
+
350
+ await userEvent.click(prevButton);
351
+
352
+ await waitFor(() => {
353
+ expect(scrollToSpy).toHaveBeenCalledWith({
354
+ behavior: 'smooth',
355
+ top: -600,
230
356
  });
231
357
  });
232
358
  });
@@ -274,8 +400,8 @@ describe('UpsellSlider', () => {
274
400
  });
275
401
 
276
402
  describe('scrollToSlide', () => {
277
- it('scrolls to the next slide', async () => {
278
- const ref = React.createRef<API>();
403
+ it('scrolls to a specific slide', async () => {
404
+ const ref = React.createRef<SliderTypes.API>();
279
405
  render(<Slider ref={ref}>
280
406
  <span key={1}/>
281
407
  <span key={2}/>
@@ -300,12 +426,72 @@ describe('UpsellSlider', () => {
300
426
  expect(scrollToSpy).toHaveBeenCalledWith({
301
427
  behavior: 'smooth',
302
428
  left: 1500,
303
- top: 0,
429
+ });
430
+ });
431
+ });
432
+
433
+ it('scrolls to a specific slide', async () => {
434
+ const ref = React.createRef<SliderTypes.API>();
435
+ render(<Slider ref={ref}>
436
+ <span key={1}/>
437
+ <span key={2}/>
438
+ <span key={3}/>
439
+ <span key={4}/>
440
+ </Slider>);
441
+
442
+ const slides = screen.getAllByRole('listitem');
443
+
444
+ slides.forEach((child, i) => {
445
+ Object.defineProperty(child, 'clientWidth', { configurable: true, value: 500 });
446
+ Object.defineProperty(child, 'offsetLeft', { value: 500 * (i + 1) });
447
+ });
448
+
449
+ act(() => {
450
+ if (ref.current !== null) {
451
+ ref.current.scrollToSlide(2, 'smooth');
452
+ }
453
+ });
454
+
455
+ await waitFor(() => {
456
+ expect(scrollToSpy).toHaveBeenCalledWith({
457
+ behavior: 'smooth',
458
+ left: 1500,
459
+ });
460
+ });
461
+ });
462
+
463
+ it('scrolls to a specific slide vertically', async () => {
464
+ const ref = React.createRef<SliderTypes.API>();
465
+ render(<Slider ref={ref} orientation={Orientation.VERTICAL}>
466
+ <span key={1}/>
467
+ <span key={2}/>
468
+ <span key={3}/>
469
+ <span key={4}/>
470
+ </Slider>);
471
+
472
+ const slides = screen.getAllByRole('listitem');
473
+
474
+ slides.forEach((child, i) => {
475
+ Object.defineProperty(child, 'clientHeight', { configurable: true, value: 500 });
476
+ Object.defineProperty(child, 'offsetTop', { value: 500 * (i + 1) });
477
+ });
478
+
479
+ act(() => {
480
+ if (ref.current !== null) {
481
+ ref.current.scrollToSlide(2, 'smooth');
482
+ }
483
+ });
484
+
485
+ await waitFor(() => {
486
+ expect(scrollToSpy).toHaveBeenCalledWith({
487
+ behavior: 'smooth',
488
+ top: 1500,
304
489
  });
305
490
  });
306
491
  });
307
492
  });
308
493
 
494
+
309
495
  describe('initialSlideIndex', () => {
310
496
  it('Opens the slider with the initialSlide', async () => {
311
497
  render(<Slider initialSlideIndex={2}>
@@ -341,7 +527,7 @@ describe('UpsellSlider', () => {
341
527
 
342
528
  describe('visible slide indexes', () => {
343
529
  it('retrieves the first and last fully visible slide indices', () => {
344
- const ref = React.createRef<API>();
530
+ const ref = React.createRef<SliderTypes.API>();
345
531
  render(<Slider ref={ref}>
346
532
  <span key={1}/>
347
533
  <span key={2}/>
@@ -367,52 +553,5 @@ describe('UpsellSlider', () => {
367
553
  expect(ref.current!.getLastFullyVisibleSlideIndex()).toBe(2);
368
554
  });
369
555
  });
370
-
371
- describe('scrollToSlide', () => {
372
- it('scrolls to the next slide', async () => {
373
- const ref = React.createRef<API>();
374
- render(<Slider ref={ref}>
375
- <span key={1}/>
376
- <span key={2}/>
377
- <span key={3}/>
378
- <span key={4}/>
379
- </Slider>);
380
-
381
- const slides = screen.getAllByRole('listitem');
382
-
383
- slides.forEach((child, i) => {
384
- Object.defineProperty(child, 'clientWidth', { configurable: true, value: 500 });
385
- Object.defineProperty(child, 'offsetLeft', { value: 500 * (i + 1) });
386
- });
387
-
388
- act(() => {
389
- if (ref.current !== null) {
390
- ref.current.scrollToSlide(2, 'smooth');
391
- }
392
- });
393
-
394
- await waitFor(() => {
395
- expect(scrollToSpy).toHaveBeenCalledWith({
396
- behavior: 'smooth',
397
- left: 1500,
398
- top: 0,
399
- });
400
- });
401
- });
402
- });
403
-
404
- describe('initialSlideIndex', () => {
405
- it('Opens the slider with the initialSlide', async () => {
406
- render(<Slider initialSlideIndex={2}>
407
- <span key={ 1 } data-testid="child-1"/>
408
- <span key={ 2 } data-testid="child-2"/>
409
- <span key={ 3 } data-testid="child-3"/>
410
- <span key={ 4 } data-testid="child-4"/>
411
- </Slider>);
412
-
413
- await waitFor(() => {
414
- expect(scrollToSpy).toHaveBeenCalledTimes(1);
415
- });
416
- });
417
- });
418
556
  });
557
+
package/src/Slider.tsx CHANGED
@@ -1,6 +1,15 @@
1
1
  import classNames from 'classnames';
2
2
  import type { MouseEvent as ReactMouseEvent } from 'react';
3
- import { Children, forwardRef, PropsWithChildren, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
3
+ import {
4
+ Children,
5
+ forwardRef,
6
+ PropsWithChildren,
7
+ useCallback,
8
+ useEffect,
9
+ useImperativeHandle,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
4
13
  import { NextButton } from './Components/Controls/NextButton';
5
14
  import { PreviousButton } from './Components/Controls/PreviousButton';
6
15
  import { NavigationDirection, useSlider, Visibility } from './Hooks/UseSlider';
@@ -13,12 +22,16 @@ export namespace SliderTypes {
13
22
  getLastFullyVisibleSlideIndex(): number;
14
23
  }
15
24
  }
16
-
25
+ export enum Orientation {
26
+ HORIZONTAL = 'horizontal',
27
+ VERTICAL = 'vertical',
28
+ }
17
29
  interface Settings {
18
30
  // Sets whether the navigation buttons (next/prev) are no longer rendered
19
31
  hideNavigationButtons?: boolean;
20
32
  initialSlideIndex?: number;
21
33
  onSlide?: () => void;
34
+ orientation?: Orientation,
22
35
  }
23
36
 
24
37
  interface SlideVisibilityEntry {
@@ -26,7 +39,13 @@ interface SlideVisibilityEntry {
26
39
  visibility: Visibility;
27
40
  }
28
41
 
29
- export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>(({ children, hideNavigationButtons = false, initialSlideIndex = 0, onSlide = () => null }, ref) => {
42
+ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>(({
43
+ children,
44
+ hideNavigationButtons = false,
45
+ initialSlideIndex = 0,
46
+ onSlide = () => null,
47
+ orientation = Orientation.HORIZONTAL,
48
+ }, ref) => {
30
49
  const slides = useRef<SlideVisibilityEntry[]>([]);
31
50
  const wrapper = useRef<HTMLDivElement | null>(null);
32
51
 
@@ -36,13 +55,21 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
36
55
  const [isScrollable, setIsScrollable] = useState<boolean>(false);
37
56
  const [isDragging, setIsDragging] = useState<boolean>(false);
38
57
  const [isBlockingClicks, setIsBlockingClicks] = useState<boolean>(false);
39
- const [mousePosition, setMousePosition] = useState<{ clientX: number; scrollX: number }>({
58
+
59
+ const [mousePosition, setMousePosition] = useState<{
60
+ clientX: number;
61
+ clientY: number
62
+ scrollX: number;
63
+ scrollY: number;
64
+ }>({
40
65
  clientX: 0,
66
+ clientY: 0,
41
67
  scrollX: 0,
68
+ scrollY: 0,
42
69
  });
43
70
 
44
71
  const {
45
- getLeftPositionToScrollTo,
72
+ getPositionToScrollTo,
46
73
  getVisibilityByIntersectionRatio,
47
74
  addVisibleSlide,
48
75
  addPartiallyVisibleSlide,
@@ -51,6 +78,7 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
51
78
  getFirstVisibleSlideIndex,
52
79
  removeVisibleSlide,
53
80
  removePartiallyVisibleSlide,
81
+ shouldBlockClicks,
54
82
  } = useSlider();
55
83
 
56
84
  const blockChildClickHandler = (event: ReactMouseEvent<HTMLDivElement>) => {
@@ -68,7 +96,9 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
68
96
  setMousePosition({
69
97
  ...mousePosition,
70
98
  clientX: event.clientX,
99
+ clientY: event.clientY,
71
100
  scrollX: wrapper.current?.scrollLeft ?? 0,
101
+ scrollY: wrapper.current?.scrollTop ?? 0,
72
102
  });
73
103
 
74
104
  setIsDragging(true);
@@ -81,11 +111,22 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
81
111
  return;
82
112
  }
83
113
 
84
- if (Math.abs(mousePosition.clientX - event.clientX) > 5) {
85
- setIsBlockingClicks(true);
86
- }
114
+ switch (orientation) {
115
+ case Orientation.HORIZONTAL:
116
+ if (shouldBlockClicks(mousePosition.clientX - event.clientX)) {
117
+ setIsBlockingClicks(true);
118
+ }
119
+
120
+ currentWrapper.scrollLeft = mousePosition.scrollX + mousePosition.clientX - event.clientX;
121
+ break;
122
+ case Orientation.VERTICAL:
123
+ if (shouldBlockClicks(mousePosition.clientY - event.clientY)) {
124
+ setIsBlockingClicks(true);
125
+ }
87
126
 
88
- currentWrapper.scrollLeft = mousePosition.scrollX + mousePosition.clientX - event.clientX;
127
+ currentWrapper.scrollTop = mousePosition.scrollY + mousePosition.clientY - event.clientY;
128
+ break;
129
+ }
89
130
  };
90
131
 
91
132
  const addSlide = (node: HTMLDivElement, index: number) => {
@@ -103,27 +144,51 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
103
144
  return;
104
145
  }
105
146
 
106
- const direction = (index > getFirstVisibleSlideIndex()) ? NavigationDirection.NEXT : NavigationDirection.PREV;
147
+ const navDirection = (index >= getFirstVisibleSlideIndex()) ? NavigationDirection.NEXT : NavigationDirection.PREV;
148
+
149
+ let scrollLeft = undefined;
150
+ let scrollTop = undefined;
151
+
152
+ switch (orientation) {
153
+ case Orientation.HORIZONTAL:
154
+ scrollLeft = getPositionToScrollTo(
155
+ navDirection,
156
+ targetSlide.element.offsetLeft,
157
+ currentWrapper.offsetLeft,
158
+ currentWrapper.clientWidth,
159
+ targetSlide.element.clientWidth,
160
+ );
161
+ break;
162
+ case Orientation.VERTICAL:
163
+ scrollTop = getPositionToScrollTo(
164
+ navDirection,
165
+ targetSlide.element.offsetTop,
166
+ currentWrapper.offsetTop,
167
+ currentWrapper.clientHeight,
168
+ targetSlide.element.clientHeight,
169
+ );
170
+ break;
171
+ }
172
+
173
+ const scrollOptions: Partial<ScrollToOptions> = {
174
+ behavior,
175
+ ...(Number.isInteger(scrollLeft) && { left: scrollLeft } ),
176
+ ...(Number.isInteger(scrollTop) && { top: scrollTop } ),
177
+ };
107
178
 
108
- const scrollLeft = getLeftPositionToScrollTo(
109
- direction,
110
- targetSlide.element.offsetLeft,
111
- currentWrapper.offsetLeft,
112
- currentWrapper.clientWidth,
113
- targetSlide.element.clientWidth,
114
- );
115
179
 
116
- currentWrapper.scrollTo({ behavior, left: scrollLeft, top: 0 });
180
+ currentWrapper.scrollTo(scrollOptions);
117
181
  };
118
182
 
119
- const navigate = (direction: NavigationDirection) => {
183
+ const navigate = (navDirection: NavigationDirection) => {
120
184
  const currentWrapper = wrapper.current;
121
185
 
122
186
  if (!currentWrapper) {
123
187
  return;
124
188
  }
125
189
 
126
- const targetSlideIndex = direction === NavigationDirection.PREV ? getFirstVisibleSlideIndex() - 1 : getLastVisibleSlideIndex() + 1;
190
+ const targetSlideIndex = navDirection === NavigationDirection.PREV ? getFirstVisibleSlideIndex() - 1 : getLastVisibleSlideIndex() + 1;
191
+
127
192
  scrollToSlide(targetSlideIndex, 'smooth');
128
193
  };
129
194
 
@@ -141,7 +206,7 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
141
206
  return () => {};
142
207
  }
143
208
 
144
- const checkScrollable = () => setIsScrollable(currentWrapper.scrollWidth > currentWrapper.clientWidth);
209
+ const checkScrollable = () => setIsScrollable(orientation === Orientation.VERTICAL ? currentWrapper.scrollHeight > currentWrapper.clientHeight : currentWrapper.scrollWidth > currentWrapper.clientWidth);
145
210
 
146
211
  const scrollToInitialSlide = () => {
147
212
  if (initialSlideIndex !== 0) {
@@ -151,9 +216,25 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
151
216
  return;
152
217
  }
153
218
 
154
- const scrollLeft = targetSlide.element.offsetLeft - currentWrapper.offsetLeft;
219
+ let scrollLeft = undefined;
220
+ let scrollTop = undefined;
221
+
222
+ switch (orientation) {
223
+ case Orientation.HORIZONTAL:
224
+ scrollLeft = targetSlide.element.offsetLeft - currentWrapper.offsetLeft;
225
+ break;
226
+ case Orientation.VERTICAL:
227
+ scrollTop = targetSlide.element.offsetTop - currentWrapper.offsetTop;
228
+ break;
229
+ }
230
+
231
+ const scrollOptions: Partial<ScrollToOptions> = {
232
+ behavior: 'instant',
233
+ ...(Number.isInteger(scrollLeft) && { left: scrollLeft } ),
234
+ ...(Number.isInteger(scrollTop) && { top: scrollTop } ),
235
+ };
155
236
 
156
- currentWrapper.scrollTo({ behavior: 'instant', left: scrollLeft, top: 0 });
237
+ currentWrapper.scrollTo(scrollOptions);
157
238
  }
158
239
  };
159
240
 
@@ -165,7 +246,7 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
165
246
  return () => {
166
247
  window?.removeEventListener('resize', checkScrollable);
167
248
  };
168
- }, [wrapper, initialSlideIndex]);
249
+ }, [wrapper, initialSlideIndex, orientation]);
169
250
 
170
251
  useEffect(() => {
171
252
  const onDocumentMouseUp = (event: MouseEvent) => {
@@ -246,8 +327,10 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
246
327
  onMouseUp={mouseUpHandler}
247
328
  onClickCapture={blockChildClickHandler}
248
329
  className={classNames('slider__wrapper', {
249
- 'is-scrollable': isScrollable,
250
- 'is-dragging': isDragging,
330
+ 'slider__wrapper--is-scrollable': isScrollable,
331
+ 'slider__wrapper--is-dragging': isDragging,
332
+ 'slider__wrapper--is-horizontal': orientation === Orientation.HORIZONTAL,
333
+ 'slider__wrapper--is-vertical': orientation === Orientation.VERTICAL,
251
334
  })}
252
335
  >
253
336
  {Children.map(children, (child, index: number) => (
@@ -258,8 +341,8 @@ export const Slider = forwardRef<SliderTypes.API, PropsWithChildren<Settings>>((
258
341
  </div>
259
342
  { !hideNavigationButtons && (
260
343
  <>
261
- <PreviousButton onClick={() => navigate(NavigationDirection.PREV)} isHidden={!prevArrowVisible}/>
262
- <NextButton onClick={() => navigate(NavigationDirection.NEXT)} isHidden={!nextArrowVisible}/>
344
+ <PreviousButton onClick={() => navigate(NavigationDirection.PREV)} isHidden={!prevArrowVisible} direction={orientation}/>
345
+ <NextButton onClick={() => navigate(NavigationDirection.NEXT)} isHidden={!nextArrowVisible} direction={orientation}/>
263
346
  </>
264
347
  )}
265
348
  </div>