@astryxdesign/lab 0.4.2-canary.10bcaa1 → 0.4.2-canary.255e810
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/dist/BottomSheet/useSheetGestures.d.ts +5 -1
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +77 -8
- package/package.json +3 -3
- package/src/BottomSheet/BottomSheet.test.tsx +36 -1
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +48 -2
- package/src/BottomSheet/useSheetGestures.test.ts +96 -0
- package/src/BottomSheet/useSheetGestures.ts +113 -8
- package/src/ListInput/ListInput.doc.mjs +8 -0
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* - /packages/lab/src/BottomSheet/useSheetGestures.doc.mjs
|
|
23
23
|
* - /packages/lab/src/BottomSheet/useSheetGestures.test.ts
|
|
24
24
|
*/
|
|
25
|
-
import { type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
|
|
25
|
+
import { type CSSProperties, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent } from 'react';
|
|
26
26
|
export interface UseSheetGesturesOptions {
|
|
27
27
|
/** Whether the owning sheet is open. Drag state resets when it closes. */
|
|
28
28
|
isOpen: boolean;
|
|
@@ -59,6 +59,8 @@ export interface SheetContentProps {
|
|
|
59
59
|
}
|
|
60
60
|
export interface SheetHandleProps {
|
|
61
61
|
style: CSSProperties;
|
|
62
|
+
onContextMenu: (event: ReactMouseEvent) => void;
|
|
63
|
+
onLostPointerCapture: (event: ReactPointerEvent) => void;
|
|
62
64
|
onPointerDown: (event: ReactPointerEvent) => void;
|
|
63
65
|
onPointerMove: (event: ReactPointerEvent) => void;
|
|
64
66
|
onPointerUp: (event: ReactPointerEvent) => void;
|
|
@@ -66,6 +68,8 @@ export interface SheetHandleProps {
|
|
|
66
68
|
}
|
|
67
69
|
export interface SheetBodyProps {
|
|
68
70
|
ref: (node: HTMLElement | null) => void;
|
|
71
|
+
onContextMenu: (event: ReactMouseEvent) => void;
|
|
72
|
+
onLostPointerCapture: (event: ReactPointerEvent) => void;
|
|
69
73
|
onPointerDown: (event: ReactPointerEvent) => void;
|
|
70
74
|
onPointerMove: (event: ReactPointerEvent) => void;
|
|
71
75
|
onPointerUp: (event: ReactPointerEvent) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSheetGestures.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/useSheetGestures.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,YAAY,IAAI,iBAAiB,EACvC,MAAM,OAAO,CAAC;AAgEf,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,MAAM,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gFAAgF;IAChF,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IAC7B,4DAA4D;IAC5D,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,aAAa,CAAC;IACrB,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,WAAW,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChD,eAAe,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,WAAW,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChD,eAAe,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,0EAA0E;IAC1E,YAAY,EAAE,iBAAiB,CAAC;IAChC,gEAAgE;IAChE,WAAW,EAAE,gBAAgB,CAAC;IAC9B;;;;OAIG;IACH,SAAS,EAAE,cAAc,CAAC;IAC1B,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,UAAU,EAAE,OAAO,CAAC;CACrB;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,UAAiB,EACjB,SAAS,EACT,WAAW,EACX,MAAM,EACN,cAAc,GACf,EAAE,uBAAuB,GAAG,sBAAsB,
|
|
1
|
+
{"version":3,"file":"useSheetGestures.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/useSheetGestures.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,UAAU,IAAI,eAAe,EAClC,KAAK,YAAY,IAAI,iBAAiB,EACvC,MAAM,OAAO,CAAC;AAgEf,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,MAAM,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gFAAgF;IAChF,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IAC7B,4DAA4D;IAC5D,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,aAAa,CAAC;IACrB,aAAa,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD,oBAAoB,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACzD,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,WAAW,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChD,eAAe,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD,oBAAoB,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACzD,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,WAAW,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChD,eAAe,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,0EAA0E;IAC1E,YAAY,EAAE,iBAAiB,CAAC;IAChC,gEAAgE;IAChE,WAAW,EAAE,gBAAgB,CAAC;IAC9B;;;;OAIG;IACH,SAAS,EAAE,cAAc,CAAC;IAC1B,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,UAAU,EAAE,OAAO,CAAC;CACrB;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,UAAiB,EACjB,SAAS,EACT,WAAW,EACX,MAAM,EACN,cAAc,GACf,EAAE,uBAAuB,GAAG,sBAAsB,CAolBlD"}
|
|
@@ -188,6 +188,44 @@ export function useSheetGestures({
|
|
|
188
188
|
const detentOffsets = useCallback(height => {
|
|
189
189
|
return computeDetentOffsets(height, snapHeightsRef.current?.() ?? []);
|
|
190
190
|
}, []);
|
|
191
|
+
const cancelDrag = useCallback(target => {
|
|
192
|
+
const state = dragStateRef.current;
|
|
193
|
+
if (state == null) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Clear first: releasePointerCapture() may synchronously dispatch
|
|
198
|
+
// lostpointercapture, which must observe that this drag is already done.
|
|
199
|
+
dragStateRef.current = null;
|
|
200
|
+
if (target?.hasPointerCapture?.(state.pointerId)) {
|
|
201
|
+
target.releasePointerCapture(state.pointerId);
|
|
202
|
+
}
|
|
203
|
+
setDragOffset(state.baseOffset);
|
|
204
|
+
setIsDragging(false);
|
|
205
|
+
|
|
206
|
+
// An interrupted drag returns to its previous resting detent. Restore
|
|
207
|
+
// the matching scrim opacity as well so the modal shell cannot remain
|
|
208
|
+
// dimmed with its sheet translated out of view.
|
|
209
|
+
const offsets = detentOffsets(state.height);
|
|
210
|
+
const maxOffset = offsets[offsets.length - 1];
|
|
211
|
+
const shortestDetentHeight = state.height - maxOffset;
|
|
212
|
+
const dismissOffset = maxOffset + shortestDetentHeight * DISMISS_OVERSHOOT_RATIO;
|
|
213
|
+
onScrimOpacityRef.current?.(scrimOpacityForOffset(state.baseOffset, offsets, dismissOffset));
|
|
214
|
+
}, [detentOffsets]);
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const handleWindowBlur = () => cancelDrag();
|
|
217
|
+
const handleVisibilityChange = () => {
|
|
218
|
+
if (document.visibilityState === 'hidden') {
|
|
219
|
+
cancelDrag();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
window.addEventListener('blur', handleWindowBlur);
|
|
223
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
224
|
+
return () => {
|
|
225
|
+
window.removeEventListener('blur', handleWindowBlur);
|
|
226
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
227
|
+
};
|
|
228
|
+
}, [cancelDrag]);
|
|
191
229
|
const settleFromDrag = useCallback((offset, velocity, height, dir, travel, baseOffset) => {
|
|
192
230
|
const offsets = detentOffsets(height);
|
|
193
231
|
const maxOffset = offsets[offsets.length - 1];
|
|
@@ -255,12 +293,27 @@ export function useSheetGestures({
|
|
|
255
293
|
setIsDragging(true);
|
|
256
294
|
}, [settledOffset]);
|
|
257
295
|
const handlePointerDown = useCallback(event => {
|
|
296
|
+
if (event.button !== 0 || !event.isPrimary) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
258
299
|
// The handle has no native focus action. Prevent pointer-down from
|
|
259
300
|
// moving focus off a form control on a tap; once the pointer actually
|
|
260
301
|
// moves, BottomSheet dismisses the keyboard as sheet travel begins.
|
|
261
302
|
event.preventDefault();
|
|
262
303
|
beginDrag(event, measureHeight());
|
|
263
304
|
}, [beginDrag, measureHeight]);
|
|
305
|
+
const handleContextMenu = useCallback(event => {
|
|
306
|
+
if (dragStateRef.current == null) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
event.preventDefault();
|
|
310
|
+
cancelDrag(event.currentTarget);
|
|
311
|
+
}, [cancelDrag]);
|
|
312
|
+
const handleLostPointerCapture = useCallback(event => {
|
|
313
|
+
if (dragStateRef.current?.pointerId === event.pointerId) {
|
|
314
|
+
cancelDrag();
|
|
315
|
+
}
|
|
316
|
+
}, [cancelDrag]);
|
|
264
317
|
const handlePointerMove = useCallback(event => {
|
|
265
318
|
const state = dragStateRef.current;
|
|
266
319
|
if (!state || state.pointerId !== event.pointerId) {
|
|
@@ -303,11 +356,11 @@ export function useSheetGestures({
|
|
|
303
356
|
return;
|
|
304
357
|
}
|
|
305
358
|
const target = event.currentTarget;
|
|
306
|
-
target.releasePointerCapture?.(event.pointerId);
|
|
307
359
|
const delta = event.clientY - state.startCoord;
|
|
308
360
|
const offset = Math.max(0, state.baseOffset + delta);
|
|
309
361
|
const dir = delta === 0 ? 0 : delta > 0 ? 1 : -1;
|
|
310
362
|
dragStateRef.current = null;
|
|
363
|
+
target.releasePointerCapture?.(event.pointerId);
|
|
311
364
|
setIsDragging(false);
|
|
312
365
|
settleFromDrag(offset, state.velocity, state.height || 1, dir, Math.abs(delta), state.baseOffset);
|
|
313
366
|
}, [settleFromDrag]);
|
|
@@ -317,6 +370,9 @@ export function useSheetGestures({
|
|
|
317
370
|
// native pan starts.
|
|
318
371
|
const armedBodyRef = useRef(null);
|
|
319
372
|
const handleBodyPointerDown = useCallback(event => {
|
|
373
|
+
if (event.button !== 0 || !event.isPrimary) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
320
376
|
const scroller = event.currentTarget;
|
|
321
377
|
if (scroller.scrollTop > 0) {
|
|
322
378
|
armedBodyRef.current = null;
|
|
@@ -363,11 +419,13 @@ export function useSheetGestures({
|
|
|
363
419
|
const touchDragRef = useRef(null);
|
|
364
420
|
const previousTouchHandlersRef = useRef(null);
|
|
365
421
|
const beginDragRef = useRef(beginDrag);
|
|
422
|
+
const cancelDragRef = useRef(cancelDrag);
|
|
366
423
|
const pointerMoveRef = useRef(handlePointerMove);
|
|
367
424
|
const endDragRef = useRef(endDrag);
|
|
368
425
|
const measureHeightRef = useRef(measureHeight);
|
|
369
426
|
useEffect(() => {
|
|
370
427
|
beginDragRef.current = beginDrag;
|
|
428
|
+
cancelDragRef.current = cancelDrag;
|
|
371
429
|
pointerMoveRef.current = handlePointerMove;
|
|
372
430
|
endDragRef.current = endDrag;
|
|
373
431
|
measureHeightRef.current = measureHeight;
|
|
@@ -443,11 +501,18 @@ export function useSheetGestures({
|
|
|
443
501
|
};
|
|
444
502
|
const onTouchEnd = event => {
|
|
445
503
|
touchDragRef.current = null;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
504
|
+
const pointerId = dragStateRef.current?.pointerId;
|
|
505
|
+
if (pointerId == null) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const t = [...event.changedTouches].find(touch => touch.identifier === pointerId);
|
|
509
|
+
const target = event.currentTarget;
|
|
510
|
+
if (t) {
|
|
511
|
+
endDragRef.current(asPointer(t, target));
|
|
512
|
+
} else if (event.touches.length === 0) {
|
|
513
|
+
// Some interrupted multi-touch sequences omit the active touch from
|
|
514
|
+
// changedTouches. If no fingers remain, the drag cannot finish later.
|
|
515
|
+
cancelDragRef.current(target);
|
|
451
516
|
}
|
|
452
517
|
};
|
|
453
518
|
const prev = bodyNodeRef.current;
|
|
@@ -498,18 +563,22 @@ export function useSheetGestures({
|
|
|
498
563
|
touchAction: 'none',
|
|
499
564
|
cursor: 'grab'
|
|
500
565
|
},
|
|
566
|
+
onContextMenu: handleContextMenu,
|
|
567
|
+
onLostPointerCapture: handleLostPointerCapture,
|
|
501
568
|
onPointerDown: handlePointerDown,
|
|
502
569
|
onPointerMove: handlePointerMove,
|
|
503
570
|
onPointerUp: endDrag,
|
|
504
571
|
onPointerCancel: endDrag
|
|
505
|
-
}), [handlePointerDown, handlePointerMove
|
|
572
|
+
}), [endDrag, handleContextMenu, handleLostPointerCapture, handlePointerDown, handlePointerMove]);
|
|
506
573
|
const bodyProps = useMemo(() => ({
|
|
507
574
|
ref: bodyRef,
|
|
575
|
+
onContextMenu: handleContextMenu,
|
|
576
|
+
onLostPointerCapture: handleLostPointerCapture,
|
|
508
577
|
onPointerDown: handleBodyPointerDown,
|
|
509
578
|
onPointerMove: handleBodyPointerMove,
|
|
510
579
|
onPointerUp: handleBodyEnd,
|
|
511
580
|
onPointerCancel: handleBodyEnd
|
|
512
|
-
}), [bodyRef, handleBodyPointerDown, handleBodyPointerMove,
|
|
581
|
+
}), [bodyRef, handleBodyEnd, handleBodyPointerDown, handleBodyPointerMove, handleContextMenu, handleLostPointerCapture]);
|
|
513
582
|
return {
|
|
514
583
|
sheetRef,
|
|
515
584
|
contentProps,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/lab",
|
|
3
|
-
"version": "0.4.2-canary.
|
|
3
|
+
"version": "0.4.2-canary.255e810",
|
|
4
4
|
"description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
|
|
5
5
|
"author": "Meta Open Source",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@stylexjs/stylex": ">=0.10.0",
|
|
48
|
-
"@astryxdesign/core": "0.4.2-canary.
|
|
48
|
+
"@astryxdesign/core": "0.4.2-canary.255e810",
|
|
49
49
|
"react": ">=19.0.0",
|
|
50
50
|
"react-dom": ">=19.0.0"
|
|
51
51
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"d3-shape": "^3.2.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@astryxdesign/cli": "0.4.2-canary.
|
|
58
|
+
"@astryxdesign/cli": "0.4.2-canary.255e810",
|
|
59
59
|
"@types/d3-array": "^3.2.2",
|
|
60
60
|
"@types/d3-scale": "^4.0.9",
|
|
61
61
|
"@types/d3-shape": "^3.1.8",
|
|
@@ -185,7 +185,12 @@ function getHandle(): HTMLElement {
|
|
|
185
185
|
// clientY, so dispatch plain events with the coords the handlers read.
|
|
186
186
|
function drag(handle: HTMLElement, points: {y: number}[]) {
|
|
187
187
|
const [down, ...rest] = points;
|
|
188
|
-
fireEvent.pointerDown(handle, {
|
|
188
|
+
fireEvent.pointerDown(handle, {
|
|
189
|
+
pointerId: 1,
|
|
190
|
+
clientY: down.y,
|
|
191
|
+
button: 0,
|
|
192
|
+
isPrimary: true,
|
|
193
|
+
});
|
|
189
194
|
for (const p of rest) {
|
|
190
195
|
fireEvent.pointerMove(handle, {pointerId: 1, clientY: p.y});
|
|
191
196
|
}
|
|
@@ -216,7 +221,9 @@ function fireTimedPointer(
|
|
|
216
221
|
) {
|
|
217
222
|
const event = new Event(type, {bubbles: true, cancelable: true});
|
|
218
223
|
Object.defineProperties(event, {
|
|
224
|
+
button: {value: 0},
|
|
219
225
|
clientY: {value: y},
|
|
226
|
+
isPrimary: {value: true},
|
|
220
227
|
pointerId: {value: 1},
|
|
221
228
|
timeStamp: {value: time},
|
|
222
229
|
});
|
|
@@ -534,6 +541,32 @@ describe('BottomSheet', () => {
|
|
|
534
541
|
drag(getHandle(), [{y: 0}, {y: 40}, {y: 120}]);
|
|
535
542
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
536
543
|
});
|
|
544
|
+
|
|
545
|
+
it('restores the sheet when a context menu interrupts an active drag', () => {
|
|
546
|
+
const onOpenChange = vi.fn();
|
|
547
|
+
render(
|
|
548
|
+
<BottomSheet isOpen onOpenChange={onOpenChange} label="Filters">
|
|
549
|
+
Content
|
|
550
|
+
</BottomSheet>,
|
|
551
|
+
);
|
|
552
|
+
const handle = getHandle();
|
|
553
|
+
const dialog = screen.getByRole('dialog');
|
|
554
|
+
|
|
555
|
+
fireEvent.pointerDown(handle, {
|
|
556
|
+
pointerId: 1,
|
|
557
|
+
clientY: 0,
|
|
558
|
+
button: 0,
|
|
559
|
+
isPrimary: true,
|
|
560
|
+
});
|
|
561
|
+
fireEvent.pointerMove(handle, {pointerId: 1, clientY: 300});
|
|
562
|
+
expect(getSheet().style.transform).toBe('translateY(300px)');
|
|
563
|
+
|
|
564
|
+
expect(fireEvent.contextMenu(handle)).toBe(false);
|
|
565
|
+
|
|
566
|
+
expect(getSheet().style.transform).toBe('');
|
|
567
|
+
expect(dialog).toHaveStyle({'--_sheet-scrim-opacity': '1'});
|
|
568
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
569
|
+
});
|
|
537
570
|
});
|
|
538
571
|
|
|
539
572
|
describe('height', () => {
|
|
@@ -1122,6 +1155,8 @@ describe('BottomSheet', () => {
|
|
|
1122
1155
|
const pointerDownAllowed = fireEvent.pointerDown(getHandle(), {
|
|
1123
1156
|
pointerId: 1,
|
|
1124
1157
|
clientY: 0,
|
|
1158
|
+
button: 0,
|
|
1159
|
+
isPrimary: true,
|
|
1125
1160
|
});
|
|
1126
1161
|
expect(pointerDownAllowed).toBe(false);
|
|
1127
1162
|
expect(document.activeElement).toBe(input);
|
|
@@ -484,6 +484,8 @@ describe('BottomSheetSwitcher', () => {
|
|
|
484
484
|
pointerId: 1,
|
|
485
485
|
clientY: 100,
|
|
486
486
|
timeStamp: 0,
|
|
487
|
+
button: 0,
|
|
488
|
+
isPrimary: true,
|
|
487
489
|
});
|
|
488
490
|
fireEvent.pointerMove(handle, {
|
|
489
491
|
pointerId: 1,
|
|
@@ -665,7 +667,12 @@ describe('BottomSheetSwitcher', () => {
|
|
|
665
667
|
}
|
|
666
668
|
|
|
667
669
|
fireEvent.click(dialog);
|
|
668
|
-
fireEvent.pointerDown(handle, {
|
|
670
|
+
fireEvent.pointerDown(handle, {
|
|
671
|
+
pointerId: 1,
|
|
672
|
+
clientY: 0,
|
|
673
|
+
button: 0,
|
|
674
|
+
isPrimary: true,
|
|
675
|
+
});
|
|
669
676
|
fireEvent.pointerMove(handle, {pointerId: 1, clientY: 120});
|
|
670
677
|
fireEvent.pointerUp(handle, {pointerId: 1, clientY: 120});
|
|
671
678
|
|
|
@@ -704,7 +711,12 @@ describe('BottomSheetSwitcher', () => {
|
|
|
704
711
|
fireEvent.click(dialog);
|
|
705
712
|
fireEvent.keyDown(dialog, {key: 'Escape'});
|
|
706
713
|
fireEvent(dialog, new Event('cancel', {cancelable: true}));
|
|
707
|
-
fireEvent.pointerDown(handle, {
|
|
714
|
+
fireEvent.pointerDown(handle, {
|
|
715
|
+
pointerId: 1,
|
|
716
|
+
clientY: 0,
|
|
717
|
+
button: 0,
|
|
718
|
+
isPrimary: true,
|
|
719
|
+
});
|
|
708
720
|
fireEvent.pointerMove(handle, {pointerId: 1, clientY: 120});
|
|
709
721
|
fireEvent.pointerUp(handle, {pointerId: 1, clientY: 120});
|
|
710
722
|
|
|
@@ -712,6 +724,40 @@ describe('BottomSheetSwitcher', () => {
|
|
|
712
724
|
expect(dialog).toHaveStyle({'--_sheet-scrim-opacity': '1'});
|
|
713
725
|
});
|
|
714
726
|
|
|
727
|
+
it('restores the active sheet when a context menu interrupts its drag', () => {
|
|
728
|
+
const onActiveSheetChange = vi.fn();
|
|
729
|
+
render(
|
|
730
|
+
<BottomSheetSwitcher
|
|
731
|
+
activeSheet="details"
|
|
732
|
+
onActiveSheetChange={onActiveSheetChange}>
|
|
733
|
+
<BottomSheet sheetId="details" label="Details">
|
|
734
|
+
Content
|
|
735
|
+
</BottomSheet>
|
|
736
|
+
</BottomSheetSwitcher>,
|
|
737
|
+
);
|
|
738
|
+
const dialog = getSharedDialog();
|
|
739
|
+
const panel = getSheetPanel(dialog);
|
|
740
|
+
const handle = panel.firstElementChild;
|
|
741
|
+
if (!(handle instanceof HTMLElement)) {
|
|
742
|
+
throw new Error('sheet handle not found');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
fireEvent.pointerDown(handle, {
|
|
746
|
+
pointerId: 1,
|
|
747
|
+
clientY: 0,
|
|
748
|
+
button: 0,
|
|
749
|
+
isPrimary: true,
|
|
750
|
+
});
|
|
751
|
+
fireEvent.pointerMove(handle, {pointerId: 1, clientY: 300});
|
|
752
|
+
expect(panel.style.transform).toBe('translateY(300px)');
|
|
753
|
+
|
|
754
|
+
expect(fireEvent.contextMenu(handle)).toBe(false);
|
|
755
|
+
|
|
756
|
+
expect(panel.style.transform).toBe('');
|
|
757
|
+
expect(dialog).toHaveStyle({'--_sheet-scrim-opacity': '1'});
|
|
758
|
+
expect(onActiveSheetChange).not.toHaveBeenCalled();
|
|
759
|
+
});
|
|
760
|
+
|
|
715
761
|
it('ignores Escape while an IME composition is active', () => {
|
|
716
762
|
const onActiveSheetChange = vi.fn();
|
|
717
763
|
render(
|
|
@@ -26,9 +26,13 @@ function pointerEvent(
|
|
|
26
26
|
timeStamp: number,
|
|
27
27
|
target: HTMLElement,
|
|
28
28
|
pointerId = 1,
|
|
29
|
+
button = 0,
|
|
30
|
+
isPrimary = true,
|
|
29
31
|
) {
|
|
30
32
|
return {
|
|
31
33
|
pointerId,
|
|
34
|
+
button,
|
|
35
|
+
isPrimary,
|
|
32
36
|
clientY,
|
|
33
37
|
timeStamp,
|
|
34
38
|
currentTarget: target,
|
|
@@ -288,6 +292,62 @@ describe('useSheetGestures', () => {
|
|
|
288
292
|
);
|
|
289
293
|
});
|
|
290
294
|
|
|
295
|
+
it('returns to the settled detent when a context menu interrupts a drag', () => {
|
|
296
|
+
const onScrimOpacity = vi.fn();
|
|
297
|
+
const {hook, onDismiss} = setup({
|
|
298
|
+
snapHeights: () => [200, 300],
|
|
299
|
+
onScrimOpacity,
|
|
300
|
+
});
|
|
301
|
+
const target = makeTarget();
|
|
302
|
+
down(hook, 0, 0, target);
|
|
303
|
+
move(hook, 300, 1000, target);
|
|
304
|
+
|
|
305
|
+
const preventDefault = vi.fn();
|
|
306
|
+
act(() =>
|
|
307
|
+
hook.result.current.handleProps.onContextMenu({
|
|
308
|
+
currentTarget: target,
|
|
309
|
+
preventDefault,
|
|
310
|
+
} as unknown as React.MouseEvent),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
expect(preventDefault).toHaveBeenCalledTimes(1);
|
|
314
|
+
expect(hook.result.current.isDragging).toBe(false);
|
|
315
|
+
expect(hook.result.current.contentProps.style.transform).toBeUndefined();
|
|
316
|
+
expect(onScrimOpacity).toHaveBeenLastCalledWith(1);
|
|
317
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('returns to the settled detent when pointer capture is lost', () => {
|
|
321
|
+
const {hook, onDismiss} = setup({snapHeights: () => [200]});
|
|
322
|
+
const target = makeTarget();
|
|
323
|
+
down(hook, 0, 0, target);
|
|
324
|
+
move(hook, 300, 1000, target);
|
|
325
|
+
|
|
326
|
+
act(() =>
|
|
327
|
+
hook.result.current.handleProps.onLostPointerCapture(
|
|
328
|
+
pointerEvent(300, 1000, target),
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
expect(hook.result.current.isDragging).toBe(false);
|
|
333
|
+
expect(hook.result.current.contentProps.style.transform).toBeUndefined();
|
|
334
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('does not start a drag from a secondary pointer button', () => {
|
|
338
|
+
const {hook} = setup();
|
|
339
|
+
const target = makeTarget();
|
|
340
|
+
act(() => hook.result.current.sheetRef(target));
|
|
341
|
+
|
|
342
|
+
act(() =>
|
|
343
|
+
hook.result.current.handleProps.onPointerDown(
|
|
344
|
+
pointerEvent(0, 0, target, 1, 2),
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(hook.result.current.isDragging).toBe(false);
|
|
349
|
+
});
|
|
350
|
+
|
|
291
351
|
it('rubber-bands an upward drag past the fully-open position', () => {
|
|
292
352
|
const {hook} = setup({snapHeights: () => [200]});
|
|
293
353
|
const t = makeTarget();
|
|
@@ -394,6 +454,12 @@ describe('useSheetGestures', () => {
|
|
|
394
454
|
Object.defineProperty(ev, 'changedTouches', {
|
|
395
455
|
value: [{identifier: id, clientY: y}],
|
|
396
456
|
});
|
|
457
|
+
Object.defineProperty(ev, 'touches', {
|
|
458
|
+
value:
|
|
459
|
+
type === 'touchend' || type === 'touchcancel'
|
|
460
|
+
? []
|
|
461
|
+
: [{identifier: id, clientY: y}],
|
|
462
|
+
});
|
|
397
463
|
Object.defineProperty(ev, 'currentTarget', {value: el});
|
|
398
464
|
el.dispatchEvent(ev);
|
|
399
465
|
return ev;
|
|
@@ -432,6 +498,36 @@ describe('useSheetGestures', () => {
|
|
|
432
498
|
expect(hook.result.current.isDragging).toBe(true);
|
|
433
499
|
});
|
|
434
500
|
|
|
501
|
+
it('finishes the active drag when another ended touch is listed first', () => {
|
|
502
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
503
|
+
const el = makeScroller({
|
|
504
|
+
scrollTop: 0,
|
|
505
|
+
clientHeight: 200,
|
|
506
|
+
scrollHeight: 800,
|
|
507
|
+
});
|
|
508
|
+
act(() => hook.result.current.sheetRef(el));
|
|
509
|
+
act(() => hook.result.current.bodyProps.ref(el));
|
|
510
|
+
act(() => {
|
|
511
|
+
touch(el, 'touchstart', 0, 2);
|
|
512
|
+
touch(el, 'touchmove', 300, 2);
|
|
513
|
+
|
|
514
|
+
const end = new Event('touchend', {
|
|
515
|
+
bubbles: true,
|
|
516
|
+
cancelable: true,
|
|
517
|
+
});
|
|
518
|
+
Object.defineProperty(end, 'changedTouches', {
|
|
519
|
+
value: [
|
|
520
|
+
{identifier: 1, clientY: 100},
|
|
521
|
+
{identifier: 2, clientY: 300},
|
|
522
|
+
],
|
|
523
|
+
});
|
|
524
|
+
Object.defineProperty(end, 'touches', {value: []});
|
|
525
|
+
el.dispatchEvent(end);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
expect(hook.result.current.isDragging).toBe(false);
|
|
529
|
+
});
|
|
530
|
+
|
|
435
531
|
it('leaves native scrolling alone in the middle of the content', () => {
|
|
436
532
|
const {hook} = setup({snapHeights: () => [200]});
|
|
437
533
|
const el = makeScroller({
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
useRef,
|
|
35
35
|
useState,
|
|
36
36
|
type CSSProperties,
|
|
37
|
+
type MouseEvent as ReactMouseEvent,
|
|
37
38
|
type PointerEvent as ReactPointerEvent,
|
|
38
39
|
} from 'react';
|
|
39
40
|
import {
|
|
@@ -137,6 +138,8 @@ export interface SheetContentProps {
|
|
|
137
138
|
|
|
138
139
|
export interface SheetHandleProps {
|
|
139
140
|
style: CSSProperties;
|
|
141
|
+
onContextMenu: (event: ReactMouseEvent) => void;
|
|
142
|
+
onLostPointerCapture: (event: ReactPointerEvent) => void;
|
|
140
143
|
onPointerDown: (event: ReactPointerEvent) => void;
|
|
141
144
|
onPointerMove: (event: ReactPointerEvent) => void;
|
|
142
145
|
onPointerUp: (event: ReactPointerEvent) => void;
|
|
@@ -145,6 +148,8 @@ export interface SheetHandleProps {
|
|
|
145
148
|
|
|
146
149
|
export interface SheetBodyProps {
|
|
147
150
|
ref: (node: HTMLElement | null) => void;
|
|
151
|
+
onContextMenu: (event: ReactMouseEvent) => void;
|
|
152
|
+
onLostPointerCapture: (event: ReactPointerEvent) => void;
|
|
148
153
|
onPointerDown: (event: ReactPointerEvent) => void;
|
|
149
154
|
onPointerMove: (event: ReactPointerEvent) => void;
|
|
150
155
|
onPointerUp: (event: ReactPointerEvent) => void;
|
|
@@ -292,6 +297,52 @@ export function useSheetGestures({
|
|
|
292
297
|
return computeDetentOffsets(height, snapHeightsRef.current?.() ?? []);
|
|
293
298
|
}, []);
|
|
294
299
|
|
|
300
|
+
const cancelDrag = useCallback(
|
|
301
|
+
(target?: HTMLElement) => {
|
|
302
|
+
const state = dragStateRef.current;
|
|
303
|
+
if (state == null) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Clear first: releasePointerCapture() may synchronously dispatch
|
|
308
|
+
// lostpointercapture, which must observe that this drag is already done.
|
|
309
|
+
dragStateRef.current = null;
|
|
310
|
+
if (target?.hasPointerCapture?.(state.pointerId)) {
|
|
311
|
+
target.releasePointerCapture(state.pointerId);
|
|
312
|
+
}
|
|
313
|
+
setDragOffset(state.baseOffset);
|
|
314
|
+
setIsDragging(false);
|
|
315
|
+
|
|
316
|
+
// An interrupted drag returns to its previous resting detent. Restore
|
|
317
|
+
// the matching scrim opacity as well so the modal shell cannot remain
|
|
318
|
+
// dimmed with its sheet translated out of view.
|
|
319
|
+
const offsets = detentOffsets(state.height);
|
|
320
|
+
const maxOffset = offsets[offsets.length - 1];
|
|
321
|
+
const shortestDetentHeight = state.height - maxOffset;
|
|
322
|
+
const dismissOffset =
|
|
323
|
+
maxOffset + shortestDetentHeight * DISMISS_OVERSHOOT_RATIO;
|
|
324
|
+
onScrimOpacityRef.current?.(
|
|
325
|
+
scrimOpacityForOffset(state.baseOffset, offsets, dismissOffset),
|
|
326
|
+
);
|
|
327
|
+
},
|
|
328
|
+
[detentOffsets],
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
const handleWindowBlur = () => cancelDrag();
|
|
333
|
+
const handleVisibilityChange = () => {
|
|
334
|
+
if (document.visibilityState === 'hidden') {
|
|
335
|
+
cancelDrag();
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
window.addEventListener('blur', handleWindowBlur);
|
|
339
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
340
|
+
return () => {
|
|
341
|
+
window.removeEventListener('blur', handleWindowBlur);
|
|
342
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
343
|
+
};
|
|
344
|
+
}, [cancelDrag]);
|
|
345
|
+
|
|
295
346
|
const settleFromDrag = useCallback(
|
|
296
347
|
(
|
|
297
348
|
offset: number,
|
|
@@ -379,6 +430,9 @@ export function useSheetGestures({
|
|
|
379
430
|
|
|
380
431
|
const handlePointerDown = useCallback(
|
|
381
432
|
(event: ReactPointerEvent) => {
|
|
433
|
+
if (event.button !== 0 || !event.isPrimary) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
382
436
|
// The handle has no native focus action. Prevent pointer-down from
|
|
383
437
|
// moving focus off a form control on a tap; once the pointer actually
|
|
384
438
|
// moves, BottomSheet dismisses the keyboard as sheet travel begins.
|
|
@@ -388,6 +442,26 @@ export function useSheetGestures({
|
|
|
388
442
|
[beginDrag, measureHeight],
|
|
389
443
|
);
|
|
390
444
|
|
|
445
|
+
const handleContextMenu = useCallback(
|
|
446
|
+
(event: ReactMouseEvent) => {
|
|
447
|
+
if (dragStateRef.current == null) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
event.preventDefault();
|
|
451
|
+
cancelDrag(event.currentTarget as HTMLElement);
|
|
452
|
+
},
|
|
453
|
+
[cancelDrag],
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const handleLostPointerCapture = useCallback(
|
|
457
|
+
(event: ReactPointerEvent) => {
|
|
458
|
+
if (dragStateRef.current?.pointerId === event.pointerId) {
|
|
459
|
+
cancelDrag();
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
[cancelDrag],
|
|
463
|
+
);
|
|
464
|
+
|
|
391
465
|
const handlePointerMove = useCallback(
|
|
392
466
|
(event: ReactPointerEvent) => {
|
|
393
467
|
const state = dragStateRef.current;
|
|
@@ -439,11 +513,11 @@ export function useSheetGestures({
|
|
|
439
513
|
return;
|
|
440
514
|
}
|
|
441
515
|
const target = event.currentTarget as HTMLElement;
|
|
442
|
-
target.releasePointerCapture?.(event.pointerId);
|
|
443
516
|
const delta = event.clientY - state.startCoord;
|
|
444
517
|
const offset = Math.max(0, state.baseOffset + delta);
|
|
445
518
|
const dir = delta === 0 ? 0 : delta > 0 ? 1 : -1;
|
|
446
519
|
dragStateRef.current = null;
|
|
520
|
+
target.releasePointerCapture?.(event.pointerId);
|
|
447
521
|
setIsDragging(false);
|
|
448
522
|
settleFromDrag(
|
|
449
523
|
offset,
|
|
@@ -467,6 +541,9 @@ export function useSheetGestures({
|
|
|
467
541
|
} | null>(null);
|
|
468
542
|
|
|
469
543
|
const handleBodyPointerDown = useCallback((event: ReactPointerEvent) => {
|
|
544
|
+
if (event.button !== 0 || !event.isPrimary) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
470
547
|
const scroller = event.currentTarget as HTMLElement;
|
|
471
548
|
if (scroller.scrollTop > 0) {
|
|
472
549
|
armedBodyRef.current = null;
|
|
@@ -531,11 +608,13 @@ export function useSheetGestures({
|
|
|
531
608
|
} | null>(null);
|
|
532
609
|
|
|
533
610
|
const beginDragRef = useRef(beginDrag);
|
|
611
|
+
const cancelDragRef = useRef(cancelDrag);
|
|
534
612
|
const pointerMoveRef = useRef(handlePointerMove);
|
|
535
613
|
const endDragRef = useRef(endDrag);
|
|
536
614
|
const measureHeightRef = useRef(measureHeight);
|
|
537
615
|
useEffect(() => {
|
|
538
616
|
beginDragRef.current = beginDrag;
|
|
617
|
+
cancelDragRef.current = cancelDrag;
|
|
539
618
|
pointerMoveRef.current = handlePointerMove;
|
|
540
619
|
endDragRef.current = endDrag;
|
|
541
620
|
measureHeightRef.current = measureHeight;
|
|
@@ -624,11 +703,20 @@ export function useSheetGestures({
|
|
|
624
703
|
|
|
625
704
|
const onTouchEnd = (event: TouchEvent) => {
|
|
626
705
|
touchDragRef.current = null;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
706
|
+
const pointerId = dragStateRef.current?.pointerId;
|
|
707
|
+
if (pointerId == null) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const t = [...event.changedTouches].find(
|
|
711
|
+
touch => touch.identifier === pointerId,
|
|
712
|
+
);
|
|
713
|
+
const target = event.currentTarget as HTMLElement;
|
|
714
|
+
if (t) {
|
|
715
|
+
endDragRef.current(asPointer(t, target));
|
|
716
|
+
} else if (event.touches.length === 0) {
|
|
717
|
+
// Some interrupted multi-touch sequences omit the active touch from
|
|
718
|
+
// changedTouches. If no fingers remain, the drag cannot finish later.
|
|
719
|
+
cancelDragRef.current(target);
|
|
632
720
|
}
|
|
633
721
|
};
|
|
634
722
|
|
|
@@ -677,23 +765,40 @@ export function useSheetGestures({
|
|
|
677
765
|
const handleProps = useMemo<SheetHandleProps>(
|
|
678
766
|
() => ({
|
|
679
767
|
style: {touchAction: 'none', cursor: 'grab'},
|
|
768
|
+
onContextMenu: handleContextMenu,
|
|
769
|
+
onLostPointerCapture: handleLostPointerCapture,
|
|
680
770
|
onPointerDown: handlePointerDown,
|
|
681
771
|
onPointerMove: handlePointerMove,
|
|
682
772
|
onPointerUp: endDrag,
|
|
683
773
|
onPointerCancel: endDrag,
|
|
684
774
|
}),
|
|
685
|
-
[
|
|
775
|
+
[
|
|
776
|
+
endDrag,
|
|
777
|
+
handleContextMenu,
|
|
778
|
+
handleLostPointerCapture,
|
|
779
|
+
handlePointerDown,
|
|
780
|
+
handlePointerMove,
|
|
781
|
+
],
|
|
686
782
|
);
|
|
687
783
|
|
|
688
784
|
const bodyProps = useMemo<SheetBodyProps>(
|
|
689
785
|
() => ({
|
|
690
786
|
ref: bodyRef,
|
|
787
|
+
onContextMenu: handleContextMenu,
|
|
788
|
+
onLostPointerCapture: handleLostPointerCapture,
|
|
691
789
|
onPointerDown: handleBodyPointerDown,
|
|
692
790
|
onPointerMove: handleBodyPointerMove,
|
|
693
791
|
onPointerUp: handleBodyEnd,
|
|
694
792
|
onPointerCancel: handleBodyEnd,
|
|
695
793
|
}),
|
|
696
|
-
[
|
|
794
|
+
[
|
|
795
|
+
bodyRef,
|
|
796
|
+
handleBodyEnd,
|
|
797
|
+
handleBodyPointerDown,
|
|
798
|
+
handleBodyPointerMove,
|
|
799
|
+
handleContextMenu,
|
|
800
|
+
handleLostPointerCapture,
|
|
801
|
+
],
|
|
697
802
|
);
|
|
698
803
|
|
|
699
804
|
return {
|