@expofp/ui 3.42.1 → 3.43.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.
@@ -130,7 +130,7 @@ const Overlay = ({ children, open, size = 'medium', particles, className, onChan
130
130
  const { onPointerDown } = useSheetDrag({
131
131
  enabled: mobile && !disableDrag && !forceFull,
132
132
  panelRef: overlayPanelRef,
133
- scrollRef,
133
+ scrollElement,
134
134
  snapPoints,
135
135
  current: currentSnap,
136
136
  openedAtRef,
@@ -6,7 +6,8 @@ export interface SnapPoint<T extends string> {
6
6
  interface UseSheetDragOptions<T extends string> {
7
7
  enabled: boolean;
8
8
  panelRef: RefObject<HTMLDivElement | null>;
9
- scrollRef: RefObject<HTMLDivElement | null>;
9
+ /** An element, not a ref: the touchmove listener has to follow it as it mounts. */
10
+ scrollElement: HTMLDivElement | null;
10
11
  /** Ascending by height; the last one is the tallest. */
11
12
  snapPoints: readonly SnapPoint<T>[];
12
13
  current: T;
@@ -20,7 +21,7 @@ interface UseSheetDragOptions<T extends string> {
20
21
  onSettle: () => void;
21
22
  onHandleTap: () => void;
22
23
  }
23
- export declare function useSheetDrag<T extends string>({ enabled, panelRef, scrollRef, snapPoints, current, openedAtRef, onDragStart, onSnap, onSettle, onHandleTap, }: UseSheetDragOptions<T>): {
24
+ export declare function useSheetDrag<T extends string>({ enabled, panelRef, scrollElement, snapPoints, current, openedAtRef, onDragStart, onSnap, onSettle, onHandleTap, }: UseSheetDragOptions<T>): {
24
25
  onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
25
26
  };
26
27
  export {};
@@ -31,7 +31,20 @@ const readOffset = (panel) => {
31
31
  return null;
32
32
  }
33
33
  };
34
- export function useSheetDrag({ enabled, panelRef, scrollRef, snapPoints, current, openedAtRef, onDragStart, onSnap, onSettle, onHandleTap, }) {
34
+ /** Whether a horizontal scroller — a chip row, a gallery strip — sits under the finger. */
35
+ const scrollsSideways = (target, panel) => {
36
+ let element = target instanceof Element ? target : null;
37
+ while (element && panel?.contains(element)) {
38
+ if (element.scrollWidth > element.clientWidth) {
39
+ const { overflowX } = getComputedStyle(element);
40
+ if (overflowX === 'auto' || overflowX === 'scroll')
41
+ return true;
42
+ }
43
+ element = element.parentElement;
44
+ }
45
+ return false;
46
+ };
47
+ export function useSheetDrag({ enabled, panelRef, scrollElement, snapPoints, current, openedAtRef, onDragStart, onSnap, onSettle, onHandleTap, }) {
35
48
  const gestureRef = useRef(null);
36
49
  const scrollBlockedAtRef = useRef(0);
37
50
  const settleRef = useRef(null);
@@ -142,11 +155,44 @@ export function useSheetDrag({ enabled, panelRef, scrollRef, snapPoints, current
142
155
  const startOffset = liveOffset ?? offsetOf(stateRef.current.current);
143
156
  const gesture = {
144
157
  pointerId: event.pointerId,
158
+ startX: event.clientX,
145
159
  startY: event.clientY,
160
+ lastY: event.clientY,
146
161
  startTime: performance.now(),
147
162
  startOffset,
148
163
  offset: startOffset,
149
164
  committed: false,
165
+ commit: (clientX, clientY) => {
166
+ if (gesture.committed)
167
+ return true;
168
+ const deltaX = clientX - gesture.startX;
169
+ const deltaY = clientY - gesture.startY;
170
+ if (Math.hypot(deltaX, deltaY) < MOVE_TOLERANCE)
171
+ return false;
172
+ if (Math.abs(deltaX) > Math.abs(deltaY)) {
173
+ // A sideways move belongs to a chip row or gallery strip under the finger. Anywhere
174
+ // else it is the finger settling, which says nothing about the direction yet.
175
+ if (scrollsSideways(event.target, panelRef.current))
176
+ stop();
177
+ return false;
178
+ }
179
+ if (!shouldDrag(event.target, deltaY > 0)) {
180
+ stop();
181
+ return false;
182
+ }
183
+ gesture.committed = true;
184
+ try {
185
+ panelRef.current?.setPointerCapture(gesture.pointerId);
186
+ }
187
+ catch {
188
+ // Throws when the pointer is already gone; the window listeners
189
+ // below carry the gesture either way.
190
+ }
191
+ if (scrollElement)
192
+ scrollElement.style.overflow = 'hidden';
193
+ callbacksRef.current.onDragStart();
194
+ return true;
195
+ },
150
196
  };
151
197
  gestureRef.current = gesture;
152
198
  const stop = () => {
@@ -161,9 +207,8 @@ export function useSheetDrag({ enabled, panelRef, scrollRef, snapPoints, current
161
207
  if (endEvent.pointerId !== gesture.pointerId)
162
208
  return;
163
209
  stop();
164
- const scrollEl = scrollRef.current;
165
- if (scrollEl)
166
- scrollEl.style.removeProperty('overflow');
210
+ if (scrollElement)
211
+ scrollElement.style.removeProperty('overflow');
167
212
  if (!gesture.committed) {
168
213
  const target = event.target;
169
214
  if (target instanceof Element && target.closest('[data-sheet-handle]')) {
@@ -171,7 +216,7 @@ export function useSheetDrag({ enabled, panelRef, scrollRef, snapPoints, current
171
216
  }
172
217
  return;
173
218
  }
174
- const distance = gesture.startY - endEvent.clientY;
219
+ const distance = gesture.startY - gesture.lastY;
175
220
  const elapsed = Math.max(performance.now() - gesture.startTime, 1);
176
221
  const velocity = Math.abs(distance) / elapsed;
177
222
  const target = resolveTarget(gesture.offset, distance, velocity);
@@ -182,27 +227,10 @@ export function useSheetDrag({ enabled, panelRef, scrollRef, snapPoints, current
182
227
  const handleMove = (moveEvent) => {
183
228
  if (moveEvent.pointerId !== gesture.pointerId)
184
229
  return;
230
+ gesture.lastY = moveEvent.clientY;
231
+ if (!gesture.commit(moveEvent.clientX, moveEvent.clientY))
232
+ return;
185
233
  const delta = moveEvent.clientY - gesture.startY;
186
- if (!gesture.committed) {
187
- if (Math.abs(delta) < MOVE_TOLERANCE)
188
- return;
189
- if (!shouldDrag(event.target, delta > 0)) {
190
- stop();
191
- return;
192
- }
193
- gesture.committed = true;
194
- try {
195
- panelRef.current?.setPointerCapture(gesture.pointerId);
196
- }
197
- catch {
198
- // Throws when the pointer is already gone; the window listeners
199
- // below carry the gesture either way.
200
- }
201
- const scrollEl = scrollRef.current;
202
- if (scrollEl)
203
- scrollEl.style.overflow = 'hidden';
204
- callbacksRef.current.onDragStart();
205
- }
206
234
  const limit = offsetOf(stateRef.current.snapPoints[0].name);
207
235
  const raw = gesture.startOffset + delta;
208
236
  const offset = raw < 0 ? -dampen(-raw) : raw > limit ? limit + dampen(raw - limit) : raw;
@@ -219,9 +247,28 @@ export function useSheetDrag({ enabled, panelRef, scrollRef, snapPoints, current
219
247
  panelRef,
220
248
  resolveTarget,
221
249
  scheduleSettle,
222
- scrollRef,
250
+ scrollElement,
223
251
  shouldDrag,
224
252
  writeOffset,
225
253
  ]);
254
+ // Non-passive, so bound natively (React's touch listeners are passive), and in place before the
255
+ // touch starts. A drag that starts on the content must keep the browser from scrolling it: a
256
+ // native scroll takes the touch over and cancels the pointer mid-drag (#1686). Once that scroll
257
+ // starts, touchmoves are no longer cancelable, and a pointermove can arrive a frame after its
258
+ // touchmove, so the drag is decided here as well.
259
+ useEffect(() => {
260
+ if (!scrollElement || !enabled)
261
+ return;
262
+ const handleTouchMove = (event) => {
263
+ const gesture = gestureRef.current;
264
+ const touch = event.targetTouches[0];
265
+ if (!gesture || !touch || !event.cancelable)
266
+ return;
267
+ if (gesture.commit(touch.clientX, touch.clientY))
268
+ event.preventDefault();
269
+ };
270
+ scrollElement.addEventListener('touchmove', handleTouchMove, { passive: false });
271
+ return () => scrollElement.removeEventListener('touchmove', handleTouchMove);
272
+ }, [scrollElement, enabled]);
226
273
  return { onPointerDown };
227
274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/ui",
3
- "version": "3.42.1",
3
+ "version": "3.43.1",
4
4
  "type": "module",
5
5
  "description": "ExpoFP SDK internal: shared pure React UI components",
6
6
  "homepage": "https://developer.expofp.com/",