@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.
- package/build/inputs/SelectInput.js +32 -1
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +32 -1
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/main.css +97 -77
- package/build/prompt/InlinePrompt/InlinePrompt.js +8 -10
- package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
- package/build/prompt/InlinePrompt/InlinePrompt.mjs +9 -11
- package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.js +45 -0
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -0
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs +43 -0
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -0
- package/build/styles/main.css +97 -77
- package/build/styles/prompt/InlinePrompt/InlinePrompt.css +2 -23
- package/build/styles/prompt/PrimitivePrompt/PrimitivePrompt.css +41 -0
- package/build/tabs/Tabs.js +171 -14
- package/build/tabs/Tabs.js.map +1 -1
- package/build/tabs/Tabs.mjs +173 -16
- package/build/tabs/Tabs.mjs.map +1 -1
- package/build/tabs/utils.js +18 -0
- package/build/tabs/utils.js.map +1 -1
- package/build/tabs/utils.mjs +17 -1
- package/build/tabs/utils.mjs.map +1 -1
- package/build/types/inputs/SelectInput.d.ts +2 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts +1 -1
- package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -1
- package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts +30 -0
- package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -0
- package/build/types/prompt/PrimitivePrompt/index.d.ts +3 -0
- package/build/types/prompt/PrimitivePrompt/index.d.ts.map +1 -0
- package/build/types/tabs/Tabs.d.ts +24 -2
- package/build/types/tabs/Tabs.d.ts.map +1 -1
- package/build/types/tabs/utils.d.ts +2 -0
- package/build/types/tabs/utils.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/inputs/SelectInput.spec.tsx +54 -0
- package/src/inputs/SelectInput.story.tsx +68 -0
- package/src/inputs/SelectInput.tsx +57 -6
- package/src/listItem/ListItem.spec.tsx +6 -8
- package/src/main.css +97 -77
- package/src/main.less +2 -1
- package/src/prompt/InlinePrompt/InlinePrompt.css +2 -23
- package/src/prompt/InlinePrompt/InlinePrompt.less +3 -18
- package/src/prompt/InlinePrompt/InlinePrompt.spec.tsx +6 -6
- package/src/prompt/InlinePrompt/InlinePrompt.tsx +6 -7
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.css +41 -0
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.less +37 -0
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.tsx +70 -0
- package/src/prompt/PrimitivePrompt/index.ts +2 -0
- package/src/tabs/Tabs.spec.tsx +22 -0
- package/src/tabs/Tabs.story.tsx +45 -1
- package/src/tabs/Tabs.tsx +240 -23
- package/src/tabs/utils.spec.ts +58 -0
- 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.
|
|
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.
|
|
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.
|
|
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 } =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
397
|
-
{
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
);
|
package/src/tabs/utils.spec.ts
CHANGED
|
@@ -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));
|