@octanejs/base-ui 0.1.1 → 0.1.3

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.
Files changed (67) hide show
  1. package/README.md +7 -1
  2. package/package.json +12 -4
  3. package/src/alert-dialog.ts +47 -0
  4. package/src/checkbox.ts +7 -16
  5. package/src/dialog.ts +859 -0
  6. package/src/field.ts +4 -30
  7. package/src/index.ts +3 -0
  8. package/src/number-field.ts +8 -45
  9. package/src/popover.ts +1205 -0
  10. package/src/radio.ts +3 -24
  11. package/src/slider.ts +4 -5
  12. package/src/switch.ts +4 -30
  13. package/src/utils/InternalBackdrop.ts +28 -0
  14. package/src/utils/adaptiveOriginMiddleware.ts +82 -0
  15. package/src/utils/closePart.ts +61 -0
  16. package/src/utils/constants.ts +9 -0
  17. package/src/utils/createChangeEventDetails.ts +7 -0
  18. package/src/utils/dom.ts +9 -0
  19. package/src/utils/empty.ts +7 -0
  20. package/src/utils/floating/FloatingFocusManager.ts +833 -0
  21. package/src/utils/floating/FloatingPortal.ts +268 -0
  22. package/src/utils/floating/FloatingRootStore.ts +127 -0
  23. package/src/utils/floating/FloatingTree.ts +70 -0
  24. package/src/utils/floating/FloatingTreeStore.ts +22 -0
  25. package/src/utils/floating/FocusGuard.ts +34 -0
  26. package/src/utils/floating/composite.ts +20 -0
  27. package/src/utils/floating/constants.ts +8 -0
  28. package/src/utils/floating/createAttribute.ts +4 -0
  29. package/src/utils/floating/createEventEmitter.ts +20 -0
  30. package/src/utils/floating/element.ts +54 -0
  31. package/src/utils/floating/enqueueFocus.ts +38 -0
  32. package/src/utils/floating/event.ts +53 -0
  33. package/src/utils/floating/getEmptyRootContext.ts +19 -0
  34. package/src/utils/floating/markOthers.ts +197 -0
  35. package/src/utils/floating/nodes.ts +31 -0
  36. package/src/utils/floating/tabbable.ts +260 -0
  37. package/src/utils/floating/types.ts +57 -0
  38. package/src/utils/floating/useClick.ts +174 -0
  39. package/src/utils/floating/useDismiss.ts +641 -0
  40. package/src/utils/floating/useFloating.ts +198 -0
  41. package/src/utils/floating/useFloatingRootContext.ts +81 -0
  42. package/src/utils/floating/useHoverFloatingInteraction.ts +12 -0
  43. package/src/utils/floating/useHoverReferenceInteraction.ts +17 -0
  44. package/src/utils/floating/useSyncedFloatingRootContext.ts +93 -0
  45. package/src/utils/getDisabledMountTransitionStyles.ts +12 -0
  46. package/src/utils/hideMiddleware.ts +22 -0
  47. package/src/utils/inertValue.ts +5 -0
  48. package/src/utils/mergeCleanups.ts +13 -0
  49. package/src/utils/platform.ts +46 -2
  50. package/src/utils/popupStateMapping.ts +48 -0
  51. package/src/utils/popups/index.ts +4 -0
  52. package/src/utils/popups/popupStoreUtils.ts +484 -0
  53. package/src/utils/popups/popupTriggerMap.ts +62 -0
  54. package/src/utils/popups/store.ts +147 -0
  55. package/src/utils/popups/useTriggerFocusGuards.ts +73 -0
  56. package/src/utils/store/ReactStore.ts +168 -0
  57. package/src/utils/store/Store.ts +84 -0
  58. package/src/utils/store/createSelector.ts +66 -0
  59. package/src/utils/store/useStore.ts +49 -0
  60. package/src/utils/useAnchorPositioning.ts +581 -0
  61. package/src/utils/useAnchoredPopupScrollLock.ts +50 -0
  62. package/src/utils/useAnimationFrame.ts +28 -5
  63. package/src/utils/useEnhancedClickHandler.ts +47 -0
  64. package/src/utils/useOnFirstRender.ts +11 -0
  65. package/src/utils/useOpenInteractionType.ts +32 -0
  66. package/src/utils/usePositioner.ts +48 -0
  67. package/src/utils/useScrollLock.ts +253 -0
