@cleartrip/ct-design-base-dragger 2.0.0-TEST.0 → 4.0.0-SNAPSHOT-test.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.
package/package.json CHANGED
@@ -1,25 +1,32 @@
1
1
  {
2
2
  "name": "@cleartrip/ct-design-base-dragger",
3
- "version": "2.0.0-TEST.0",
3
+ "version": "4.0.0-SNAPSHOT-test.0",
4
4
  "description": "A Base Component which can be wrapper to make a component draggable",
5
5
  "types": "dist/index.d.ts",
6
- "main": "dist/ct-design-base-dragger.cjs.js",
6
+ "main": "./dist/ct-design-base-dragger.cjs.js",
7
7
  "jsnext:main": "dist/ct-design-base-dragger.esm.js",
8
8
  "module": "dist/ct-design-base-dragger.esm.js",
9
9
  "sideEffects": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/ct-design-base-dragger.d.ts",
13
+ "import": "./dist/ct-design-base-dragger.esm.js",
14
+ "default": "./dist/ct-design-base-dragger.cjs.js"
15
+ }
16
+ },
10
17
  "browser": {
11
18
  "./dist/ct-design-base-dragger.esm.js": "./dist/ct-design-base-dragger.browser.esm.js",
12
19
  "./dist/ct-design-base-dragger.cjs.js": "./dist/ct-design-base-dragger.browser.cjs.js"
13
20
  },
14
21
  "files": [
15
- "dist"
22
+ "dist",
23
+ "src"
16
24
  ],
17
25
  "dependencies": {
18
- "@cleartrip/ct-design-container": "4.0.0-TEST.0"
19
- },
20
- "devDependencies": {
21
- "@cleartrip/ct-design-theme": "4.0.0-TEST.0"
26
+ "@cleartrip/ct-design-container": "4.0.0-SNAPSHOT-test.0",
27
+ "@cleartrip/ct-design-theme": "4.0.0-SNAPSHOT-test.0"
22
28
  },
29
+ "devDependencies": {},
23
30
  "peerDependencies": {
24
31
  "react": ">=16.8.0",
25
32
  "react-dom": ">=16.8.0"
@@ -33,6 +40,7 @@
33
40
  "test": "echo \"Error: no test specified\" && exit 1",
34
41
  "watch-package": "rollup -c -w",
35
42
  "build-package": "rollup -c",
36
- "build-package:clean": "rm -rf dist && rollup -c"
43
+ "build-package:clean": "rm -rf dist && rollup -c",
44
+ "publish-package:local": "yalc publish"
37
45
  }
38
46
  }
