react-swipe-action 0.2.0 → 0.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 CHANGED
@@ -1,6 +1,15 @@
1
- # React swipe action [![npm](https://img.shields.io/npm/v/react-swipe-action.svg)](https://www.npmjs.com/package/react-swipe-action) ![npm type definitions](https://img.shields.io/npm/types/react-swipe-action.svg)
1
+ # React Swipe Action [![npm](https://img.shields.io/npm/v/react-swipe-action.svg)](https://www.npmjs.com/package/react-swipe-action) ![npm type definitions](https://img.shields.io/npm/types/react-swipe-action.svg)
2
2
 
3
- Swipe to reveal or perform an action. Try interactive [Storybook demo](https://filipchalupa.cz/react-swipe-action/).
3
+ Swipe to reveal or perform an action. Try the interactive [Storybook demo](https://filipchalupa.cz/react-swipe-action/).
4
+
5
+ ## Features
6
+
7
+ - Works with both mouse and touch events.
8
+ - Supports actions on both the left (start) and right (end) sides.
9
+ - "Long swipe" gesture to trigger an action.
10
+ - Customizable content and backgrounds for each side.
11
+ - `useSwipeActionReset` hook to programmatically close the swipe action.
12
+ - Written in TypeScript.
4
13
 
5
14
  ## Installation
6
15
 
@@ -10,28 +19,184 @@ npm install react-swipe-action
10
19
 
11
20
  ## How to use
12
21
 
22
+ Import the component and the CSS file.
23
+
13
24
  ```jsx
14
25
  import { SwipeAction } from 'react-swipe-action'
15
26
  import 'react-swipe-action/dist/index.css'
27
+ ```
28
+
29
+ Use the `SwipeAction` component. The `main` prop is a function that receives a `handle` which you should pass to the element you want to be draggable.
30
+
31
+ ```jsx
32
+ <SwipeAction
33
+ main={(handle) => (
34
+ <div className="main-content">
35
+ Swipe me
36
+ {handle}
37
+ </div>
38
+ )}
39
+ inertia
40
+ startAction={...}
41
+ endAction={...}
42
+ />
43
+ ```
44
+
45
+ ### Actions
46
+
47
+ You can define `startAction` (swipe from left to right) and/or `endAction` (swipe from right to left).
48
+
49
+ An action is an object with the following properties:
50
+
51
+ - `content`: The content to be revealed (e.g., a button).
52
+ - `background`: The background that is shown behind the content.
53
+ - `onLongSwipe`: A function to be called when a "long swipe" is performed.
54
+
55
+ ### Inertia
56
+
57
+ Pass the `inertia` prop to enable momentum-based snapping — the element will spring-animate to the nearest snap point after release.
58
+
59
+ ```jsx
60
+ <SwipeAction inertia .../>
61
+ ```
62
+
63
+ Action and its properties are optional, so you can choose to only implement one side or just the long swipe.
64
+
65
+ ```jsx
66
+ <SwipeAction
67
+ main={(handle) => <button>Main content{handle}</button>}
68
+ endAction={{
69
+ content: <button onClick={() => alert('Action clicked!')}>Delete</button>,
70
+ background: <div style={{ backgroundColor: 'red' }} />,
71
+ onLongSwipe: () => alert('Long swipe!'),
72
+ }}
73
+ />
74
+ ```
75
+
76
+ ### Closing the action programmatically
77
+
78
+ You can use the `useSwipeActionReset` hook to get a function that will reset the swipe action to its initial state. This is useful when you want to close the action after a button is clicked.
79
+
80
+ ```jsx
81
+ import { SwipeAction, useSwipeActionReset } from 'react-swipe-action'
82
+
83
+ const ActionButton = ({ onClick, children }) => {
84
+ const reset = useSwipeActionReset()
85
+ return (
86
+ <button
87
+ onClick={async () => {
88
+ await onClick()
89
+ reset()
90
+ }}
91
+ >
92
+ {children}
93
+ </button>
94
+ )
95
+ }
96
+
97
+ // ...
98
+
99
+ ;<SwipeAction
100
+ // ...
101
+ endAction={{
102
+ content: (
103
+ <ActionButton onClick={() => alert('Deleted!')}>Delete</ActionButton>
104
+ ),
105
+ // ...
106
+ }}
107
+ />
108
+ ```
109
+
110
+ ## API
111
+
112
+ ### `<SwipeAction />` Props
113
+
114
+ | Prop | Type | Description |
115
+ | ------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
116
+ | `main` | `(handle: ReactNode) => ReactNode` | **Required.** A function that returns the main content. It receives a `handle` which should be rendered on the draggable element. |
117
+ | `inertia` | `boolean` | Enables momentum-based snapping — the element spring-animates to the nearest snap point after release. |
118
+ | `startAction` | `Action` | An action to be performed when swiping from left to right. |
119
+ | `endAction` | `Action` | An action to be performed when swiping from right to left. |
120
+
121
+ ### `Action` Type
122
+
123
+ | Prop | Type | Description |
124
+ | ------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
125
+ | `content` | `ReactNode` | The content to be revealed when swiping. |
126
+ | `background` | `ReactNode` | The background to be shown behind the content. |
127
+ | `onLongSwipe` | `() => void` | A callback function that is triggered on a "long swipe" gesture. This can be used for quick actions without revealing the content. |
128
+
129
+ ### `useSwipeActionReset()`
130
+
131
+ A custom hook that returns a `reset` function. Call `reset()` to programmatically close the swipe action.
132
+
133
+ ## Example
134
+
135
+ ```jsx
136
+ import { SwipeAction, useSwipeActionReset } from 'react-swipe-action'
137
+ import 'react-swipe-action/dist/index.css'
138
+
139
+ const pretendWork = () => new Promise((resolve) => setTimeout(resolve, 1000))
140
+
141
+ const ActionButton = ({ onClick, children }) => {
142
+ const reset = useSwipeActionReset()
143
+ return (
144
+ <button
145
+ onClick={async () => {
146
+ await onClick()
147
+ reset()
148
+ }}
149
+ >
150
+ {children}
151
+ </button>
152
+ )
153
+ }
16
154
 
17
155
  const App = () => {
18
156
  return (
19
157
  <SwipeAction
20
- main={(handle) => <button onClick={() => { alert('Click') }} style={{ position: 'relative' }}>
21
- Button
22
- {handle}
23
- </button>}
158
+ main={(handle) => (
159
+ <button
160
+ className="main"
161
+ onClick={() => {
162
+ alert("You've clicked me!")
163
+ }}
164
+ >
165
+ Swipe me
166
+ {handle}
167
+ </button>
168
+ )}
169
+ startAction={{
170
+ content: (
171
+ <ActionButton
172
+ onClick={() => {
173
+ alert('Click left side!')
174
+ }}
175
+ >
176
+ 🔖
177
+ </ActionButton>
178
+ ),
179
+ background: <div style={{ backgroundColor: 'blue' }} />,
180
+ onLongSwipe: () => {
181
+ alert('Long swipe from left side!')
182
+ },
183
+ }}
24
184
  endAction={{
25
185
  content: (
26
- <button
27
- type="button"
28
- onClick={() => { alert('Right action') }}
186
+ <ActionButton
187
+ onClick={async () => {
188
+ await pretendWork()
189
+ alert('Click right side which took some time to process!')
190
+ }}
29
191
  >
30
- Right action
31
- </button>
192
+ 🗑️
193
+ </ActionButton>
32
194
  ),
33
- onLongSwipe: () => { alert('Right action') },
34
195
  background: <div style={{ backgroundColor: 'red' }} />,
196
+ onLongSwipe: async () => {
197
+ await pretendWork()
198
+ alert('Long swipe from right side which took some time to process!')
199
+ },
35
200
  }}
36
201
  />
37
202
  )
@@ -12,6 +12,7 @@ export type Action = {
12
12
  });
13
13
  export type SwipeActionProps = {
14
14
  main: (handle: ReactNode) => ReactNode;
15
+ inertia?: boolean;
15
16
  startAction?: Action;
16
17
  endAction?: Action;
17
18
  };
@@ -1,103 +1,97 @@
1
1
  "use client";
2
- import { __assign } from './_virtual/_tslib.js';
3
2
  import { jsx, jsxs } from 'react/jsx-runtime';
4
- import { createContext, useState, useCallback, useRef, useMemo, useContext } from 'react';
3
+ import { createContext, useState, useRef, useCallback, useMemo, useContext } from 'react';
5
4
  import { useDrag } from 'react-use-drag';
6
5
  import styles from './SwipeAction.module.css.js';
6
+ import { useElementWidth } from './useElementWidth.js';
7
7
 
8
- var context = createContext({
9
- reset: function () {
8
+ const context = createContext({
9
+ reset: () => {
10
10
  throw new Error("Can't call reset outside SwipeAction component.");
11
11
  },
12
12
  });
13
- var SwipeAction = function (_a) {
14
- var startAction = _a.startAction, endAction = _a.endAction, main = _a.main;
15
- var _b = useState(0), position = _b[0], setPosition = _b[1];
16
- var _c = useState(0), positionOffset = _c[0], setPositionOffset = _c[1];
17
- var _d = useState(false), isSwiping = _d[0], setIsSwiping = _d[1];
18
- var onRelativePositionChange = useCallback(function (x) {
13
+ const SwipeAction = ({ startAction, endAction, main, inertia, }) => {
14
+ const [position, setPosition] = useState(0);
15
+ const [positionOffset, setPositionOffset] = useState(0);
16
+ const [isSwiping, setIsSwiping] = useState(false);
17
+ const [startActionContentRef, startWidth] = useElementWidth();
18
+ const [endActionContentRef, endWidth] = useElementWidth();
19
+ const [mainRef, mainWidth] = useElementWidth();
20
+ const isLongSwipeEnabled = useRef(true); // Prevents long swipe from being triggered twice in React strict mode
21
+ const onRelativePositionChange = useCallback(({ x }) => {
19
22
  if (Math.abs(x) > 5) {
20
23
  setIsSwiping(true);
21
24
  }
22
25
  setPositionOffset(x);
23
26
  }, []);
24
- var isLongSwipeEnabled = useRef(true); // Prevents long swipe from being triggered twice in React strict mode
25
- var onEnd = useCallback(function (x) {
27
+ const onEnd = useCallback(({ x }) => {
28
+ const newPosition = position + x;
29
+ setPosition(newPosition);
30
+ setPositionOffset(0);
26
31
  if (x === 0) {
27
32
  setIsSwiping(false);
28
33
  }
29
- else {
30
- setPosition(function (position) {
31
- var _a, _b, _c, _d;
32
- var newPosition = position + x;
33
- var mainWidth = (_b = (_a = mainRef.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) !== null && _b !== void 0 ? _b : 0;
34
- var handleContent = function (ref, position, onLongSwipe) {
35
- var _a;
36
- var contentWidth = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.offsetWidth;
37
- var positionSign = position === 'start' ? 1 : -1;
38
- var normalizedSwipePosition = positionSign * newPosition;
39
- if (onLongSwipe &&
40
- contentWidth !== undefined &&
41
- normalizedSwipePosition >
42
- Math.max(mainWidth / 4, contentWidth) * 1.6) {
43
- if (isLongSwipeEnabled.current) {
44
- Promise.resolve(onLongSwipe()).then(function () {
45
- setPosition(0);
46
- });
47
- isLongSwipeEnabled.current = false;
48
- }
49
- return positionSign * mainWidth;
50
- }
51
- if (contentWidth !== 0 &&
52
- contentWidth !== undefined &&
53
- normalizedSwipePosition > contentWidth * 0.7) {
54
- return positionSign * contentWidth;
55
- }
56
- if (Math.sign(newPosition) === positionSign) {
57
- setTimeout(function () {
58
- setIsSwiping(false);
59
- }, 200); // Delay to ignore immediate click
60
- return 0;
61
- }
62
- };
63
- return ((_d = (_c = handleContent(startActionContentRef, 'start', startAction === null || startAction === void 0 ? void 0 : startAction.onLongSwipe)) !== null && _c !== void 0 ? _c : handleContent(endActionContentRef, 'end', endAction === null || endAction === void 0 ? void 0 : endAction.onLongSwipe)) !== null && _d !== void 0 ? _d : newPosition);
64
- });
34
+ else if (newPosition === 0) {
35
+ setTimeout(() => {
36
+ setIsSwiping(false);
37
+ }, 200); // Delay to ignore immediate click
65
38
  }
66
- setPositionOffset(0);
67
- }, [startAction === null || startAction === void 0 ? void 0 : startAction.onLongSwipe, endAction === null || endAction === void 0 ? void 0 : endAction.onLongSwipe]);
68
- var onStart = useCallback(function () {
39
+ if (mainWidth > 0 && Math.abs(newPosition) >= mainWidth - 0.5) {
40
+ const onLongSwipe = newPosition > 0 ? startAction === null || startAction === void 0 ? void 0 : startAction.onLongSwipe : endAction === null || endAction === void 0 ? void 0 : endAction.onLongSwipe;
41
+ if (onLongSwipe && isLongSwipeEnabled.current) {
42
+ isLongSwipeEnabled.current = false;
43
+ Promise.resolve(onLongSwipe()).then(() => {
44
+ setPosition(0);
45
+ setIsSwiping(false);
46
+ });
47
+ }
48
+ }
49
+ }, [position, mainWidth, startAction === null || startAction === void 0 ? void 0 : startAction.onLongSwipe, endAction === null || endAction === void 0 ? void 0 : endAction.onLongSwipe]);
50
+ const onStart = useCallback(() => {
69
51
  isLongSwipeEnabled.current = true;
70
52
  }, []);
71
- var elementProps = useDrag({
72
- onStart: onStart,
73
- onRelativePositionChange: onRelativePositionChange,
74
- onEnd: onEnd,
75
- }).elementProps;
76
- var mainRef = useRef(null);
77
- var startActionContentRef = useRef(null);
78
- var endActionContentRef = useRef(null);
79
- var x = useMemo(function () {
80
- return Math.max(endAction ? Number.NEGATIVE_INFINITY : 0, Math.min(startAction ? Number.POSITIVE_INFINITY : 0, position + positionOffset));
81
- }, [position, positionOffset, startAction, endAction]);
82
- var reset = useCallback(function () {
53
+ const snapPoints = useMemo(() => {
54
+ const points = [{ x: -position, y: 0 }];
55
+ if (startAction) {
56
+ if (startWidth > 0)
57
+ points.push({ x: startWidth - position, y: 0 });
58
+ if (startAction.onLongSwipe && mainWidth > 0)
59
+ points.push({ x: mainWidth - position, y: 0 });
60
+ }
61
+ if (endAction) {
62
+ if (endWidth > 0)
63
+ points.push({ x: -endWidth - position, y: 0 });
64
+ if (endAction.onLongSwipe && mainWidth > 0)
65
+ points.push({ x: -mainWidth - position, y: 0 });
66
+ }
67
+ return points;
68
+ }, [position, mainWidth, startWidth, endWidth, startAction, endAction]);
69
+ const { elementProps } = useDrag({
70
+ onStart,
71
+ onRelativePositionChange,
72
+ onEnd,
73
+ inertia,
74
+ snapPoints,
75
+ });
76
+ const x = useMemo(() => Math.max(endAction ? Number.NEGATIVE_INFINITY : 0, Math.min(startAction ? Number.POSITIVE_INFINITY : 0, position + positionOffset)), [position, positionOffset, startAction, endAction]);
77
+ const reset = useCallback(() => {
83
78
  setPosition(0);
84
79
  setPositionOffset(0);
85
80
  setIsSwiping(false);
86
81
  }, []);
87
- return (jsx(context.Provider, { value: { reset: reset }, children: jsxs("div", { className: styles.wrapper, children: [startAction && x > 0 && (jsx(Action, { position: "start", content: startAction.content, background: startAction.background, contentRef: startActionContentRef })), endAction && x < 0 && (jsx(Action, { position: "end", content: endAction.content, background: endAction.background, contentRef: endActionContentRef })), jsx("div", { className: styles.main, ref: mainRef, style: {
88
- '--x': "".concat(x, "px"),
89
- }, children: main(jsx("div", __assign({ className: styles.handle }, elementProps, { onClick: function (event) {
82
+ return (jsx(context.Provider, { value: { reset }, children: jsxs("div", { className: styles.wrapper, children: [startAction && x > 0 && (jsx(Action, { position: "start", content: startAction.content, background: startAction.background, contentRef: startActionContentRef })), endAction && x < 0 && (jsx(Action, { position: "end", content: endAction.content, background: endAction.background, contentRef: endActionContentRef })), jsx("div", { className: styles.main, ref: mainRef, style: {
83
+ '--x': `${x}px`,
84
+ }, children: main(jsx("div", Object.assign({ className: styles.handle }, elementProps, { onClick: (event) => {
90
85
  if (isSwiping) {
91
86
  event.stopPropagation();
92
87
  }
93
88
  } }))) })] }) }));
94
89
  };
95
- var Action = function (_a) {
96
- var content = _a.content, background = _a.background, position = _a.position, contentRef = _a.contentRef;
90
+ const Action = ({ content, background, position, contentRef }) => {
97
91
  // @TODO: allow focus by tab key before visible
98
- return (jsxs("div", { className: "".concat(styles.action, " ").concat(styles["is_position_".concat(position)]), children: [jsx("div", { className: styles.action_background, children: background }), jsx("div", { className: styles.action_content, ref: contentRef, children: content })] }));
92
+ return (jsxs("div", { className: `${styles.action} ${styles[`is_position_${position}`]}`, children: [jsx("div", { className: styles.action_background, children: background }), jsx("div", { className: styles.action_content, ref: contentRef, children: content })] }));
99
93
  };
100
- var useSwipeActionReset = function () { return useContext(context).reset; };
94
+ const useSwipeActionReset = () => useContext(context).reset;
101
95
 
102
96
  export { SwipeAction, useSwipeActionReset };
103
97
  //# sourceMappingURL=SwipeAction.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SwipeAction.js","sources":["../src/SwipeAction.tsx"],"sourcesContent":["'use client'\n\nimport type {\n\tCSSProperties,\n\tFunctionComponent,\n\tReactNode,\n\tRefObject,\n} from 'react'\nimport {\n\tcreateContext,\n\tuseCallback,\n\tuseContext,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport { useDrag } from 'react-use-drag'\nimport styles from './SwipeAction.module.css'\n\n// @TODO: add inertia\n// @TODO: animate snapping\n// @TODO: add threshold - maybe to react-use-drag\n\ntype OnLongSwipe = () => void | Promise<void>\ntype Content = ReactNode\n\nexport type Action = {\n\tbackground?: ReactNode\n\tcontent?: Content\n\tonLongSwipe?: OnLongSwipe\n} & (\n\t| {\n\t\t\tcontent: Content\n\t }\n\t| {\n\t\t\tonLongSwipe: OnLongSwipe\n\t }\n)\n\nconst context = createContext({\n\treset: (): void => {\n\t\tthrow new Error(\"Can't call reset outside SwipeAction component.\")\n\t},\n})\n\nexport type SwipeActionProps = {\n\tmain: (handle: ReactNode) => ReactNode\n\tstartAction?: Action\n\tendAction?: Action\n}\n\nexport const SwipeAction: FunctionComponent<SwipeActionProps> = ({\n\tstartAction,\n\tendAction,\n\tmain,\n}) => {\n\tconst [position, setPosition] = useState(0)\n\tconst [positionOffset, setPositionOffset] = useState(0)\n\tconst [isSwiping, setIsSwiping] = useState(false)\n\tconst onRelativePositionChange = useCallback((x: number) => {\n\t\tif (Math.abs(x) > 5) {\n\t\t\tsetIsSwiping(true)\n\t\t}\n\t\tsetPositionOffset(x)\n\t}, [])\n\tconst isLongSwipeEnabled = useRef(true) // Prevents long swipe from being triggered twice in React strict mode\n\tconst onEnd = useCallback(\n\t\t(x: number) => {\n\t\t\tif (x === 0) {\n\t\t\t\tsetIsSwiping(false)\n\t\t\t} else {\n\t\t\t\tsetPosition((position) => {\n\t\t\t\t\tconst newPosition = position + x\n\t\t\t\t\tconst mainWidth = mainRef.current?.offsetWidth ?? 0\n\n\t\t\t\t\tconst handleContent = (\n\t\t\t\t\t\tref: RefObject<HTMLDivElement>,\n\t\t\t\t\t\tposition: Position,\n\t\t\t\t\t\tonLongSwipe: undefined | OnLongSwipe,\n\t\t\t\t\t) => {\n\t\t\t\t\t\tconst contentWidth = ref.current?.offsetWidth\n\t\t\t\t\t\tconst positionSign = position === 'start' ? 1 : -1\n\t\t\t\t\t\tconst normalizedSwipePosition = positionSign * newPosition\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tonLongSwipe &&\n\t\t\t\t\t\t\tcontentWidth !== undefined &&\n\t\t\t\t\t\t\tnormalizedSwipePosition >\n\t\t\t\t\t\t\t\tMath.max(mainWidth / 4, contentWidth) * 1.6\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tif (isLongSwipeEnabled.current) {\n\t\t\t\t\t\t\t\tPromise.resolve(onLongSwipe()).then(() => {\n\t\t\t\t\t\t\t\t\tsetPosition(0)\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\tisLongSwipeEnabled.current = false\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn positionSign * mainWidth\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tcontentWidth !== 0 &&\n\t\t\t\t\t\t\tcontentWidth !== undefined &&\n\t\t\t\t\t\t\tnormalizedSwipePosition > contentWidth * 0.7\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\treturn positionSign * contentWidth\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (Math.sign(newPosition) === positionSign) {\n\t\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\t\tsetIsSwiping(false)\n\t\t\t\t\t\t\t}, 200) // Delay to ignore immediate click\n\t\t\t\t\t\t\treturn 0\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn (\n\t\t\t\t\t\thandleContent(\n\t\t\t\t\t\t\tstartActionContentRef,\n\t\t\t\t\t\t\t'start',\n\t\t\t\t\t\t\tstartAction?.onLongSwipe,\n\t\t\t\t\t\t) ??\n\t\t\t\t\t\thandleContent(endActionContentRef, 'end', endAction?.onLongSwipe) ??\n\t\t\t\t\t\tnewPosition\n\t\t\t\t\t)\n\t\t\t\t})\n\t\t\t}\n\t\t\tsetPositionOffset(0)\n\t\t},\n\t\t[startAction?.onLongSwipe, endAction?.onLongSwipe],\n\t)\n\tconst onStart = useCallback(() => {\n\t\tisLongSwipeEnabled.current = true\n\t}, [])\n\tconst { elementProps } = useDrag({\n\t\tonStart,\n\t\tonRelativePositionChange,\n\t\tonEnd,\n\t})\n\tconst mainRef = useRef<HTMLDivElement>(null)\n\tconst startActionContentRef = useRef<HTMLDivElement>(null)\n\tconst endActionContentRef = useRef<HTMLDivElement>(null)\n\n\tconst x = useMemo(\n\t\t() =>\n\t\t\tMath.max(\n\t\t\t\tendAction ? Number.NEGATIVE_INFINITY : 0,\n\t\t\t\tMath.min(\n\t\t\t\t\tstartAction ? Number.POSITIVE_INFINITY : 0,\n\t\t\t\t\tposition + positionOffset,\n\t\t\t\t),\n\t\t\t),\n\t\t[position, positionOffset, startAction, endAction],\n\t)\n\n\tconst reset = useCallback(() => {\n\t\tsetPosition(0)\n\t\tsetPositionOffset(0)\n\t\tsetIsSwiping(false)\n\t}, [])\n\n\treturn (\n\t\t<context.Provider value={{ reset }}>\n\t\t\t<div className={styles.wrapper}>\n\t\t\t\t{startAction && x > 0 && (\n\t\t\t\t\t<Action\n\t\t\t\t\t\tposition=\"start\"\n\t\t\t\t\t\tcontent={startAction.content}\n\t\t\t\t\t\tbackground={startAction.background}\n\t\t\t\t\t\tcontentRef={startActionContentRef}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t\t{endAction && x < 0 && (\n\t\t\t\t\t<Action\n\t\t\t\t\t\tposition=\"end\"\n\t\t\t\t\t\tcontent={endAction.content}\n\t\t\t\t\t\tbackground={endAction.background}\n\t\t\t\t\t\tcontentRef={endActionContentRef}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t\t<div\n\t\t\t\t\tclassName={styles.main}\n\t\t\t\t\tref={mainRef}\n\t\t\t\t\tstyle={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t'--x': `${x}px`,\n\t\t\t\t\t\t} as CSSProperties\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t{main(\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={styles.handle}\n\t\t\t\t\t\t\t{...elementProps}\n\t\t\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\t\t\tif (isSwiping) {\n\t\t\t\t\t\t\t\t\tevent.stopPropagation()\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>,\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</context.Provider>\n\t)\n}\n\ntype Position = 'start' | 'end'\n\nconst Action: FunctionComponent<{\n\tposition: Position\n\tcontent: ReactNode\n\tbackground: ReactNode\n\tcontentRef: RefObject<HTMLDivElement>\n}> = ({ content, background, position, contentRef }) => {\n\t// @TODO: allow focus by tab key before visible\n\treturn (\n\t\t<div className={`${styles.action} ${styles[`is_position_${position}`]}`}>\n\t\t\t<div className={styles.action_background}>{background}</div>\n\t\t\t<div className={styles.action_content} ref={contentRef}>\n\t\t\t\t{content}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nexport const useSwipeActionReset = () => useContext(context).reset\n"],"names":[],"mappings":";;;;;;;AAuCA;AACC;AACC;;AAED;AAQM;AACN;;;;AAOA;;;;;;;AAOA;AAEE;;;;;;AAIE;;AAGA;;;AAMC;AACA;AACA;AAEC;;AAEC;AAED;;;AAGC;AACA;;;;;AAMD;AACA;;;;AAKA;;AAEA;AACA;;AAEF;AAEA;AASD;;;AAGF;;AAIA;;;AAGA;AACA;AACA;AACA;AACD;AACA;AACA;;AAIE;;;;;;;;;AAyCqB;;;;AAWjB;AAOP;AAIA;;;AAOC;AAQD;AAEO;;"}
1
+ {"version":3,"file":"SwipeAction.js","sources":["../src/SwipeAction.tsx"],"sourcesContent":["'use client'\n\nimport type {\n\tCSSProperties,\n\tFunctionComponent,\n\tReactNode,\n\tRefCallback,\n} from 'react'\nimport {\n\tcreateContext,\n\tuseCallback,\n\tuseContext,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport {\n\tuseDrag,\n\ttype Position,\n\ttype PositionWithVelocity,\n} from 'react-use-drag'\nimport styles from './SwipeAction.module.css'\nimport { useElementWidth } from './useElementWidth'\n\ntype OnLongSwipe = () => void | Promise<void>\ntype Content = ReactNode\n\nexport type Action = {\n\tbackground?: ReactNode\n\tcontent?: Content\n\tonLongSwipe?: OnLongSwipe\n} & (\n\t| {\n\t\t\tcontent: Content\n\t }\n\t| {\n\t\t\tonLongSwipe: OnLongSwipe\n\t }\n)\n\nconst context = createContext({\n\treset: (): void => {\n\t\tthrow new Error(\"Can't call reset outside SwipeAction component.\")\n\t},\n})\n\nexport type SwipeActionProps = {\n\tmain: (handle: ReactNode) => ReactNode\n\tinertia?: boolean\n\tstartAction?: Action\n\tendAction?: Action\n}\n\nexport const SwipeAction: FunctionComponent<SwipeActionProps> = ({\n\tstartAction,\n\tendAction,\n\tmain,\n\tinertia,\n}) => {\n\tconst [position, setPosition] = useState(0)\n\tconst [positionOffset, setPositionOffset] = useState(0)\n\tconst [isSwiping, setIsSwiping] = useState(false)\n\tconst [startActionContentRef, startWidth] = useElementWidth()\n\tconst [endActionContentRef, endWidth] = useElementWidth()\n\tconst [mainRef, mainWidth] = useElementWidth()\n\tconst isLongSwipeEnabled = useRef(true) // Prevents long swipe from being triggered twice in React strict mode\n\tconst onRelativePositionChange = useCallback(\n\t\t({ x }: PositionWithVelocity) => {\n\t\t\tif (Math.abs(x) > 5) {\n\t\t\t\tsetIsSwiping(true)\n\t\t\t}\n\t\t\tsetPositionOffset(x)\n\t\t},\n\t\t[],\n\t)\n\tconst onEnd = useCallback(\n\t\t({ x }: PositionWithVelocity) => {\n\t\t\tconst newPosition = position + x\n\t\t\tsetPosition(newPosition)\n\t\t\tsetPositionOffset(0)\n\t\t\tif (x === 0) {\n\t\t\t\tsetIsSwiping(false)\n\t\t\t} else if (newPosition === 0) {\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tsetIsSwiping(false)\n\t\t\t\t}, 200) // Delay to ignore immediate click\n\t\t\t}\n\t\t\tif (mainWidth > 0 && Math.abs(newPosition) >= mainWidth - 0.5) {\n\t\t\t\tconst onLongSwipe =\n\t\t\t\t\tnewPosition > 0 ? startAction?.onLongSwipe : endAction?.onLongSwipe\n\t\t\t\tif (onLongSwipe && isLongSwipeEnabled.current) {\n\t\t\t\t\tisLongSwipeEnabled.current = false\n\t\t\t\t\tPromise.resolve(onLongSwipe()).then(() => {\n\t\t\t\t\t\tsetPosition(0)\n\t\t\t\t\t\tsetIsSwiping(false)\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[position, mainWidth, startAction?.onLongSwipe, endAction?.onLongSwipe],\n\t)\n\tconst onStart = useCallback(() => {\n\t\tisLongSwipeEnabled.current = true\n\t}, [])\n\tconst snapPoints = useMemo((): Position[] => {\n\t\tconst points: Position[] = [{ x: -position, y: 0 }]\n\t\tif (startAction) {\n\t\t\tif (startWidth > 0) points.push({ x: startWidth - position, y: 0 })\n\t\t\tif (startAction.onLongSwipe && mainWidth > 0)\n\t\t\t\tpoints.push({ x: mainWidth - position, y: 0 })\n\t\t}\n\t\tif (endAction) {\n\t\t\tif (endWidth > 0) points.push({ x: -endWidth - position, y: 0 })\n\t\t\tif (endAction.onLongSwipe && mainWidth > 0)\n\t\t\t\tpoints.push({ x: -mainWidth - position, y: 0 })\n\t\t}\n\t\treturn points\n\t}, [position, mainWidth, startWidth, endWidth, startAction, endAction])\n\tconst { elementProps } = useDrag({\n\t\tonStart,\n\t\tonRelativePositionChange,\n\t\tonEnd,\n\t\tinertia,\n\t\tsnapPoints,\n\t})\n\n\tconst x = useMemo(\n\t\t() =>\n\t\t\tMath.max(\n\t\t\t\tendAction ? Number.NEGATIVE_INFINITY : 0,\n\t\t\t\tMath.min(\n\t\t\t\t\tstartAction ? Number.POSITIVE_INFINITY : 0,\n\t\t\t\t\tposition + positionOffset,\n\t\t\t\t),\n\t\t\t),\n\t\t[position, positionOffset, startAction, endAction],\n\t)\n\n\tconst reset = useCallback(() => {\n\t\tsetPosition(0)\n\t\tsetPositionOffset(0)\n\t\tsetIsSwiping(false)\n\t}, [])\n\n\treturn (\n\t\t<context.Provider value={{ reset }}>\n\t\t\t<div className={styles.wrapper}>\n\t\t\t\t{startAction && x > 0 && (\n\t\t\t\t\t<Action\n\t\t\t\t\t\tposition=\"start\"\n\t\t\t\t\t\tcontent={startAction.content}\n\t\t\t\t\t\tbackground={startAction.background}\n\t\t\t\t\t\tcontentRef={startActionContentRef}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t\t{endAction && x < 0 && (\n\t\t\t\t\t<Action\n\t\t\t\t\t\tposition=\"end\"\n\t\t\t\t\t\tcontent={endAction.content}\n\t\t\t\t\t\tbackground={endAction.background}\n\t\t\t\t\t\tcontentRef={endActionContentRef}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t\t<div\n\t\t\t\t\tclassName={styles.main}\n\t\t\t\t\tref={mainRef}\n\t\t\t\t\tstyle={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t'--x': `${x}px`,\n\t\t\t\t\t\t} as CSSProperties\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t{main(\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={styles.handle}\n\t\t\t\t\t\t\t{...elementProps}\n\t\t\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\t\t\tif (isSwiping) {\n\t\t\t\t\t\t\t\t\tevent.stopPropagation()\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>,\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</context.Provider>\n\t)\n}\n\ntype ActionPosition = 'start' | 'end'\n\nconst Action: FunctionComponent<{\n\tposition: ActionPosition\n\tcontent: ReactNode\n\tbackground: ReactNode\n\tcontentRef: RefCallback<HTMLDivElement>\n}> = ({ content, background, position, contentRef }) => {\n\t// @TODO: allow focus by tab key before visible\n\treturn (\n\t\t<div className={`${styles.action} ${styles[`is_position_${position}`]}`}>\n\t\t\t<div className={styles.action_background}>{background}</div>\n\t\t\t<div className={styles.action_content} ref={contentRef}>\n\t\t\t\t{content}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nexport const useSwipeActionReset = () => useContext(context).reset\n"],"names":[],"mappings":";;;;;;;AAwCA;;AAEE;;AAED;AASM;;;;;;;;;;;;;;;AAwBJ;;;AAGA;;;AAEO;;;AAGN;;AAED;;AAGC;AACC;;;;AAIA;;;;AAMJ;AACC;;AAED;AACC;;;AAEqB;AACpB;AACC;;;;AAGiB;AAClB;AACC;;AAEF;AACD;AACA;;;;;;AAMC;;AAcD;;;;;;;AA+BuB;;;;AAWjB;AAOP;AAIA;;AAOC;AAQD;AAEO;;"}
@@ -0,0 +1,2 @@
1
+ import { type RefCallback } from 'react';
2
+ export declare const useElementWidth: () => [RefCallback<HTMLElement | null>, number];
@@ -0,0 +1,23 @@
1
+ import { useState, useRef, useCallback } from 'react';
2
+
3
+ const useElementWidth = () => {
4
+ const [width, setWidth] = useState(0);
5
+ const observerRef = useRef(null);
6
+ const ref = useCallback((element) => {
7
+ var _a;
8
+ (_a = observerRef.current) === null || _a === void 0 ? void 0 : _a.disconnect();
9
+ observerRef.current = null;
10
+ if (!element) {
11
+ return;
12
+ }
13
+ observerRef.current = new ResizeObserver(([entry]) => {
14
+ var _a, _b, _c;
15
+ setWidth((_c = (_b = (_a = entry.borderBoxSize) === null || _a === void 0 ? void 0 : _a.at(0)) === null || _b === void 0 ? void 0 : _b.inlineSize) !== null && _c !== void 0 ? _c : entry.contentRect.width);
16
+ });
17
+ observerRef.current.observe(element);
18
+ }, []);
19
+ return [ref, width];
20
+ };
21
+
22
+ export { useElementWidth };
23
+ //# sourceMappingURL=useElementWidth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useElementWidth.js","sources":["../src/useElementWidth.ts"],"sourcesContent":["import { type RefCallback, useCallback, useRef, useState } from 'react'\n\nexport const useElementWidth = (): [\n\tRefCallback<HTMLElement | null>,\n\tnumber,\n] => {\n\tconst [width, setWidth] = useState(0)\n\tconst observerRef = useRef<ResizeObserver | null>(null)\n\n\tconst ref = useCallback((element: HTMLElement | null) => {\n\t\tobserverRef.current?.disconnect()\n\t\tobserverRef.current = null\n\t\tif (!element) {\n\t\t\treturn\n\t\t}\n\t\tobserverRef.current = new ResizeObserver(([entry]) => {\n\t\t\tsetWidth(\n\t\t\t\tentry.borderBoxSize?.at(0)?.inlineSize ?? entry.contentRect.width,\n\t\t\t)\n\t\t})\n\t\tobserverRef.current.observe(element)\n\t}, [])\n\n\treturn [ref, width]\n}\n"],"names":[],"mappings":";;AAEO,MAAM,eAAe,GAAG,MAG3B;IACH,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;AACrC,IAAA,MAAM,WAAW,GAAG,MAAM,CAAwB,IAAI,CAAC,CAAA;AAEvD,IAAA,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,OAA2B,KAAI;;AACvD,QAAA,CAAA,EAAA,GAAA,WAAW,CAAC,OAAO,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,UAAU,EAAE,CAAA;AACjC,QAAA,WAAW,CAAC,OAAO,GAAG,IAAI,CAAA;QAC1B,IAAI,CAAC,OAAO,EAAE;YACb,OAAM;SACN;QACD,WAAW,CAAC,OAAO,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,KAAI;;YACpD,QAAQ,CACP,MAAA,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,KAAK,CAAC,aAAa,0CAAE,EAAE,CAAC,CAAC,CAAC,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,UAAU,MAAI,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAA,KAAK,CAAC,WAAW,CAAC,KAAK,CACjE,CAAA;AACF,SAAC,CAAC,CAAA;AACF,QAAA,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;KACpC,EAAE,EAAE,CAAC,CAAA;AAEN,IAAA,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;AACpB;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-swipe-action",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Swipe to reveal or perform an action.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,6 +8,7 @@
8
8
  "scripts": {
9
9
  "start": "rollup -c -w",
10
10
  "build": "rollup -c",
11
+ "dev": "npm run storybook",
11
12
  "prepare": "npm run build",
12
13
  "storybook": "storybook dev -p 6006",
13
14
  "build-storybook": "storybook build"
@@ -60,6 +61,6 @@
60
61
  "/dist/"
61
62
  ],
62
63
  "dependencies": {
63
- "react-use-drag": "^0.3.2"
64
+ "react-use-drag": "^1.7.1"
64
65
  }
65
66
  }
@@ -1,12 +0,0 @@
1
- import type { StoryObj } from '@storybook/react';
2
- import './SwipeAction.stories.css';
3
- declare const meta: {
4
- title: string;
5
- tags: string[];
6
- parameters: {
7
- layout: string;
8
- };
9
- };
10
- export default meta;
11
- type Story = StoryObj<typeof meta>;
12
- export declare const All: Story;
@@ -1,35 +0,0 @@
1
- /******************************************************************************
2
- Copyright (c) Microsoft Corporation.
3
-
4
- Permission to use, copy, modify, and/or distribute this software for any
5
- purpose with or without fee is hereby granted.
6
-
7
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
9
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13
- PERFORMANCE OF THIS SOFTWARE.
14
- ***************************************************************************** */
15
- /* global Reflect, Promise, SuppressedError, Symbol */
16
-
17
-
18
- var __assign = function() {
19
- __assign = Object.assign || function __assign(t) {
20
- for (var s, i = 1, n = arguments.length; i < n; i++) {
21
- s = arguments[i];
22
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
23
- }
24
- return t;
25
- };
26
- return __assign.apply(this, arguments);
27
- };
28
-
29
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
30
- var e = new Error(message);
31
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
32
- };
33
-
34
- export { __assign };
35
- //# sourceMappingURL=_tslib.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"_tslib.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}