@yoursurprise/slider 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/Components/Controls/NextButton.d.ts +2 -0
- package/dist/Components/Controls/PreviousButton.d.ts +2 -0
- package/dist/Hooks/UseSlider.d.ts +2 -1
- package/dist/Slider.d.ts +5 -0
- package/dist/index.css +41 -12
- package/dist/index.css.map +1 -1
- package/dist/index.js +90 -29
- package/dist/index.js.map +1 -1
- package/dist/module.js +90 -29
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/Components/Controls/Button.scss +32 -4
- package/src/Components/Controls/NextButton.tsx +9 -3
- package/src/Components/Controls/PreviousButton.tsx +9 -3
- package/src/Hooks/UseSider.test.ts +8 -2
- package/src/Hooks/UseSlider.ts +13 -7
- package/src/Slider.scss +23 -15
- package/src/Slider.test.tsx +225 -86
- package/src/Slider.tsx +111 -28
package/src/Slider.test.tsx
CHANGED
|
@@ -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 {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
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('
|
|
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(
|
|
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('
|
|
82
|
-
renderSliderWithDimensions(
|
|
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')).
|
|
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
|
-
|
|
129
|
-
fireEvent.
|
|
130
|
-
|
|
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
|
-
|
|
148
|
-
fireEvent.
|
|
149
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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>>(({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
180
|
+
currentWrapper.scrollTo(scrollOptions);
|
|
117
181
|
};
|
|
118
182
|
|
|
119
|
-
const navigate = (
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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>
|