@@ -0,0 +1,641 @@
1
+ // Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useDismiss.ts (v1.6.0), octane-
2
+ // adapted: reads the FloatingRootStore (`store.useState`/`select`/`setOpen`/`context`); native
3
+ // events (no `.nativeEvent`); every hook threads an explicit slot. Returns `{ reference, floating,
4
+ // trigger }` prop bags. Closes the popup on Escape / outside press (with the full intentional/sloppy
5
+ // press-type + touch + nested-tree logic).
6
+ import { useRef, useEffect, useMemo } from 'octane';
7
+ import {
8
+ getComputedStyle,
9
+ getParentNode,
10
+ isElement,
11
+ isHTMLElement,
12
+ isLastTraversableNode,
13
+ isShadowRoot,
14
+ } from '@floating-ui/utils/dom';
15
+
16
+ import { subSlot } from '../../internal';
17
+ import { addEventListener } from '../addEventListener';
18
+ import { mergeCleanups } from '../mergeCleanups';
19
+ import { ownerDocument } from '../owner';
20
+ import { useStableCallback } from '../useStableCallback';
21
+ import { Timeout, useTimeout } from '../useTimeout';
22
+ import { platform } from '../platform';
23
+ import { useFloatingTree } from './FloatingTree';
24
+ import type { FloatingTreeStore } from './FloatingTreeStore';
25
+ import type { ElementProps, FloatingContext, FloatingRootContext } from './types';
26
+ import { createChangeEventDetails, REASONS } from '../createChangeEventDetails';
27
+ import { createAttribute } from './createAttribute';
28
+ import { contains, getTarget, isEventTargetWithin, isRootElement } from './element';
29
+ import { isReactEvent } from './event';
30
+ import { getNodeChildren } from './nodes';
31
+
32
+ type PressType = 'intentional' | 'sloppy';
33
+
34
+ function alwaysFalse() {
35
+ return false;
36
+ }
37
+
38
+ export function normalizeProp(
39
+ normalizable?: boolean | { escapeKey?: boolean | undefined; outsidePress?: boolean | undefined },
40
+ ) {
41
+ return {
42
+ escapeKey:
43
+ typeof normalizable === 'boolean' ? normalizable : (normalizable?.escapeKey ?? false),
44
+ outsidePress:
45
+ typeof normalizable === 'boolean' ? normalizable : (normalizable?.outsidePress ?? true),
46
+ };
47
+ }
48
+
49
+ export interface UseDismissProps {
50
+ enabled?: boolean | undefined;
51
+ escapeKey?: boolean | undefined;
52
+ referencePress?: (() => boolean) | undefined;
53
+ outsidePress?: boolean | ((event: MouseEvent | TouchEvent) => boolean) | undefined;
54
+ outsidePressEvent?:
55
+ | PressType
56
+ | { mouse: PressType; touch: PressType }
57
+ | (() => PressType | { mouse: PressType; touch: PressType })
58
+ | undefined;
59
+ bubbles?:
60
+ | boolean
61
+ | { escapeKey?: boolean | undefined; outsidePress?: boolean | undefined }
62
+ | undefined;
63
+ externalTree?: FloatingTreeStore | undefined;
64
+ }
65
+
66
+ export function useDismiss(
67
+ context: FloatingRootContext | FloatingContext,
68
+ props: UseDismissProps,
69
+ slot: symbol | undefined,
70
+ ): ElementProps {
71
+ const {
72
+ enabled = true,
73
+ escapeKey = true,
74
+ outsidePress: outsidePressProp = true,
75
+ outsidePressEvent = 'sloppy',
76
+ referencePress = alwaysFalse,
77
+ bubbles,
78
+ externalTree,
79
+ } = props;
80
+
81
+ const store = (context && 'rootStore' in context ? context.rootStore : context) as any;
82
+
83
+ const open = store.useState('open', subSlot(slot, 'open'));
84
+ const floatingElement = store.useState('floatingElement', subSlot(slot, 'fel'));
85
+ const { dataRef } = store.context;
86
+
87
+ const tree = useFloatingTree(externalTree);
88
+ const outsidePressFn = useStableCallback(
89
+ typeof outsidePressProp === 'function' ? outsidePressProp : () => false,
90
+ subSlot(slot, 'opf'),
91
+ );
92
+ const outsidePress = typeof outsidePressProp === 'function' ? outsidePressFn : outsidePressProp;
93
+ const outsidePressEnabled = outsidePress !== false;
94
+ const getOutsidePressEventProp = useStableCallback(
95
+ () => outsidePressEvent,
96
+ subSlot(slot, 'gope'),
97
+ );
98
+
99
+ const { escapeKey: escapeKeyBubbles, outsidePress: outsidePressBubbles } = normalizeProp(bubbles);
100
+
101
+ const pressStartedInsideRef = useRef(false, subSlot(slot, 'psi'));
102
+ const pressStartPreventedRef = useRef(false, subSlot(slot, 'psp'));
103
+ const suppressNextOutsideClickRef = useRef(false, subSlot(slot, 'snoc'));
104
+ const isComposingRef = useRef(false, subSlot(slot, 'ic'));
105
+ const currentPointerTypeRef = useRef<string>('', subSlot(slot, 'cpt'));
106
+
107
+ const touchStateRef = useRef<{
108
+ startTime: number;
109
+ startX: number;
110
+ startY: number;
111
+ dismissOnTouchEnd: boolean;
112
+ dismissOnMouseDown: boolean;
113
+ } | null>(null, subSlot(slot, 'ts'));
114
+
115
+ const cancelDismissOnEndTimeout = useTimeout(subSlot(slot, 'cdet'));
116
+ const clearInsideReactTreeTimeout = useTimeout(subSlot(slot, 'cirt'));
117
+
118
+ const clearInsideReactTree = useStableCallback(
119
+ () => {
120
+ clearInsideReactTreeTimeout.clear();
121
+ dataRef.current.insideReactTree = false;
122
+ },
123
+ subSlot(slot, 'cir'),
124
+ );
125
+
126
+ const hasBlockingChild = useStableCallback(
127
+ (bubbleKey: '__escapeKeyBubbles' | '__outsidePressBubbles') => {
128
+ const nodeId = dataRef.current.floatingContext?.nodeId;
129
+ const children = tree ? getNodeChildren(tree.nodesRef.current, nodeId) : [];
130
+ return children.some(
131
+ (child) => child.context?.open && !child.context.dataRef.current[bubbleKey],
132
+ );
133
+ },
134
+ subSlot(slot, 'hbc'),
135
+ );
136
+
137
+ const isEventWithinOwnElements = useStableCallback(
138
+ (event: Event) => {
139
+ return (
140
+ isEventTargetWithin(event, store.select('floatingElement')) ||
141
+ isEventTargetWithin(event, store.select('domReferenceElement'))
142
+ );
143
+ },
144
+ subSlot(slot, 'iewoe'),
145
+ );
146
+
147
+ const closeOnReferencePress = useStableCallback(
148
+ (event: any) => {
149
+ if (!referencePress()) {
150
+ return;
151
+ }
152
+ store.setOpen(false, createChangeEventDetails(REASONS.triggerPress, event));
153
+ },
154
+ subSlot(slot, 'corp'),
155
+ );
156
+
157
+ const closeOnEscapeKeyDown = useStableCallback(
158
+ (event: any) => {
159
+ if (!open || !enabled || !escapeKey || event.key !== 'Escape') {
160
+ return;
161
+ }
162
+ if (isComposingRef.current) {
163
+ return;
164
+ }
165
+ if (!escapeKeyBubbles && hasBlockingChild('__escapeKeyBubbles')) {
166
+ return;
167
+ }
168
+ const native = isReactEvent(event) ? event.nativeEvent : event;
169
+ const eventDetails = createChangeEventDetails(REASONS.escapeKey, native);
170
+ store.setOpen(false, eventDetails);
171
+ if (!eventDetails.isCanceled) {
172
+ event.preventDefault();
173
+ }
174
+ if (!escapeKeyBubbles && !eventDetails.isPropagationAllowed) {
175
+ event.stopPropagation();
176
+ }
177
+ },
178
+ subSlot(slot, 'coekd'),
179
+ );
180
+
181
+ const markInsideReactTree = useStableCallback(
182
+ () => {
183
+ dataRef.current.insideReactTree = true;
184
+ clearInsideReactTreeTimeout.start(0, clearInsideReactTree);
185
+ },
186
+ subSlot(slot, 'mirt'),
187
+ );
188
+
189
+ const markPressStartedInsideReactTree = useStableCallback(
190
+ (event: any) => {
191
+ if (!open || !enabled || event.button !== 0) {
192
+ return;
193
+ }
194
+ const target = getTarget(event) as Element | null;
195
+ if (!contains(store.select('floatingElement'), target)) {
196
+ return;
197
+ }
198
+ if (!pressStartedInsideRef.current) {
199
+ pressStartedInsideRef.current = true;
200
+ pressStartPreventedRef.current = false;
201
+ }
202
+ },
203
+ subSlot(slot, 'mpsirt'),
204
+ );
205
+
206
+ const markInsidePressStartPrevented = useStableCallback(
207
+ (event: any) => {
208
+ if (!open || !enabled) {
209
+ return;
210
+ }
211
+ if (!event.defaultPrevented) {
212
+ return;
213
+ }
214
+ if (pressStartedInsideRef.current) {
215
+ pressStartPreventedRef.current = true;
216
+ }
217
+ },
218
+ subSlot(slot, 'mipsp'),
219
+ );
220
+
221
+ useEffect(
222
+ () => {
223
+ if (!open || !enabled) {
224
+ return undefined;
225
+ }
226
+
227
+ dataRef.current.__escapeKeyBubbles = escapeKeyBubbles;
228
+ dataRef.current.__outsidePressBubbles = outsidePressBubbles;
229
+
230
+ const compositionTimeout = new Timeout();
231
+ const preventedPressSuppressionTimeout = new Timeout();
232
+
233
+ function handleCompositionStart() {
234
+ compositionTimeout.clear();
235
+ isComposingRef.current = true;
236
+ }
237
+
238
+ function handleCompositionEnd() {
239
+ compositionTimeout.start(platform.engine.webkit ? 5 : 0, () => {
240
+ isComposingRef.current = false;
241
+ });
242
+ }
243
+
244
+ function suppressImmediateOutsideClickAfterPreventedStart() {
245
+ suppressNextOutsideClickRef.current = true;
246
+ preventedPressSuppressionTimeout.start(0, () => {
247
+ suppressNextOutsideClickRef.current = false;
248
+ });
249
+ }
250
+
251
+ function resetPressStartState() {
252
+ pressStartedInsideRef.current = false;
253
+ pressStartPreventedRef.current = false;
254
+ }
255
+
256
+ function getOutsidePressEvent(): PressType {
257
+ const type = currentPointerTypeRef.current as 'pen' | 'mouse' | 'touch' | '';
258
+ const computedType = type === 'pen' || !type ? 'mouse' : type;
259
+ const outsidePressEventValue = getOutsidePressEventProp();
260
+ const resolved =
261
+ typeof outsidePressEventValue === 'function'
262
+ ? outsidePressEventValue()
263
+ : outsidePressEventValue;
264
+ if (typeof resolved === 'string') {
265
+ return resolved as PressType;
266
+ }
267
+ return (resolved as { mouse: PressType; touch: PressType })[computedType];
268
+ }
269
+
270
+ function shouldIgnoreEvent(event: Event) {
271
+ const computedOutsidePressEvent = getOutsidePressEvent();
272
+ return (
273
+ (computedOutsidePressEvent === 'intentional' && event.type !== 'click') ||
274
+ (computedOutsidePressEvent === 'sloppy' && event.type === 'click')
275
+ );
276
+ }
277
+
278
+ function isEventWithinFloatingTree(event: Event) {
279
+ const nodeId = dataRef.current.floatingContext?.nodeId;
280
+ const targetIsInsideChildren =
281
+ tree &&
282
+ getNodeChildren(tree.nodesRef.current, nodeId).some((node) =>
283
+ isEventTargetWithin(event, node.context?.elements.floating),
284
+ );
285
+ return isEventWithinOwnElements(event) || targetIsInsideChildren;
286
+ }
287
+
288
+ function closeOnPressOutside(event: MouseEvent | PointerEvent | TouchEvent) {
289
+ if (shouldIgnoreEvent(event)) {
290
+ if (event.type !== 'click' && !isEventWithinOwnElements(event)) {
291
+ preventedPressSuppressionTimeout.clear();
292
+ suppressNextOutsideClickRef.current = false;
293
+ }
294
+ clearInsideReactTree();
295
+ return;
296
+ }
297
+
298
+ if (dataRef.current.insideReactTree) {
299
+ clearInsideReactTree();
300
+ return;
301
+ }
302
+
303
+ const target = getTarget(event);
304
+ const inertSelector = `[${createAttribute('inert')}]`;
305
+ const targetRoot = isElement(target) ? target.getRootNode() : null;
306
+ const markers = Array.from(
307
+ (isShadowRoot(targetRoot)
308
+ ? targetRoot
309
+ : ownerDocument(store.select('floatingElement'))
310
+ ).querySelectorAll(inertSelector),
311
+ );
312
+
313
+ const triggers = store.context.triggerElements;
314
+
315
+ if (
316
+ target &&
317
+ (triggers.hasElement(target as Element) ||
318
+ triggers.hasMatchingElement((trigger: Element) => contains(trigger, target as Element)))
319
+ ) {
320
+ return;
321
+ }
322
+
323
+ let targetRootAncestor = isElement(target) ? target : null;
324
+ while (targetRootAncestor && !isLastTraversableNode(targetRootAncestor)) {
325
+ const nextParent = getParentNode(targetRootAncestor);
326
+ if (isLastTraversableNode(nextParent) || !isElement(nextParent)) {
327
+ break;
328
+ }
329
+ targetRootAncestor = nextParent;
330
+ }
331
+
332
+ if (
333
+ markers.length &&
334
+ isElement(target) &&
335
+ !isRootElement(target) &&
336
+ !contains(target, store.select('floatingElement')) &&
337
+ markers.every((marker) => !contains(targetRootAncestor, marker))
338
+ ) {
339
+ return;
340
+ }
341
+
342
+ if (isHTMLElement(target) && !('touches' in event)) {
343
+ const lastTraversableNode = isLastTraversableNode(target);
344
+ const style = getComputedStyle(target);
345
+ const scrollRe = /auto|scroll/;
346
+ const isScrollableX = lastTraversableNode || scrollRe.test(style.overflowX);
347
+ const isScrollableY = lastTraversableNode || scrollRe.test(style.overflowY);
348
+ const canScrollX =
349
+ isScrollableX && target.clientWidth > 0 && target.scrollWidth > target.clientWidth;
350
+ const canScrollY =
351
+ isScrollableY && target.clientHeight > 0 && target.scrollHeight > target.clientHeight;
352
+ const isRTL = style.direction === 'rtl';
353
+ const pressedVerticalScrollbar =
354
+ canScrollY &&
355
+ (isRTL
356
+ ? (event as MouseEvent).offsetX <= target.offsetWidth - target.clientWidth
357
+ : (event as MouseEvent).offsetX > target.clientWidth);
358
+ const pressedHorizontalScrollbar =
359
+ canScrollX && (event as MouseEvent).offsetY > target.clientHeight;
360
+ if (pressedVerticalScrollbar || pressedHorizontalScrollbar) {
361
+ return;
362
+ }
363
+ }
364
+
365
+ if (isEventWithinFloatingTree(event)) {
366
+ return;
367
+ }
368
+
369
+ if (getOutsidePressEvent() === 'intentional' && suppressNextOutsideClickRef.current) {
370
+ preventedPressSuppressionTimeout.clear();
371
+ suppressNextOutsideClickRef.current = false;
372
+ return;
373
+ }
374
+
375
+ if (typeof outsidePress === 'function' && !outsidePress(event)) {
376
+ return;
377
+ }
378
+
379
+ if (hasBlockingChild('__outsidePressBubbles')) {
380
+ return;
381
+ }
382
+
383
+ store.setOpen(false, createChangeEventDetails(REASONS.outsidePress, event));
384
+ clearInsideReactTree();
385
+ }
386
+
387
+ function handlePointerDown(event: PointerEvent) {
388
+ if (
389
+ getOutsidePressEvent() !== 'sloppy' ||
390
+ event.pointerType === 'touch' ||
391
+ !store.select('open') ||
392
+ !enabled ||
393
+ isEventWithinOwnElements(event)
394
+ ) {
395
+ return;
396
+ }
397
+ closeOnPressOutside(event);
398
+ }
399
+
400
+ function handleTouchStart(event: TouchEvent) {
401
+ if (
402
+ getOutsidePressEvent() !== 'sloppy' ||
403
+ !store.select('open') ||
404
+ !enabled ||
405
+ isEventWithinOwnElements(event)
406
+ ) {
407
+ return;
408
+ }
409
+ const touch = event.touches[0];
410
+ if (touch) {
411
+ touchStateRef.current = {
412
+ startTime: Date.now(),
413
+ startX: touch.clientX,
414
+ startY: touch.clientY,
415
+ dismissOnTouchEnd: false,
416
+ dismissOnMouseDown: true,
417
+ };
418
+ cancelDismissOnEndTimeout.start(1000, () => {
419
+ if (touchStateRef.current) {
420
+ touchStateRef.current.dismissOnTouchEnd = false;
421
+ touchStateRef.current.dismissOnMouseDown = false;
422
+ }
423
+ });
424
+ }
425
+ }
426
+
427
+ function addTargetEventListenerOnce<EventType extends Event>(
428
+ event: EventType,
429
+ listener: (event: EventType) => void,
430
+ ) {
431
+ const target = getTarget(event);
432
+ if (!target) {
433
+ return;
434
+ }
435
+ const unsubscribe = addEventListener(target as any, event.type, () => {
436
+ listener(event);
437
+ unsubscribe();
438
+ });
439
+ }
440
+
441
+ function handleTouchStartCapture(event: TouchEvent) {
442
+ currentPointerTypeRef.current = 'touch';
443
+ addTargetEventListenerOnce(event, handleTouchStart);
444
+ }
445
+
446
+ function closeOnPressOutsideCapture(event: PointerEvent | MouseEvent) {
447
+ cancelDismissOnEndTimeout.clear();
448
+ if (event.type === 'pointerdown') {
449
+ currentPointerTypeRef.current = (event as PointerEvent).pointerType;
450
+ }
451
+ if (
452
+ event.type === 'mousedown' &&
453
+ touchStateRef.current &&
454
+ !touchStateRef.current.dismissOnMouseDown
455
+ ) {
456
+ return;
457
+ }
458
+ addTargetEventListenerOnce(event, (targetEvent) => {
459
+ if (targetEvent.type === 'pointerdown') {
460
+ handlePointerDown(targetEvent as PointerEvent);
461
+ } else {
462
+ closeOnPressOutside(targetEvent as MouseEvent);
463
+ }
464
+ });
465
+ }
466
+
467
+ function handlePressEndCapture(event: PointerEvent | MouseEvent) {
468
+ if (!pressStartedInsideRef.current) {
469
+ return;
470
+ }
471
+ const pressStartedInsideDefaultPrevented = pressStartPreventedRef.current;
472
+ resetPressStartState();
473
+ if (getOutsidePressEvent() !== 'intentional') {
474
+ return;
475
+ }
476
+ if (event.type === 'pointercancel') {
477
+ if (pressStartedInsideDefaultPrevented) {
478
+ suppressImmediateOutsideClickAfterPreventedStart();
479
+ }
480
+ return;
481
+ }
482
+ if (isEventWithinFloatingTree(event)) {
483
+ return;
484
+ }
485
+ if (pressStartedInsideDefaultPrevented) {
486
+ suppressImmediateOutsideClickAfterPreventedStart();
487
+ return;
488
+ }
489
+ if (typeof outsidePress === 'function' && !outsidePress(event as MouseEvent)) {
490
+ return;
491
+ }
492
+ preventedPressSuppressionTimeout.clear();
493
+ suppressNextOutsideClickRef.current = true;
494
+ clearInsideReactTree();
495
+ }
496
+
497
+ function handleTouchMove(event: TouchEvent) {
498
+ if (
499
+ getOutsidePressEvent() !== 'sloppy' ||
500
+ !touchStateRef.current ||
501
+ isEventWithinOwnElements(event)
502
+ ) {
503
+ return;
504
+ }
505
+ const touch = event.touches[0];
506
+ if (!touch) {
507
+ return;
508
+ }
509
+ const deltaX = Math.abs(touch.clientX - touchStateRef.current.startX);
510
+ const deltaY = Math.abs(touch.clientY - touchStateRef.current.startY);
511
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
512
+ if (distance > 5) {
513
+ touchStateRef.current.dismissOnTouchEnd = true;
514
+ }
515
+ if (distance > 10) {
516
+ closeOnPressOutside(event);
517
+ cancelDismissOnEndTimeout.clear();
518
+ touchStateRef.current = null;
519
+ }
520
+ }
521
+
522
+ function handleTouchMoveCapture(event: TouchEvent) {
523
+ addTargetEventListenerOnce(event, handleTouchMove);
524
+ }
525
+
526
+ function handleTouchEnd(event: TouchEvent) {
527
+ if (
528
+ getOutsidePressEvent() !== 'sloppy' ||
529
+ !touchStateRef.current ||
530
+ isEventWithinOwnElements(event)
531
+ ) {
532
+ return;
533
+ }
534
+ if (touchStateRef.current.dismissOnTouchEnd) {
535
+ closeOnPressOutside(event);
536
+ }
537
+ cancelDismissOnEndTimeout.clear();
538
+ touchStateRef.current = null;
539
+ }
540
+
541
+ function handleTouchEndCapture(event: TouchEvent) {
542
+ addTargetEventListenerOnce(event, handleTouchEnd);
543
+ }
544
+
545
+ const doc = ownerDocument(floatingElement);
546
+ const unsubscribe = mergeCleanups(
547
+ escapeKey &&
548
+ mergeCleanups(
549
+ addEventListener(doc, 'keydown', closeOnEscapeKeyDown as EventListener),
550
+ addEventListener(doc, 'compositionstart', handleCompositionStart),
551
+ addEventListener(doc, 'compositionend', handleCompositionEnd),
552
+ ),
553
+ outsidePressEnabled &&
554
+ mergeCleanups(
555
+ addEventListener(doc, 'click', closeOnPressOutsideCapture as EventListener, true),
556
+ addEventListener(doc, 'pointerdown', closeOnPressOutsideCapture as EventListener, true),
557
+ addEventListener(doc, 'pointerup', handlePressEndCapture as EventListener, true),
558
+ addEventListener(doc, 'pointercancel', handlePressEndCapture as EventListener, true),
559
+ addEventListener(doc, 'mousedown', closeOnPressOutsideCapture as EventListener, true),
560
+ addEventListener(doc, 'mouseup', handlePressEndCapture as EventListener, true),
561
+ addEventListener(doc, 'touchstart', handleTouchStartCapture as EventListener, true),
562
+ addEventListener(doc, 'touchmove', handleTouchMoveCapture as EventListener, true),
563
+ addEventListener(doc, 'touchend', handleTouchEndCapture as EventListener, true),
564
+ ),
565
+ );
566
+
567
+ return () => {
568
+ unsubscribe();
569
+ compositionTimeout.clear();
570
+ preventedPressSuppressionTimeout.clear();
571
+ resetPressStartState();
572
+ suppressNextOutsideClickRef.current = false;
573
+ };
574
+ },
575
+ [
576
+ dataRef,
577
+ floatingElement,
578
+ escapeKey,
579
+ outsidePressEnabled,
580
+ outsidePress,
581
+ open,
582
+ enabled,
583
+ escapeKeyBubbles,
584
+ outsidePressBubbles,
585
+ closeOnEscapeKeyDown,
586
+ clearInsideReactTree,
587
+ getOutsidePressEventProp,
588
+ hasBlockingChild,
589
+ isEventWithinOwnElements,
590
+ tree,
591
+ store,
592
+ cancelDismissOnEndTimeout,
593
+ ],
594
+ subSlot(slot, 'e:main'),
595
+ );
596
+
597
+ useEffect(clearInsideReactTree, [outsidePress, clearInsideReactTree], subSlot(slot, 'e:cir'));
598
+
599
+ const reference: ElementProps['reference'] = useMemo(
600
+ () => ({
601
+ onKeyDown: closeOnEscapeKeyDown,
602
+ onPointerDown: closeOnReferencePress,
603
+ onClick: closeOnReferencePress,
604
+ }),
605
+ [closeOnEscapeKeyDown, closeOnReferencePress],
606
+ subSlot(slot, 'ref'),
607
+ );
608
+
609
+ const floating: ElementProps['floating'] = useMemo(
610
+ () => ({
611
+ onKeyDown: closeOnEscapeKeyDown,
612
+ onPointerDown: markInsidePressStartPrevented,
613
+ onMouseDown: markInsidePressStartPrevented,
614
+ onClickCapture: markInsideReactTree,
615
+ onMouseDownCapture(event: any) {
616
+ markInsideReactTree();
617
+ markPressStartedInsideReactTree(event);
618
+ },
619
+ onPointerDownCapture(event: any) {
620
+ markInsideReactTree();
621
+ markPressStartedInsideReactTree(event);
622
+ },
623
+ onMouseUpCapture: markInsideReactTree,
624
+ onTouchEndCapture: markInsideReactTree,
625
+ onTouchMoveCapture: markInsideReactTree,
626
+ }),
627
+ [
628
+ closeOnEscapeKeyDown,
629
+ markInsideReactTree,
630
+ markPressStartedInsideReactTree,
631
+ markInsidePressStartPrevented,
632
+ ],
633
+ subSlot(slot, 'floating'),
634
+ );
635
+
636
+ return useMemo(
637
+ () => (enabled ? { reference, floating, trigger: reference } : {}),
638
+ [enabled, reference, floating],
639
+ subSlot(slot, 'out'),
640
+ );
641
+ }