@transferwise/components 0.0.0-experimental-358e009 → 0.0.0-experimental-220b657

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.
Files changed (56) hide show
  1. package/build/inputs/SelectInput.js +32 -1
  2. package/build/inputs/SelectInput.js.map +1 -1
  3. package/build/inputs/SelectInput.mjs +32 -1
  4. package/build/inputs/SelectInput.mjs.map +1 -1
  5. package/build/main.css +97 -77
  6. package/build/prompt/InlinePrompt/InlinePrompt.js +8 -10
  7. package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
  8. package/build/prompt/InlinePrompt/InlinePrompt.mjs +9 -11
  9. package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
  10. package/build/prompt/PrimitivePrompt/PrimitivePrompt.js +45 -0
  11. package/build/prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -0
  12. package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs +43 -0
  13. package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -0
  14. package/build/styles/main.css +97 -77
  15. package/build/styles/prompt/InlinePrompt/InlinePrompt.css +2 -23
  16. package/build/styles/prompt/PrimitivePrompt/PrimitivePrompt.css +41 -0
  17. package/build/tabs/Tabs.js +171 -14
  18. package/build/tabs/Tabs.js.map +1 -1
  19. package/build/tabs/Tabs.mjs +173 -16
  20. package/build/tabs/Tabs.mjs.map +1 -1
  21. package/build/tabs/utils.js +18 -0
  22. package/build/tabs/utils.js.map +1 -1
  23. package/build/tabs/utils.mjs +17 -1
  24. package/build/tabs/utils.mjs.map +1 -1
  25. package/build/types/inputs/SelectInput.d.ts +2 -1
  26. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  27. package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts +1 -1
  28. package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -1
  29. package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts +30 -0
  30. package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -0
  31. package/build/types/prompt/PrimitivePrompt/index.d.ts +3 -0
  32. package/build/types/prompt/PrimitivePrompt/index.d.ts.map +1 -0
  33. package/build/types/tabs/Tabs.d.ts +24 -2
  34. package/build/types/tabs/Tabs.d.ts.map +1 -1
  35. package/build/types/tabs/utils.d.ts +2 -0
  36. package/build/types/tabs/utils.d.ts.map +1 -1
  37. package/package.json +2 -1
  38. package/src/inputs/SelectInput.spec.tsx +54 -0
  39. package/src/inputs/SelectInput.story.tsx +68 -0
  40. package/src/inputs/SelectInput.tsx +57 -6
  41. package/src/listItem/ListItem.spec.tsx +6 -8
  42. package/src/main.css +97 -77
  43. package/src/main.less +2 -1
  44. package/src/prompt/InlinePrompt/InlinePrompt.css +2 -23
  45. package/src/prompt/InlinePrompt/InlinePrompt.less +3 -18
  46. package/src/prompt/InlinePrompt/InlinePrompt.spec.tsx +6 -6
  47. package/src/prompt/InlinePrompt/InlinePrompt.tsx +6 -7
  48. package/src/prompt/PrimitivePrompt/PrimitivePrompt.css +41 -0
  49. package/src/prompt/PrimitivePrompt/PrimitivePrompt.less +37 -0
  50. package/src/prompt/PrimitivePrompt/PrimitivePrompt.tsx +70 -0
  51. package/src/prompt/PrimitivePrompt/index.ts +2 -0
  52. package/src/tabs/Tabs.spec.tsx +22 -0
  53. package/src/tabs/Tabs.story.tsx +45 -1
  54. package/src/tabs/Tabs.tsx +240 -23
  55. package/src/tabs/utils.spec.ts +58 -0
  56. package/src/tabs/utils.ts +20 -0
package/src/tabs/Tabs.tsx CHANGED
@@ -1,18 +1,21 @@
1
+ import { Spring, animated } from '@react-spring/web';
1
2
  import { clsx } from 'clsx';
2
3
  import clamp from 'lodash.clamp';
3
- import { Component, createRef } from 'react';
4
+ import { Component, createRef, Fragment } from 'react';
4
5
 
5
- import { Width, Direction } from '../common';
6
+ import { Size, Width, Direction } from '../common';
6
7
  import { DirectionContext } from '../provider/direction';
7
8
 
8
9
  import Tab from './Tab';
9
10
  import TabList from './TabList';
10
11
  import TabPanel from './TabPanel';