@@ -0,0 +1,243 @@
1
+ /* eslint-disable local-rules/disallow-style-config-inline */
2
+ import { useRef, useState, memo, useLayoutEffect, useCallback } from 'react';
3
+
4
+ import { Container } from '@cleartrip/ct-design-container';
5
+
6
+ import { useDrag } from './useDrag';
7
+ import BaseDraggerContext from './context/BaseDraggerContext';
8
+
9
+ import { IBaseDraggerProps, IDraggerDefaultState } from './type';
10
+
11
+ export const DRAGGABLE_CONTANTS = {
12
+ THRESHOLD_DIVIDER: 2,
13
+ ORIGIN: 1500,
14
+ VELOCITY: 0.4,
15
+ OFFSET: 250,
16
+ VELOCITY_DELTA_X: 10,
17
+ };
18
+
19
+ function BaseDragger({
20
+ isVisible,
21
+ children,
22
+ minDraggableHeight = 300,
23
+ minDraggableHeaderHeight,
24
+ maxDraggableHeight,
25
+ isClosable = false,
26
+ isDraggable = true,
27
+ onDragging: onDraggingCb,
28
+ onDragEnd: onDragEndCb,
29
+ isWrappedWithTransitionGroup = false,
30
+ }: IBaseDraggerProps) {
31
+ const ref = useRef<HTMLDivElement>(null);
32
+ const [defaultStyles, setDefaultStyles] = useState<IDraggerDefaultState | null>(null);
33
+ const draggableHeaderRef = useRef<(HTMLDivElement | null)[]>([]);
34
+
35
+ const debouncedStart = useRef(false);
36
+
37
+ const calculateDefaultStyles = useCallback(() => {
38
+ const windowInnerHeight = window.innerHeight;
39
+
40
+ const { height = 0 } = ref.current?.getBoundingClientRect() ?? {};
41
+ const manualTop = windowInnerHeight - height;
42
+
43
+ const maximumPossibleHeight = Math.min(
44
+ maxDraggableHeight ?? ref.current?.scrollHeight ?? 0,
45
+ (92 * windowInnerHeight) / 100,
46
+ );
47
+
48
+ const draggerHeaderHeight = draggableHeaderRef.current?.reduce(
49
+ (h, element) => h + (element?.getBoundingClientRect().height ?? 0),
50
+ 0,
51
+ );
52
+
53
+ const minimumHeightPossible =
54
+ minDraggableHeaderHeight ?? (draggableHeaderRef.current ? (draggerHeaderHeight ?? 0) : 0);
55
+ const currentHeight = maxDraggableHeight ?? height;
56
+
57
+ const maximumDeltaThatCanBeAddedToTop = currentHeight - maximumPossibleHeight;
58
+ const maximumDeltaCanBeAddedToBottom = currentHeight - minimumHeightPossible;
59
+
60
+ const deltaPoints = [
61
+ maximumDeltaThatCanBeAddedToTop + DRAGGABLE_CONTANTS.ORIGIN,
62
+ DRAGGABLE_CONTANTS.ORIGIN,
63
+ maximumDeltaCanBeAddedToBottom + DRAGGABLE_CONTANTS.ORIGIN,
64
+ ];
65
+
66
+ if (isClosable) {
67
+ deltaPoints.push(manualTop + DRAGGABLE_CONTANTS.ORIGIN);
68
+ }
69
+
70
+ setDefaultStyles({
71
+ maximumDeltaCanBeAddedToBottom,
72
+ maximumDeltaThatCanBeAddedToTop,
73
+ deltaPoints,
74
+ initialHeight: height ?? 0,
75
+ });
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps
77
+ }, [isVisible, maxDraggableHeight, isDraggable, isClosable]);
78
+
79
+ useLayoutEffect(() => {
80
+ const getStylesInFrames = () => {
81
+ if (ref.current) {
82
+ const initialTransition = ref.current.style.transition;
83
+
84
+ requestAnimationFrame(() => {
85
+ if (ref.current) {
86
+ const originalTransition = ref.current.style.transition;
87
+ const originalTransform = ref.current.style.transform;
88
+
89
+ ref.current.style.transform = '';
90
+ ref.current.style.opacity = '0';
91
+ ref.current.style.transition = '';
92
+
93
+ requestAnimationFrame(() => {
94
+ calculateDefaultStyles();
95
+
96
+ if (ref.current) {
97
+ ref.current.style.transform = originalTransform;
98
+ ref.current.style.transition = initialTransition;
99
+
100
+ requestAnimationFrame(() => {
101
+ if (ref.current) {
102
+ ref.current.style.opacity = '1';
103
+ ref.current.style.transition = originalTransition;
104
+ }
105
+ });
106
+ }
107
+ });
108
+ }
109
+ });
110
+ }
111
+ };
112
+
113
+ if (isDraggable && !isWrappedWithTransitionGroup) {
114
+ getStylesInFrames();
115
+ }
116
+ }, [isDraggable, isWrappedWithTransitionGroup, calculateDefaultStyles]);
117
+
118
+ const onReactTransitionGroupEnd = useCallback(() => {
119
+ calculateDefaultStyles();
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ }, []);
122
+
123
+ const { onDragStart, onDragging, onDragEnd, onCustomPositionChange } = useDrag({
124
+ onDragging(event) {
125
+ if (event && defaultStyles && !debouncedStart.current) {
126
+ const { deltaY: dy, initialDxDy, dir } = event;
127
+
128
+ if (dir === 'Left' || dir === 'Right') {
129
+ return;
130
+ }
131
+
132
+ const deltaY =
133
+ dy + initialDxDy[1] > 0
134
+ ? Math.min(defaultStyles.maximumDeltaCanBeAddedToBottom, dy + initialDxDy[1])
135
+ : -Math.min(Math.abs(defaultStyles.maximumDeltaThatCanBeAddedToTop), Math.abs(dy + initialDxDy[1]));
136
+
137
+ if (ref.current) {
138
+ ref.current.style.transform = `translateY(${deltaY}px) translateZ(0px)`;
139
+ }
140
+
141
+ if (onDraggingCb) {
142
+ onDraggingCb({ viewableHeight: defaultStyles.initialHeight + -deltaY });
143
+ }
144
+ }
145
+ },
146
+ onDragEnd(event) {
147
+ if (defaultStyles) {
148
+ const { deltaY: dy, initialDxDy, dir, deltaX, velocity, absX } = event;
149
+ const { deltaPoints } = defaultStyles;
150
+
151
+ const takeVelocityInConsideration = absX <= DRAGGABLE_CONTANTS.VELOCITY_DELTA_X;
152
+
153
+ if (initialDxDy) {
154
+ const deltaOrigin = dy + initialDxDy[1] + DRAGGABLE_CONTANTS.ORIGIN;
155
+ let destination = deltaPoints[0];
156
+
157
+ if (takeVelocityInConsideration && velocity >= DRAGGABLE_CONTANTS.VELOCITY) {
158
+ const jumpBy = Math.floor(velocity / DRAGGABLE_CONTANTS.VELOCITY);
159
+ let currentDeltaIndex = -1;
160
+
161
+ deltaPoints.forEach((x, index) => {
162
+ const change = dy + initialDxDy[1] + DRAGGABLE_CONTANTS.ORIGIN;
163
+
164
+ if (change >= x && dir === 'Down') {
165
+ currentDeltaIndex = index;
166
+ } else if (x >= change && dir === 'Up') {
167
+ currentDeltaIndex = index;
168
+ }
169
+ });
170
+
171
+ if (dir === 'Up') {
172
+ destination = deltaPoints[Math.max(0, currentDeltaIndex - jumpBy)];
173
+ } else {
174
+ destination = deltaPoints[Math.min(deltaPoints.length - 1, currentDeltaIndex + jumpBy)];
175
+ }
176
+ } else {
177
+ for (let i = 0; i < deltaPoints.length - 1; ++i) {
178
+ const l = deltaPoints[i],
179
+ r = deltaPoints[i + 1];
180
+
181
+ const upperLimit = l + (r - l) / DRAGGABLE_CONTANTS.THRESHOLD_DIVIDER;
182
+
183
+ if (deltaOrigin >= l && deltaOrigin <= r) {
184
+ if (dir === 'Up') {
185
+ destination = r;
186
+
187
+ if (deltaOrigin <= upperLimit) {
188
+ destination = l;
189
+ }
190
+ } else {
191
+ destination = l;
192
+
193
+ if (upperLimit <= deltaOrigin) {
194
+ destination = r;
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ if (deltaOrigin >= deltaPoints[deltaPoints.length - 1]) {
201
+ destination = deltaPoints[deltaPoints.length - 1];
202
+ }
203
+ }
204
+
205
+ onCustomPositionChange([deltaX, destination - DRAGGABLE_CONTANTS.ORIGIN]);
206
+
207
+ if (ref.current) {
208
+ ref.current.style.transform = `translateY(${destination - DRAGGABLE_CONTANTS.ORIGIN}px) translateZ(0px)`;
209
+ }
210
+ if (onDragEndCb) {
211
+ onDragEndCb({ viewableHeight: defaultStyles.initialHeight + -(destination - DRAGGABLE_CONTANTS.ORIGIN) });
212
+ }
213
+
214
+ debouncedStart.current = true;
215
+ setTimeout(() => {
216
+ debouncedStart.current = false;
217
+ }, 200);
218
+ }
219
+ }
220
+ },
221
+ });
222
+
223
+ return (
224
+ <Container
225
+ styleConfig={{
226
+ root: [
227
+ {
228
+ height: minDraggableHeight,
229
+ },
230
+ ],
231
+ }}
232
+ >
233
+ <BaseDraggerContext
234
+ {...(defaultStyles ? { onDragStart, onDragEnd, onDragging } : {})}
235
+ draggerHeaderRef={draggableHeaderRef}
236
+ >
237
+ {children({ ref, onReactTransitionGroupEnd })}
238
+ </BaseDraggerContext>
239
+ </Container>
240
+ );
241
+ }
242
+
243
+ export default memo(BaseDragger);
@@ -0,0 +1,21 @@
1
+ import { PropsWithChildren, memo, useCallback } from 'react';
2
+ import { useDraggableHeader } from './context/BaseDraggerContext';
3
+
4
+ export function BaseDraggerHeader({ children }: PropsWithChildren) {
5
+ const { draggerHeaderRef, onDragEnd, onDragStart, onDragging } = useDraggableHeader();
6
+
7
+ const refCallback = useCallback((node: HTMLDivElement) => {
8
+ if (node) {
9
+ draggerHeaderRef.current?.push(node);
10
+ }
11
+ // eslint-disable-next-line react-hooks/exhaustive-deps
12
+ }, []);
13
+
14
+ return (
15
+ <div onTouchStart={onDragStart} onTouchMove={onDragging} onTouchEnd={onDragEnd} ref={refCallback}>
16
+ {children}
17
+ </div>
18
+ );
19
+ }
20
+
21
+ export default memo(BaseDraggerHeader);
@@ -0,0 +1,39 @@
1
+ import { PropsWithChildren, createContext, memo, useContext, useMemo, RefObject } from 'react';
2
+ import { DragEvent } from '../useDrag';
3
+
4
+ const BaseDraggerProvider = createContext<IBaseDraggerConsumerValue>({} as IBaseDraggerConsumerValue);
5
+
6
+ export interface IBaseDraggerConsumerValue {
7
+ onDragStart?: DragEvent;
8
+ onDragging?: DragEvent;
9
+ onDragEnd?: DragEvent;
10
+ draggerHeaderRef: RefObject<(HTMLDivElement | null)[]>;
11
+ }
12
+
13
+ export interface IBaseDraggerContextProps extends PropsWithChildren, IBaseDraggerConsumerValue {}
14
+
15
+ function BaseDraggerContext({
16
+ children,
17
+ onDragEnd,
18
+ onDragStart,
19
+ onDragging,
20
+ draggerHeaderRef,
21
+ }: IBaseDraggerContextProps) {
22
+ const value = useMemo(() => {
23
+ return {
24
+ onDragStart,
25
+ onDragging,
26
+ onDragEnd,
27
+ draggerHeaderRef,
28
+ };
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, [onDragEnd, onDragging, onDragStart]);
31
+
32
+ return <BaseDraggerProvider.Provider value={value}>{children}</BaseDraggerProvider.Provider>;
33
+ }
34
+
35
+ export function useDraggableHeader() {
36
+ return useContext(BaseDraggerProvider);
37
+ }
38
+
39
+ export default memo(BaseDraggerContext);
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { default as BaseDragger } from './BaseDragger';
2
+ export { default as BaseDraggerHeader } from './BaseDraggerHeader';
3
+ export { useDrag } from './useDrag';
4
+ export * from './type';
package/src/type.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { MutableRefObject, ReactNode } from 'react';
2
+
3
+ export interface IBaseDraggerProps {
4
+ children: (props: {
5
+ ref: MutableRefObject<HTMLDivElement | null>;
6
+ onReactTransitionGroupEnd?: () => void;
7
+ }) => ReactNode;
8
+ isVisible?: boolean;
9
+ isDraggable?: boolean;
10
+ isClosable?: boolean;
11
+ maxDraggableHeight?: number;
12
+ minDraggableHeight?: number;
13
+ minDraggableHeaderHeight?: number;
14
+ onDragging?: (event: IDragCallbackArgs) => void;
15
+ onDragEnd?: (event: IDragCallbackArgs) => void;
16
+ isWrappedWithTransitionGroup?: boolean;
17
+ }
18
+
19
+ export interface IDragCallbackArgs {
20
+ viewableHeight: number;
21
+ }
22
+
23
+ export interface IDraggerDefaultState {
24
+ maximumDeltaThatCanBeAddedToTop: number;
25
+ maximumDeltaCanBeAddedToBottom: number;
26
+ deltaPoints: number[];
27
+ initialHeight: number;
28
+ }
package/src/useDrag.ts ADDED
@@ -0,0 +1,176 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+ import { TouchEvent, useCallback, useState } from 'react';
3
+
4
+ export type HandledEvents = TouchEvent<HTMLDivElement>;
5
+
6
+ const DRAGGER_INITIAL_STATE = {
7
+ initial: [0, 0],
8
+ start: 0,
9
+ xy: [0, 0],
10
+ dragging: false,
11
+ initialDxDy: [0, 0],
12
+ lastDxDy: [0, 0],
13
+ eventData: {} as EventData,
14
+ };
15
+
16
+ export const LEFT = 'Left';
17
+ export const RIGHT = 'Right';
18
+ export const UP = 'Up';
19
+ export const DOWN = 'Down';
20
+
21
+ export type SwipeDirections = typeof LEFT | typeof RIGHT | typeof UP | typeof DOWN;
22
+
23
+ export type EventData = {
24
+ absX: number;
25
+ absY: number;
26
+ deltaX: number;
27
+ deltaY: number;
28
+ dir: SwipeDirections;
29
+ event: HandledEvents;
30
+ initial: number[];
31
+ velocity: number;
32
+ clientY: number;
33
+ initialDxDy: number[];
34
+ };
35
+
36
+ function getDirection(absX: number, absY: number, deltaX: number, deltaY: number): SwipeDirections {
37
+ if (absX > absY) {
38
+ if (deltaX > 0) {
39
+ return RIGHT;
40
+ }
41
+ return LEFT;
42
+ } else if (deltaY > 0) {
43
+ return DOWN;
44
+ }
45
+ return UP;
46
+ }
47
+
48
+ export type DragEvent = (e: HandledEvents) => void;
49
+
50
+ export interface IDragProps {
51
+ onDragStart?: (e: EventData) => void;
52
+ onDragging?: (e: EventData) => void;
53
+ onDragEnd?: (e: EventData) => void;
54
+ }
55
+
56
+ export function useDrag(events: IDragProps = {}) {
57
+ const [state, setState] = useState(DRAGGER_INITIAL_STATE);
58
+
59
+ const onDragStart = useCallback((event: HandledEvents) => {
60
+ const isTouch = 'touches' in event || 'changedTouches' in event;
61
+
62
+ if (isTouch && (event.touches ?? event.changedTouches).length > 1) return;
63
+
64
+ setState((previous) => {
65
+ const { clientX, clientY } = isTouch ? (event.touches[0] ?? event.changedTouches[0]) : event;
66
+
67
+ return {
68
+ ...previous,
69
+ ...DRAGGER_INITIAL_STATE,
70
+ initialDxDy: previous.initialDxDy,
71
+ lastDxDy: previous.initialDxDy,
72
+ xy: [clientX, clientY],
73
+ start: event.timeStamp || 0,
74
+ dragging: true,
75
+ };
76
+ });
77
+ }, []);
78
+
79
+ const onDragging = useCallback(
80
+ (event: HandledEvents) => {
81
+ setState((state) => {
82
+ const isTouch = 'touches' in event || 'changedTouches' in event;
83
+
84
+ if (isTouch && (event.touches ?? event.changedTouches).length > 1) {
85
+ return state;
86
+ }
87
+
88
+ event.stopPropagation();
89
+ event.preventDefault();
90
+
91
+ const { clientX, clientY } = isTouch ? (event.touches[0] ?? event.changedTouches[0]) : event;
92
+
93
+ const deltaX = clientX - state.xy[0];
94
+ const deltaY = clientY - state.xy[1];
95
+ const absX = Math.abs(deltaX);
96
+ const absY = Math.abs(deltaY);
97
+ const time = (event.timeStamp || 0) - state.start;
98
+ const velocity = Math.sqrt(absX * absX + absY * absY) / (time || 1);
99
+ const dir = getDirection(absX, absY, deltaX, deltaY);
100
+
101
+ const lastDxDy = [state.initialDxDy[0] + deltaX, state.initialDxDy[1] + deltaY];
102
+
103
+ const eventData = {
104
+ absX,
105
+ absY,
106
+ deltaX,
107
+ deltaY,
108
+ clientY,
109
+ dir,
110
+ event,
111
+ initial: state.initial,
112
+ velocity,
113
+ initialDxDy: state.initialDxDy,
114
+ };
115
+
116
+ if (events.onDragging) events.onDragging(eventData);
117
+
118
+ return {
119
+ ...state,
120
+ eventData,
121
+ lastDxDy: lastDxDy,
122
+ };
123
+ });
124
+ },
125
+ [events],
126
+ );
127
+
128
+ const onCustomPositionChange = useCallback((positions: number[]) => {
129
+ setState((state) => {
130
+ return {
131
+ ...state,
132
+ initialDxDy: positions,
133
+ lastDxDy: positions,
134
+ };
135
+ });
136
+ }, []);
137
+
138
+ const onDragEnd = useCallback(
139
+ (event: HandledEvents) => {
140
+ const eventData = state.eventData;
141
+ const distanceX = event.changedTouches[0].clientX - state.xy[0];
142
+ const distanceY = event.changedTouches[0].clientY - state.xy[1];
143
+
144
+ let swipeDirection;
145
+
146
+ if (Math.abs(distanceX) > Math.abs(distanceY)) {
147
+ // Horizontal swipe
148
+ swipeDirection = distanceX > 0 ? RIGHT : LEFT;
149
+ } else {
150
+ // Vertical swipe
151
+ swipeDirection = distanceY > 0 ? DOWN : UP;
152
+ }
153
+ if (events.onDragEnd) {
154
+ // @ts-ignore
155
+ events.onDragEnd({ absX: Math.abs(distanceX), absY: Math.abs(distanceY), dir: swipeDirection, ...eventData });
156
+ }
157
+
158
+ setState((state) => {
159
+ return {
160
+ ...state,
161
+ initialDxDy: state.lastDxDy,
162
+ };
163
+ });
164
+ },
165
+ // eslint-disable-next-line react-hooks/exhaustive-deps
166
+ [events],
167
+ );
168
+
169
+ return {
170
+ state,
171
+ onDragStart,
172
+ onDragEnd,
173
+ onDragging,
174
+ onCustomPositionChange,
175
+ };
176
+ }