@seed-design/react-drawer 1.0.10 → 1.0.11

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.
@@ -1029,6 +1029,10 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1029
1029
  const pointerStartRef = React.useRef(null);
1030
1030
  const lastKnownPointerEventRef = React.useRef(null);
1031
1031
  const wasBeyondThePointRef = React.useRef(false);
1032
+ // Whether the current gesture moved past the swipe-start threshold. Used to
1033
+ // suppress the trailing click so dragging the sheet doesn't also activate the
1034
+ // element the gesture started on (iOS still synthesizes a click on release).
1035
+ const draggedRef = React.useRef(false);
1032
1036
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1033
1037
  const isDeltaInDirection = (delta, direction, threshold = 0)=>{
1034
1038
  if (wasBeyondThePointRef.current) return true;
@@ -1086,6 +1090,7 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1086
1090
  x: event.pageX,
1087
1091
  y: event.pageY
1088
1092
  };
1093
+ draggedRef.current = false;
1089
1094
  onPress(event);
1090
1095
  },
1091
1096
  onOpenAutoFocus: (e)=>{
@@ -1122,9 +1127,14 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1122
1127
  x: xPosition,
1123
1128
  y: yPosition
1124
1129
  };
1130
+ // Once the pointer travels past the swipe-start threshold the gesture
1131
+ // is a drag, not a tap — remember it so the trailing click can be
1132
+ // suppressed on release (iOS still synthesizes one on the origin).
1133
+ const isBeyondThreshold = Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold;
1134
+ if (isBeyondThreshold) draggedRef.current = true;
1125
1135
  const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
1126
1136
  if (isAllowedToSwipe) onDrag(event);
1127
- else if (Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold) {
1137
+ else if (isBeyondThreshold) {
1128
1138
  pointerStartRef.current = null;
1129
1139
  }
1130
1140
  },
@@ -1144,6 +1154,17 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1144
1154
  handleOnPointerUp(lastKnownPointerEventRef.current);
1145
1155
  }
1146
1156
  },
1157
+ onClickCapture: (event)=>{
1158
+ restProps.onClickCapture?.(event);
1159
+ // Swallow the click that follows a drag so dragging the sheet (e.g. a
1160
+ // swipe-to-dismiss started on an item) doesn't also activate that item.
1161
+ // A plain tap leaves draggedRef false and clicks normally.
1162
+ if (draggedRef.current) {
1163
+ event.preventDefault();
1164
+ event.stopPropagation();
1165
+ draggedRef.current = false;
1166
+ }
1167
+ },
1147
1168
  onInteractOutside: (e)=>{
1148
1169
  // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
1149
1170
  if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
@@ -1006,6 +1006,10 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
1006
1006
  const pointerStartRef = useRef(null);
1007
1007
  const lastKnownPointerEventRef = useRef(null);
1008
1008
  const wasBeyondThePointRef = useRef(false);
1009
+ // Whether the current gesture moved past the swipe-start threshold. Used to
1010
+ // suppress the trailing click so dragging the sheet doesn't also activate the
1011
+ // element the gesture started on (iOS still synthesizes a click on release).
1012
+ const draggedRef = useRef(false);
1009
1013
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1010
1014
  const isDeltaInDirection = (delta, direction, threshold = 0)=>{
1011
1015
  if (wasBeyondThePointRef.current) return true;
@@ -1063,6 +1067,7 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
1063
1067
  x: event.pageX,
1064
1068
  y: event.pageY
1065
1069
  };
1070
+ draggedRef.current = false;
1066
1071
  onPress(event);
1067
1072
  },
1068
1073
  onOpenAutoFocus: (e)=>{
@@ -1099,9 +1104,14 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
1099
1104
  x: xPosition,
1100
1105
  y: yPosition
1101
1106
  };
1107
+ // Once the pointer travels past the swipe-start threshold the gesture
1108
+ // is a drag, not a tap — remember it so the trailing click can be
1109
+ // suppressed on release (iOS still synthesizes one on the origin).
1110
+ const isBeyondThreshold = Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold;
1111
+ if (isBeyondThreshold) draggedRef.current = true;
1102
1112
  const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
1103
1113
  if (isAllowedToSwipe) onDrag(event);
1104
- else if (Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold) {
1114
+ else if (isBeyondThreshold) {
1105
1115
  pointerStartRef.current = null;
1106
1116
  }
1107
1117
  },
@@ -1121,6 +1131,17 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
1121
1131
  handleOnPointerUp(lastKnownPointerEventRef.current);
1122
1132
  }
1123
1133
  },