11
12
  import {
13
+ getElasticDragDifference,
12
14
  getSwipeDifference,
13
15
  swipedLeftToRight,
14
16
  swipedRightToLeft,
15
17
  swipeShouldChangeTab,
18
+ getVelocity,
16
19
  Swipe,
17
20
  } from './utils';
18
21
 
@@ -24,29 +27,52 @@ export interface TabItem {
24
27
  disabled: boolean;
25
28
  }
26
29
 
30
+ type TabsTransitionSpacing =
31
+ | 'default'
32
+ | `${Size.EXTRA_SMALL | Size.SMALL | Size.MEDIUM | Size.LARGE}`;
33
+
27
34
  export interface TabsProps {
28
35
  tabs: TabItem[];
29
36
  selected: number;
30
37
  name: string;
38
+ animatePanelsOnClick?: boolean;
31
39
  changeTabOnSwipe?: boolean;
32
40
  className?: string;
41
+ transitionSpacing?: TabsTransitionSpacing;
33
42
  headerWidth?: `${Width}`;
43
+ id?: string;
34
44
  onTabSelect: (index: number) => void;
35
45
  }
36
46
 
37
47
  interface TabsState {
38
48
  start: Swipe | null;
49
+ translateX: number;
50
+ translateFrom: number;
51
+ translateTo: number;
39
52
  translateLineX: string | null;
53
+ isAnimating: boolean;
40
54
  isSwiping: boolean;
41
55
  isScrolling: boolean;
56
+ lastSwipeVelocity: number;
42
57
  fullWidthTabs: boolean;
58
+ currentSwipe: Swipe[];
59
+ selectedTabIndex: number;
43
60
  }
44
61
 
62
+ const SpacerWidth = {
63
+ default: 0,
64
+ xs: 8,
65
+ sm: 16,
66
+ md: 24,
67
+ lg: 32,
68
+ } satisfies Record<TabsTransitionSpacing, number>;
69
+
45
70
  export default class Tabs extends Component<TabsProps, TabsState> {
46
71
  declare props: TabsProps & Required<Pick<TabsProps, keyof typeof Tabs.defaultProps>>;
47
72
 
48
73
  static defaultProps = {
49
74
  changeTabOnSwipe: true,
75
+ transitionSpacing: 'default',
50
76
  headerWidth: Width.BLOCK,
51
77
  } satisfies Partial<TabsProps>;
52
78
 
@@ -56,10 +82,17 @@ export default class Tabs extends Component<TabsProps, TabsState> {
56
82
  super(props);
57
83
  this.state = {
58
84
  start: null,
85
+ translateX: 0,
86
+ translateFrom: 0,
87
+ translateTo: 0,
59
88
  translateLineX: null,
89
+ isAnimating: false,
60
90
  isSwiping: false,
61
91
  isScrolling: false,
92
+ lastSwipeVelocity: 0,
62
93
  fullWidthTabs: props.headerWidth === Width.BLOCK,
94
+ currentSwipe: [],
95
+ selectedTabIndex: props.selected,
63
96
  };
64
97
  }
65
98
 
@@ -69,6 +102,10 @@ export default class Tabs extends Component<TabsProps, TabsState> {
69
102
 
70
103
  tabRefs: (HTMLLIElement | null)[] = [];
71
104
 
105
+ get filteredTabsLength() {
106
+ return this.props.tabs.filter((tab) => !tab.disabled).length;
107
+ }
108
+
72
109
  get MAX_INDEX() {
73
110
  return this.props.tabs.length - 1;
74
111
  }
@@ -78,13 +115,13 @@ export default class Tabs extends Component<TabsProps, TabsState> {
78
115
 
79
116
  this.setTabWidth();
80
117
  this.switchTab(clamp(selected, MIN_INDEX, this.MAX_INDEX));
81
- this.animateLine(clamp(selected, MIN_INDEX, this.MAX_INDEX));
118
+ this.animateToTab(clamp(selected, MIN_INDEX, this.MAX_INDEX), true);
82
119
  document.body.addEventListener('touchmove', this.disableScroll, { passive: false });
83
120
  document.body.addEventListener('touchforcechange', this.disableScroll, { passive: false });
84
121
  window.addEventListener('resize', this.handleResize);
85
122
  }
86
123
 
87
- componentDidUpdate(previousProps: TabsProps) {
124
+ componentDidUpdate(previousProps: TabsProps, previousState: TabsState) {
88
125
  const currentSelected = this.props.selected;
89
126
  const previousSelected = previousProps.selected;
90
127
  const currentSelectedTab = this.props.tabs[currentSelected];
@@ -97,6 +134,8 @@ export default class Tabs extends Component<TabsProps, TabsState> {
97
134
  const previousDisabledTabsLength = previousProps.tabs.filter((tab) => !tab.disabled).length;
98
135
  const currentHeaderWidth = this.props.headerWidth;
99
136
  const previousFullHeaderWidth = previousProps.headerWidth;
137
+ const { animatePanelsOnClick } = this.props;
138
+ const instantOnClick = !animatePanelsOnClick && !previousState.isSwiping;
100
139
 
101
140
  if (
102
141
  currentHeaderWidth !== previousFullHeaderWidth ||
@@ -110,7 +149,10 @@ export default class Tabs extends Component<TabsProps, TabsState> {
110
149
  currentDisabledTabsLength !== previousDisabledTabsLength ||
111
150
  currentSelectedTabIsDisabled !== previousSelectedTabIsDisabled
112
151
  ) {
113
- this.animateLine(clamp(currentSelected, MIN_INDEX, this.MAX_INDEX));
152
+ this.animateToTab(
153
+ clamp(currentSelected, MIN_INDEX, this.MAX_INDEX),
154
+ currentSelected === previousSelected || instantOnClick,
155
+ );
114
156
  }
115
157
  }
116
158
 
@@ -122,6 +164,10 @@ export default class Tabs extends Component<TabsProps, TabsState> {
122
164
 
123
165
  handleResize = () => {
124
166
  this.setContainerWidth(this.container);
167
+
168
+ this.setState(({ selectedTabIndex }) => ({
169
+ translateTo: -(this.containerWidth * selectedTabIndex),
170
+ }));
125
171
  };
126
172
 
127
173
  setContainerRefAndWidth = (node: HTMLDivElement | null) => {
@@ -226,11 +272,51 @@ export default class Tabs extends Component<TabsProps, TabsState> {
226
272
 
227
273
  swipedOverHalfOfContainer = (difference: number) => difference / this.containerWidth >= 0.5;
228
274
 
275
+ calculateApplicableDragDifference = ({
276
+ currentSelected,
277
+ nextSelected,
278
+ start,
279
+ end,
280
+ }: {
281
+ currentSelected: number;
282
+ nextSelected: number;
283
+ start: Swipe;
284
+ end: Swipe;
285
+ }) => {
286
+ const difference = getSwipeDifference(start, end);
287
+ const elasticDrag = getElasticDragDifference(difference);
288
+
289
+ if (swipedLeftToRight(start, end)) {
290
+ if (currentSelected > MIN_INDEX && currentSelected !== nextSelected) {
291
+ return Math.min(difference, this.containerWidth);
292
+ }
293
+ return elasticDrag;
294
+ }
295
+ if (swipedRightToLeft(start, end)) {
296
+ if (currentSelected < this.MAX_INDEX && currentSelected !== nextSelected) {
297
+ return -Math.min(difference, this.containerWidth);
298
+ }
299
+ return -elasticDrag;
300
+ }
301
+
302
+ return false;
303
+ };
304
+
229
305
  switchTab = (index: number) => {
230
306
  const { onTabSelect } = this.props;
231
307
  onTabSelect(index);
232
308
  };
233
309
 
310
+ getTabIndexWithoutDisabledTabs(index: number) {
311
+ return index - this.props.tabs.slice(0, index).filter((tab) => tab.disabled).length;
312
+ }
313
+
314
+ animateToTab = (index: number, instant?: boolean) => {
315
+ this.animateLine(index);
316
+
317
+ this.animatePanel(this.getTabIndexWithoutDisabledTabs(index), instant);
318
+ };
319
+
234
320
  animateLine = (index: number) => {
235
321
  this.setState((previousState) => ({
236
322
  translateLineX: previousState.fullWidthTabs
@@ -239,6 +325,21 @@ export default class Tabs extends Component<TabsProps, TabsState> {
239
325
  }));
240
326
  };
241
327
 
328
+ // Pass `instant` to set the `translateX` to the new panel with no transition
329
+ animatePanel = (index: number, instant = false) => {
330
+ const { translateTo: currentTranslateTo } = this.state;
331
+
332
+ const translateFrom = currentTranslateTo;
333
+ const translateTo = -(this.containerWidth * index);
334
+
335
+ this.setState({
336
+ selectedTabIndex: index,
337
+ isAnimating: !instant && translateFrom !== translateTo,
338
+ translateFrom,
339
+ translateTo,
340
+ });
341
+ };
342
+
242
343
  disableScroll = (event: Event) => {
243
344
  const { isSwiping } = this.state;
244
345
 
@@ -265,7 +366,10 @@ export default class Tabs extends Component<TabsProps, TabsState> {
265
366
  };
266
367
  this.setState({
267
368
  start,
369
+ currentSwipe: [start],
268
370
  });
371
+
372
+ event.persist();
269
373
  };
270
374
 
271
375
  handleTouchMove: React.TouchEventHandler<HTMLDivElement> = (event) => {
@@ -275,6 +379,7 @@ export default class Tabs extends Component<TabsProps, TabsState> {
275
379
  }
276
380
 
277
381
  const { selected: currentSelectedFromProps } = this.props;
382
+ const selected = this.getTabIndexWithoutDisabledTabs(currentSelectedFromProps);
278
383
  const end: Swipe = {
279
384
  x: event.nativeEvent.changedTouches[0].clientX,
280
385
  y: event.nativeEvent.changedTouches[0].clientY,
@@ -284,6 +389,8 @@ export default class Tabs extends Component<TabsProps, TabsState> {
284
389
  const yAxisDifference = getSwipeDifference(start, end, 'y');
285
390
  let { isScrolling, isSwiping } = this.state;
286
391
 
392
+ event.persist();
393
+
287
394
  if (!isScrolling && !isSwiping) {
288
395
  if (difference > yAxisDifference) {
289
396
  isSwiping = true;
@@ -299,6 +406,23 @@ export default class Tabs extends Component<TabsProps, TabsState> {
299
406
  this.animateLine(
300
407
  this.swipedOverHalfOfContainer(difference) ? nextSelected : currentSelectedFromProps,
301
408
  );
409
+
410
+ const dragDifference = this.calculateApplicableDragDifference({
411
+ currentSelected: currentSelectedFromProps,
412
+ nextSelected,
413
+ start,
414
+ end,
415
+ });
416
+
417
+ const translateX = dragDifference
418
+ ? -(this.containerWidth * selected) + dragDifference
419
+ : false;
420
+
421
+ this.setState((state) => ({
422
+ currentSwipe: [...state.currentSwipe, end],
423
+ translateFrom: translateX || state.translateFrom,
424
+ translateTo: translateX || state.translateTo,
425
+ }));
302
426
  }
303
427
  };
304
428
 
@@ -315,9 +439,16 @@ export default class Tabs extends Component<TabsProps, TabsState> {
315
439
  time: Date.now(),
316
440
  };
317
441
  const difference = getSwipeDifference(start, end);
442
+ const velocity = getVelocity([...this.state.currentSwipe, end]);
443
+
444
+ this.setState({
445
+ currentSwipe: [],
446
+ });
318
447
 
319
448
  let nextSelected = selected;
320
449
 
450
+ event.persist();
451
+
321
452
  if (isSwiping) {
322
453
  if (swipeShouldChangeTab(start, end) || this.swipedOverHalfOfContainer(difference)) {
323
454
  nextSelected = this.getTabToSelect(nextSelected, start, end);
@@ -325,8 +456,12 @@ export default class Tabs extends Component<TabsProps, TabsState> {
325
456
 
326
457
  if (nextSelected !== selected) {
327
458
  this.switchTab(nextSelected);
459
+
460
+ this.setState({
461
+ lastSwipeVelocity: velocity,
462
+ });
328
463
  } else {
329
- this.animateLine(nextSelected);
464
+ this.animateToTab(nextSelected);
330
465
  }
331
466
  }
332
467
 
@@ -334,10 +469,47 @@ export default class Tabs extends Component<TabsProps, TabsState> {
334
469
  };
335
470
 
336
471
  render() {
337
- const { tabs, changeTabOnSwipe, name, selected, className, headerWidth } = this.props;
338
- const { translateLineX, fullWidthTabs } = this.state;
339
-
340
- const selectedTab = tabs[selected];
472
+ const { tabs, changeTabOnSwipe, name, selected, className, transitionSpacing, headerWidth } =
473
+ this.props;
474
+ const {
475
+ isSwiping,
476
+ translateLineX,
477
+ isAnimating,
478
+ translateFrom,
479
+ translateTo,
480
+ lastSwipeVelocity,
481
+ fullWidthTabs,
482
+ } = this.state;
483
+
484
+ const spacer = SpacerWidth[transitionSpacing];
485
+
486
+ const tabsLength = this.filteredTabsLength;
487
+
488
+ const distanceSwiped = Math.abs(-translateFrom - this.containerWidth * selected);
489
+
490
+ const remainingContainerToTravel = isSwiping
491
+ ? 1 - distanceSwiped / this.containerWidth
492
+ : 1 - Math.abs(distanceSwiped / this.containerWidth - 1);
493
+
494
+ const restrictedVelocity =
495
+ (Number.isNaN(remainingContainerToTravel) ? 0 : remainingContainerToTravel) *
496
+ Math.min(10 * Math.E, lastSwipeVelocity * 10 * Math.E);
497
+
498
+ const hidePanelOverflow = isAnimating || isSwiping;
499
+
500
+ const sliderWidth = tabsLength * this.containerWidth + spacer * 2;
501
+
502
+ // Uses `props.panelTransitionSpacing` to add a spacer in-between the `TabPanel` you're transitioning to/from
503
+ const Spacer = ({ id }: { id: string }) =>
504
+ spacer > 0 && (
505
+ <div
506
+ key={id}
507
+ style={{
508
+ width: spacer,
509
+ display: hidePanelOverflow ? 'block' : 'none',
510
+ }}
511
+ />
512
+ );
341
513
 
342
514
  return (
343
515
  <DirectionContext.Consumer>
@@ -393,19 +565,64 @@ export default class Tabs extends Component<TabsProps, TabsState> {
393
565
  />
394
566
  ) : null}
395
567
  </TabList>
396
- <div ref={this.setContainerRefAndWidth} className="tabs__panel-container">
397
- {selectedTab && !selectedTab.disabled ? (
398
- <TabPanel
399
- key={selectedTab.title}
400
- tabId={`${name}-tab-${selected}`}
401
- id={`${name}-panel-${selected}`}
402
- style={{
403
- width: '100%',
404
- }}
405
- >
406
- {selectedTab.content}
407
- </TabPanel>
408
- ) : null}
568
+ <div
569
+ ref={this.setContainerRefAndWidth}
570
+ className="tabs__panel-container"
571
+ style={{
572
+ overflow: hidePanelOverflow ? 'hidden' : 'visible',
573
+ }}
574
+ >
575
+ <Spring
576
+ from={{
577
+ transform: `translateX(${translateFrom - spacer}px)`,
578
+ }}
579
+ to={{
580
+ transform: `translateX(${translateTo - spacer}px)`,
581
+ }}
582
+ config={{
583
+ precision: isSwiping ? 1 : 0.01,
584
+ velocity: !isSwiping ? restrictedVelocity : 0,
585
+ clamp: true,
586
+ }}
587
+ onRest={() => {
588
+ if (isAnimating) {
589
+ this.setState({
590
+ isAnimating: false,
591
+ lastSwipeVelocity: 0,
592
+ });
593
+ }
594
+ }}
595
+ >
596
+ {(props) => (
597
+ <animated.div
598
+ className="tabs__slider"
599
+ style={{
600
+ width: hidePanelOverflow ? `${sliderWidth}px` : '100%',
601
+ transform: hidePanelOverflow ? props.transform : 'translateX(0px)',
602
+ }}
603
+ >
604
+ {tabs.map(({ content, disabled }, index) =>
605
+ !disabled ? (
606
+ <Fragment key={`${tabs[index].title}-fragment`}>
607
+ {index === selected && <Spacer id="left-spacer" />}
608
+ <TabPanel
609
+ key={tabs[index].title}
610
+ tabId={`${name}-tab-${index}`}
611
+ id={`${name}-panel-${index}`}
612
+ style={{
613
+ width: hidePanelOverflow ? `${this.containerWidth}px` : '100%',
614
+ display: hidePanelOverflow || index === selected ? 'block' : 'none',
615
+ }}
616
+ >
617
+ {content}
618
+ </TabPanel>
619
+ {index === selected && <Spacer id="right-spacer" />}
620
+ </Fragment>
621
+ ) : null,
622
+ )}
623
+ </animated.div>
624
+ )}
625
+ </Spring>
409
626
  </div>
410
627
  </div>
411
628
  );
@@ -1,14 +1,17 @@
1
1
  import {
2
+ getElasticDragDifference,
2
3
  swipedLeftToRight,
3
4
  swipedRightToLeft,
4
5
  getSwipeDifference,
5
6
  swipeShouldChangeTab,
7
+ getVelocity,
6
8
  Swipe,
7
9
  } from './utils';
8
10
 
9
11
  describe('Tabs Utility', () => {
10
12
  let start: Swipe;
11
13
  let end: Swipe;
14
+ let coords: Swipe[];
12
15
 
13
16
  beforeEach(() => {
14
17
  jest.clearAllMocks();
@@ -65,4 +68,59 @@ describe('Tabs Utility', () => {
65
68
  expect(swipeShouldChangeTab(start, end)).toBe(false);
66
69
  });
67
70
  });
71
+
72
+ describe('checking for velocity against an array of touches', () => {
73
+ beforeEach(() => {
74
+ coords = [
75
+ { x: 0, y: 0, time: 1569538800000 },
76
+ { x: 100, y: 0, time: 1569538825000 },
77
+ { x: 200, y: 0, time: 1569538850000 },
78
+ { x: 300, y: 0, time: 1569538875000 },
79
+ { x: 400, y: 0, time: 1569538900000 },
80
+ ];
81
+ });
82
+
83
+ it('should return the difference in velocity between the first and last touch in an array', () => {
84
+ expect(getVelocity(coords)).toBe(0.004);
85
+ });
86
+
87
+ it('should only take into account the last 5 coordinates', () => {
88
+ coords.unshift(
89
+ { x: 10000, y: 0, time: 1569538800000 },
90
+ { x: 100000, y: 0, time: 1569538800000 },
91
+ );
92
+ expect(getVelocity(coords)).toBe(0.004);
93
+ });
94
+
95
+ it('returns a default 0 if there are any issues with the touches passed in', () => {
96
+ expect(getVelocity([])).toBe(0);
97
+ });
98
+ });
99
+
100
+ describe('applying elasticity to a drag event', () => {
101
+ it('gradually reduces the amount of difference returned toward a maximum distance', () => {
102
+ global.innerWidth = 375;
103
+ const maximumDraggableDistance = 125;
104
+
105
+ expect(getElasticDragDifference(0)).toBe(0);
106
+ expect(getApproximateElasticDragDifference(50)).toBe(28);
107
+ expect(getApproximateElasticDragDifference(100)).toBe(49);
108
+ expect(getApproximateElasticDragDifference(150)).toBe(66);
109
+ expect(getApproximateElasticDragDifference(200)).toBe(79);
110
+ expect(getApproximateElasticDragDifference(250)).toBe(89);
111
+ expect(getApproximateElasticDragDifference(300)).toBe(97);
112
+ expect(getApproximateElasticDragDifference(350)).toBe(103);
113
+
114
+ expect(getElasticDragDifference(10000)).toBe(maximumDraggableDistance);
115
+ });
116
+
117
+ it('limits the maximum dragable distance on larger screens', () => {
118
+ global.innerWidth = 1024;
119
+ const maximumDraggableDistance = 150;
120
+ expect(getElasticDragDifference(10000)).toBe(maximumDraggableDistance);
121
+ });
122
+ });
68
123
  });
124
+
125
+ const getApproximateElasticDragDifference = (difference: number) =>
126
+ Math.round(getElasticDragDifference(difference));
package/src/tabs/utils.ts CHANGED
@@ -35,3 +35,23 @@ export const swipeShouldChangeTab = (start: Swipe, end: Swipe) => {
35
35
 
36
36
  return swipedSignificantDistance(difference) && swipedWithSignificantVelocity(velocity);
37
37
  };
38
+
39
+ export function getVelocity(coords: Swipe[]) {
40
+ try {
41
+ const relevant = coords.slice(Math.max(coords.length - 5, 1));
42
+ const first = relevant[0];
43
+ const last = relevant[relevant.length - 1];
44
+
45
+ return Math.abs(first.x - last.x) / (last.time - first.time);
46
+ } catch {
47
+ return 0;
48
+ }
49
+ }
50
+
51
+ /*
52
+ `elasticDrag` is the translateX value, which slows down as the difference increases
53
+ `1 - Math.E ** (-0.005 * difference)` provides a % value of how far we want to translate (0.005 being the rate)
54
+ `Math.min(150, window.innerWidth / 3)` provides the maximum translate value
55
+ */
56
+ export const getElasticDragDifference = (difference: number) =>
57
+ Math.min(150, window.innerWidth / 3) * (1 - Math.E ** (-0.005 * difference));