1134
+ onClickCapture: (event)=>{
1135
+ restProps.onClickCapture?.(event);
1136
+ // Swallow the click that follows a drag so dragging the sheet (e.g. a
1137
+ // swipe-to-dismiss started on an item) doesn't also activate that item.
1138
+ // A plain tap leaves draggedRef false and clicks normally.
1139
+ if (draggedRef.current) {
1140
+ event.preventDefault();
1141
+ event.stopPropagation();
1142
+ draggedRef.current = false;
1143
+ }
1144
+ },
1124
1145
  onInteractOutside: (e)=>{
1125
1146
  // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
1126
1147
  if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
package/lib/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- var Drawer12s = require('./Drawer-12s-B35ZJcKm.cjs');
1
+ var Drawer12s = require('./Drawer-12s-Bkn6enz-.cjs');
2
2
 
3
3
  var Drawer_namespace = {
4
4
  __proto__: null,
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DrawerBackdrop, a as DrawerCloseButton, b as DrawerContent, c as DrawerDescription, d as DrawerHandle, e as DrawerHeader, f as DrawerPositioner, g as DrawerRoot, h as DrawerTitle, i as DrawerTrigger } from './Drawer-12s-B-JAOvwq.js';
2
- export { u as useDrawer, j as useDrawerContext } from './Drawer-12s-B-JAOvwq.js';
1
+ import { D as DrawerBackdrop, a as DrawerCloseButton, b as DrawerContent, c as DrawerDescription, d as DrawerHandle, e as DrawerHeader, f as DrawerPositioner, g as DrawerRoot, h as DrawerTitle, i as DrawerTrigger } from './Drawer-12s-DxbsRb10.js';
2
+ export { u as useDrawer, j as useDrawerContext } from './Drawer-12s-DxbsRb10.js';
3
3
 
4
4
  var Drawer_namespace = {
5
5
  __proto__: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-design/react-drawer",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -0,0 +1,111 @@
1
+ import { fireEvent, render } from "@testing-library/react";
2
+ import { afterAll, beforeAll, describe, expect, it, mock, spyOn } from "bun:test";
3
+ import { DrawerContent, DrawerDescription, DrawerRoot, DrawerTitle } from "./Drawer";
4
+
5
+ function mockRect(element: HTMLElement, size = 100) {
6
+ return spyOn(element, "getBoundingClientRect").mockReturnValue({
7
+ x: 0,
8
+ y: 0,
9
+ width: size,
10
+ height: size,
11
+ top: 0,
12
+ left: 0,
13
+ right: size,
14
+ bottom: size,
15
+ toJSON: () => {},
16
+ });
17
+ }
18
+
19
+ // happy-dom's fireEvent does not carry page coordinates onto the synthetic
20
+ // event, so build the native PointerEvent and pin pageX/pageY explicitly —
21
+ // the same data a real touch gesture delivers and what the swipe-threshold
22
+ // check reads.
23
+ function firePointer(type: string, element: HTMLElement, x: number, y: number) {
24
+ const event = new PointerEvent(type, {
25
+ bubbles: true,
26
+ cancelable: true,
27
+ pointerId: 1,
28
+ pointerType: "touch",
29
+ clientX: x,
30
+ clientY: y,
31
+ });
32
+ Object.defineProperty(event, "pageX", { value: x });
33
+ Object.defineProperty(event, "pageY", { value: y });
34
+ fireEvent(element, event);
35
+ }
36
+
37
+ function setup(onItemClick: () => void) {
38
+ const utils = render(
39
+ <DrawerRoot defaultOpen dismissible direction="bottom" modal={false}>
40
+ <DrawerContent>
41
+ <DrawerTitle>Sheet</DrawerTitle>
42
+ <DrawerDescription>Sheet body</DrawerDescription>
43
+ <button type="button" data-testid="item" onClick={onItemClick}>
44
+ item
45
+ </button>
46
+ </DrawerContent>
47
+ </DrawerRoot>,
48
+ );
49
+ const content = utils.container.ownerDocument.querySelector("[data-drawer]") as HTMLElement;
50
+
51
+ // getTranslate() reads getComputedStyle().transform, which happy-dom leaves
52
+ // empty; seed an identity matrix and a measurable rect so onDrag doesn't throw.
53
+ content.style.transform = "matrix(1, 0, 0, 1, 0, 0)";
54
+ mockRect(content);
55
+
56
+ return { ...utils, content, item: utils.getByTestId("item") };
57
+ }
58
+
59
+ describe("DrawerContent trailing-click suppression", () => {
60
+ const originalSetPointerCapture = window.HTMLElement.prototype.setPointerCapture;
61
+
62
+ beforeAll(() => {
63
+ window.HTMLElement.prototype.setPointerCapture = mock(() => {});
64
+ });
65
+
66
+ afterAll(() => {
67
+ window.HTMLElement.prototype.setPointerCapture = originalSetPointerCapture;
68
+ });
69
+
70
+ it("드래그가 swipe 임계값을 넘으면 뒤따르는 click을 억제한다", () => {
71
+ const onItemClick = mock(() => {});
72
+ const { content, item } = setup(onItemClick);
73
+
74
+ firePointer("pointerdown", content, 50, 50);
75
+ firePointer("pointermove", content, 50, 85); // +35px > touch 임계값 10px
76
+ firePointer("pointerup", content, 50, 85);
77
+ fireEvent.click(item, { clientX: 50, clientY: 85 });
78
+
79
+ expect(onItemClick).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it("임계값을 넘지 않은 순수 tap은 click을 통과시킨다", () => {
83
+ const onItemClick = mock(() => {});
84
+ const { content, item } = setup(onItemClick);
85
+
86
+ firePointer("pointerdown", content, 50, 50);
87
+ firePointer("pointermove", content, 52, 53); // 임계값 이내
88
+ firePointer("pointerup", content, 52, 53);
89
+ fireEvent.click(item, { clientX: 52, clientY: 53 });
90
+
91
+ expect(onItemClick).toHaveBeenCalledTimes(1);
92
+ });
93
+
94
+ it("click 없이 끝난 드래그 뒤의 깨끗한 tap은 다음 pointerdown에서 리셋되어 통과한다", () => {
95
+ const onItemClick = mock(() => {});
96
+ const { content, item } = setup(onItemClick);
97
+
98
+ // 큰 드래그는 iOS가 click을 합성하지 않아 click 없이 끝난다.
99
+ firePointer("pointerdown", content, 50, 50);
100
+ firePointer("pointermove", content, 50, 85);
101
+ firePointer("pointerup", content, 50, 85);
102
+
103
+ // 다음 제스처의 pointerdown이 드래그 상태를 리셋하므로 tap은 정상 동작해야 한다.
104
+ firePointer("pointerdown", content, 50, 50);
105
+ firePointer("pointermove", content, 51, 51);
106
+ firePointer("pointerup", content, 51, 51);
107
+ fireEvent.click(item, { clientX: 51, clientY: 51 });
108
+
109
+ expect(onItemClick).toHaveBeenCalledTimes(1);
110
+ });
111
+ });
package/src/Drawer.tsx CHANGED
@@ -130,6 +130,10 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
130
130
  const pointerStartRef = useRef<{ x: number; y: number } | null>(null);
131
131
  const lastKnownPointerEventRef = useRef<React.PointerEvent<HTMLDivElement> | null>(null);
132
132
  const wasBeyondThePointRef = useRef(false);
133
+ // Whether the current gesture moved past the swipe-start threshold. Used to
134
+ // suppress the trailing click so dragging the sheet doesn't also activate the
135
+ // element the gesture started on (iOS still synthesizes a click on release).
136
+ const draggedRef = useRef(false);
133
137
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
134
138
 
135
139
  const isDeltaInDirection = (
@@ -197,6 +201,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
197
201
  if (handleOnly) return;
198
202
  restProps.onPointerDown?.(event);
199
203
  pointerStartRef.current = { x: event.pageX, y: event.pageY };
204
+ draggedRef.current = false;
200
205
  onPress(event);
201
206
  }}
202
207
  onOpenAutoFocus={(e) => {
@@ -235,12 +240,16 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
235
240
  const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
236
241
  const delta = { x: xPosition, y: yPosition };
237
242
 
243
+ // Once the pointer travels past the swipe-start threshold the gesture
244
+ // is a drag, not a tap — remember it so the trailing click can be
245
+ // suppressed on release (iOS still synthesizes one on the origin).
246
+ const isBeyondThreshold =
247
+ Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold;
248
+ if (isBeyondThreshold) draggedRef.current = true;
249
+
238
250
  const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
239
251
  if (isAllowedToSwipe) onDrag(event);
240
- else if (
241
- Math.abs(xPosition) > swipeStartThreshold ||
242
- Math.abs(yPosition) > swipeStartThreshold
243
- ) {
252
+ else if (isBeyondThreshold) {
244
253
  pointerStartRef.current = null;
245
254
  }
246
255
  }}
@@ -260,6 +269,17 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
260
269
  handleOnPointerUp(lastKnownPointerEventRef.current);
261
270
  }
262
271
  }}
272
+ onClickCapture={(event) => {
273
+ restProps.onClickCapture?.(event);
274
+ // Swallow the click that follows a drag so dragging the sheet (e.g. a
275
+ // swipe-to-dismiss started on an item) doesn't also activate that item.
276
+ // A plain tap leaves draggedRef false and clicks normally.
277
+ if (draggedRef.current) {
278
+ event.preventDefault();
279
+ event.stopPropagation();
280
+ draggedRef.current = false;
281
+ }
282
+ }}
263
283
  onInteractOutside={(e) => {
264
284
  // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
265
285
  if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {