@react-aria/interactions 3.25.5 → 3.26.0
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/focusSafely.main.js +4 -4
- package/dist/focusSafely.main.js.map +1 -1
- package/dist/focusSafely.mjs +4 -4
- package/dist/focusSafely.module.js +4 -4
- package/dist/focusSafely.module.js.map +1 -1
- package/dist/import.mjs +2 -2
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/module.js +2 -2
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/useFocusVisible.main.js +12 -2
- package/dist/useFocusVisible.main.js.map +1 -1
- package/dist/useFocusVisible.mjs +13 -4
- package/dist/useFocusVisible.module.js +13 -4
- package/dist/useFocusVisible.module.js.map +1 -1
- package/dist/useInteractOutside.main.js +1 -3
- package/dist/useInteractOutside.main.js.map +1 -1
- package/dist/useInteractOutside.mjs +1 -3
- package/dist/useInteractOutside.module.js +1 -3
- package/dist/useInteractOutside.module.js.map +1 -1
- package/dist/useMove.main.js +110 -74
- package/dist/useMove.main.js.map +1 -1
- package/dist/useMove.mjs +112 -76
- package/dist/useMove.module.js +112 -76
- package/dist/useMove.module.js.map +1 -1
- package/dist/usePress.main.js +188 -114
- package/dist/usePress.main.js.map +1 -1
- package/dist/usePress.mjs +190 -116
- package/dist/usePress.module.js +190 -116
- package/dist/usePress.module.js.map +1 -1
- package/dist/utils.main.js +2 -5
- package/dist/utils.main.js.map +1 -1
- package/dist/utils.mjs +3 -6
- package/dist/utils.module.js +3 -6
- package/dist/utils.module.js.map +1 -1
- package/package.json +4 -4
- package/src/focusSafely.ts +4 -4
- package/src/index.ts +1 -0
- package/src/useFocusVisible.ts +15 -3
- package/src/useInteractOutside.ts +1 -1
- package/src/useMove.ts +85 -57
- package/src/usePress.ts +192 -148
- package/src/utils.ts +3 -7
package/src/usePress.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
openLink,
|
|
30
30
|
useEffectEvent,
|
|
31
31
|
useGlobalListeners,
|
|
32
|
+
useLayoutEffect,
|
|
32
33
|
useSyncRef
|
|
33
34
|
} from '@react-aria/utils';
|
|
34
35
|
import {createSyntheticEvent, preventFocus, setEventTarget} from './utils';
|
|
@@ -36,7 +37,7 @@ import {disableTextSelection, restoreTextSelection} from './textSelection';
|
|
|
36
37
|
import {DOMAttributes, FocusableElement, PressEvent as IPressEvent, PointerType, PressEvents, RefObject} from '@react-types/shared';
|
|
37
38
|
import {flushSync} from 'react-dom';
|
|
38
39
|
import {PressResponderContext} from './context';
|
|
39
|
-
import {MouseEvent as RMouseEvent, TouchEvent as RTouchEvent, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
|
40
|
+
import {MouseEvent as RMouseEvent, TouchEvent as RTouchEvent, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
|
40
41
|
|
|
41
42
|
export interface PressProps extends PressEvents {
|
|
42
43
|
/** Whether the target is in a controlled press state (e.g. an overlay it triggers is open). */
|
|
@@ -195,9 +196,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
195
196
|
disposables: []
|
|
196
197
|
});
|
|
197
198
|
|
|
198
|
-
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
|
|
199
|
+
let {addGlobalListener, removeAllGlobalListeners, removeGlobalListener} = useGlobalListeners();
|
|
199
200
|
|
|
200
|
-
let triggerPressStart =
|
|
201
|
+
let triggerPressStart = useCallback((originalEvent: EventBase, pointerType: PointerType) => {
|
|
201
202
|
let state = ref.current;
|
|
202
203
|
if (isDisabled || state.didFirePressStart) {
|
|
203
204
|
return false;
|
|
@@ -219,9 +220,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
219
220
|
state.didFirePressStart = true;
|
|
220
221
|
setPressed(true);
|
|
221
222
|
return shouldStopPropagation;
|
|
222
|
-
});
|
|
223
|
+
}, [isDisabled, onPressStart, onPressChange]);
|
|
223
224
|
|
|
224
|
-
let triggerPressEnd =
|
|
225
|
+
let triggerPressEnd = useCallback((originalEvent: EventBase, pointerType: PointerType, wasPressed = true) => {
|
|
225
226
|
let state = ref.current;
|
|
226
227
|
if (!state.didFirePressStart) {
|
|
227
228
|
return false;
|
|
@@ -251,9 +252,10 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
251
252
|
|
|
252
253
|
state.isTriggeringEvent = false;
|
|
253
254
|
return shouldStopPropagation;
|
|
254
|
-
});
|
|
255
|
+
}, [isDisabled, onPressEnd, onPressChange, onPress]);
|
|
256
|
+
let triggerPressEndEvent = useEffectEvent(triggerPressEnd);
|
|
255
257
|
|
|
256
|
-
let triggerPressUp =
|
|
258
|
+
let triggerPressUp = useCallback((originalEvent: EventBase, pointerType: PointerType) => {
|
|
257
259
|
let state = ref.current;
|
|
258
260
|
if (isDisabled) {
|
|
259
261
|
return false;
|
|
@@ -268,15 +270,17 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
268
270
|
}
|
|
269
271
|
|
|
270
272
|
return true;
|
|
271
|
-
});
|
|
273
|
+
}, [isDisabled, onPressUp]);
|
|
274
|
+
let triggerPressUpEvent = useEffectEvent(triggerPressUp);
|
|
272
275
|
|
|
273
|
-
let cancel =
|
|
276
|
+
let cancel = useCallback((e: EventBase) => {
|
|
274
277
|
let state = ref.current;
|
|
275
278
|
if (state.isPressed && state.target) {
|
|
276
279
|
if (state.didFirePressStart && state.pointerType != null) {
|
|
277
280
|
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
278
281
|
}
|
|
279
282
|
state.isPressed = false;
|
|
283
|
+
setIsPointerPressed(null);
|
|
280
284
|
state.isOverTarget = false;
|
|
281
285
|
state.activePointerId = null;
|
|
282
286
|
state.pointerType = null;
|
|
@@ -289,19 +293,28 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
289
293
|
}
|
|
290
294
|
state.disposables = [];
|
|
291
295
|
}
|
|
292
|
-
});
|
|
296
|
+
}, [allowTextSelectionOnPress, removeAllGlobalListeners, triggerPressEnd]);
|
|
297
|
+
let cancelEvent = useEffectEvent(cancel);
|
|
293
298
|
|
|
294
|
-
let cancelOnPointerExit =
|
|
299
|
+
let cancelOnPointerExit = useCallback((e: EventBase) => {
|
|
295
300
|
if (shouldCancelOnPointerExit) {
|
|
296
301
|
cancel(e);
|
|
297
302
|
}
|
|
298
|
-
});
|
|
303
|
+
}, [shouldCancelOnPointerExit, cancel]);
|
|
304
|
+
|
|
305
|
+
let triggerClick = useCallback((e: RMouseEvent<FocusableElement>) => {
|
|
306
|
+
if (isDisabled) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
299
309
|
|
|
300
|
-
let triggerClick = useEffectEvent((e: RMouseEvent<FocusableElement>) => {
|
|
301
310
|
onClick?.(e);
|
|
302
|
-
});
|
|
311
|
+
}, [isDisabled, onClick]);
|
|
312
|
+
|
|
313
|
+
let triggerSyntheticClick = useCallback((e: KeyboardEvent | TouchEvent, target: FocusableElement) => {
|
|
314
|
+
if (isDisabled) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
303
317
|
|
|
304
|
-
let triggerSyntheticClick = useEffectEvent((e: KeyboardEvent | TouchEvent, target: FocusableElement) => {
|
|
305
318
|
// Some third-party libraries pass in onClick instead of onPress.
|
|
306
319
|
// Create a fake mouse event and trigger onClick as well.
|
|
307
320
|
// This matches the browser's native activation behavior for certain elements (e.g. button).
|
|
@@ -312,7 +325,164 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
312
325
|
setEventTarget(event, target);
|
|
313
326
|
onClick(createSyntheticEvent(event));
|
|
314
327
|
}
|
|
315
|
-
});
|
|
328
|
+
}, [isDisabled, onClick]);
|
|
329
|
+
let triggerSyntheticClickEvent = useEffectEvent(triggerSyntheticClick);
|
|
330
|
+
|
|
331
|
+
let [isElemKeyPressed, setIsElemKeyPressed] = useState<boolean>(false);
|
|
332
|
+
useLayoutEffect(() => {
|
|
333
|
+
let state = ref.current;
|
|
334
|
+
if (isElemKeyPressed) {
|
|
335
|
+
let onKeyUp = (e: KeyboardEvent) => {
|
|
336
|
+
if (state.isPressed && state.target && isValidKeyboardEvent(e, state.target)) {
|
|
337
|
+
if (shouldPreventDefaultKeyboard(getEventTarget(e), e.key)) {
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let target = getEventTarget(e);
|
|
342
|
+
let wasPressed = nodeContains(state.target, getEventTarget(e));
|
|
343
|
+
triggerPressEndEvent(createEvent(state.target, e), 'keyboard', wasPressed);
|
|
344
|
+
if (wasPressed) {
|
|
345
|
+
triggerSyntheticClickEvent(e, state.target);
|
|
346
|
+
}
|
|
347
|
+
removeAllGlobalListeners();
|
|
348
|
+
|
|
349
|
+
// If a link was triggered with a key other than Enter, open the URL ourselves.
|
|
350
|
+
// This means the link has a role override, and the default browser behavior
|
|
351
|
+
// only applies when using the Enter key.
|
|
352
|
+
if (e.key !== 'Enter' && isHTMLAnchorLink(state.target) && nodeContains(state.target, target) && !e[LINK_CLICKED]) {
|
|
353
|
+
// Store a hidden property on the event so we only trigger link click once,
|
|
354
|
+
// even if there are multiple usePress instances attached to the element.
|
|
355
|
+
e[LINK_CLICKED] = true;
|
|
356
|
+
openLink(state.target, e, false);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
state.isPressed = false;
|
|
360
|
+
setIsElemKeyPressed(false);
|
|
361
|
+
state.metaKeyEvents?.delete(e.key);
|
|
362
|
+
} else if (e.key === 'Meta' && state.metaKeyEvents?.size) {
|
|
363
|
+
// If we recorded keydown events that occurred while the Meta key was pressed,
|
|
364
|
+
// and those haven't received keyup events already, fire keyup events ourselves.
|
|
365
|
+
// See comment above for more info about the macOS bug causing this.
|
|
366
|
+
let events = state.metaKeyEvents;
|
|
367
|
+
state.metaKeyEvents = undefined;
|
|
368
|
+
for (let event of events.values()) {
|
|
369
|
+
state.target?.dispatchEvent(new KeyboardEvent('keyup', event));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
// Focus may move before the key up event, so register the event on the document
|
|
374
|
+
// instead of the same element where the key down event occurred. Make it capturing so that it will trigger
|
|
375
|
+
// before stopPropagation from useKeyboard on a child element may happen and thus we can still call triggerPress for the parent element.
|
|
376
|
+
let originalTarget = state.target;
|
|
377
|
+
let pressUp = (e) => {
|
|
378
|
+
if (originalTarget && isValidKeyboardEvent(e, originalTarget) && !e.repeat && nodeContains(originalTarget, getEventTarget(e)) && state.target) {
|
|
379
|
+
triggerPressUpEvent(createEvent(state.target, e), 'keyboard');
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
let listener = chain(pressUp, onKeyUp);
|
|
383
|
+
addGlobalListener(getOwnerDocument(state.target), 'keyup', listener, true);
|
|
384
|
+
return () => {
|
|
385
|
+
removeGlobalListener(getOwnerDocument(state.target), 'keyup', listener, true);
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}, [isElemKeyPressed, addGlobalListener, removeAllGlobalListeners, removeGlobalListener]);
|
|
389
|
+
|
|
390
|
+
let [isPointerPressed, setIsPointerPressed] = useState<'pointer' | 'mouse' | 'touch' | null>(null);
|
|
391
|
+
useLayoutEffect(() => {
|
|
392
|
+
let state = ref.current;
|
|
393
|
+
if (isPointerPressed === 'pointer') {
|
|
394
|
+
let onPointerUp = (e: PointerEvent) => {
|
|
395
|
+
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
|
|
396
|
+
if (nodeContains(state.target, getEventTarget(e)) && state.pointerType != null) {
|
|
397
|
+
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
398
|
+
// is mutated between onPointerUp and onClick, and is more compatible with third party libraries.
|
|
399
|
+
// https://github.com/adobe/react-spectrum/issues/1513
|
|
400
|
+
// https://issues.chromium.org/issues/40732224
|
|
401
|
+
// However, iOS and Android do not focus or fire onClick after a long press.
|
|
402
|
+
// We work around this by triggering a click ourselves after a timeout.
|
|
403
|
+
// This timeout is canceled during the click event in case the real one fires first.
|
|
404
|
+
// The timeout must be at least 32ms, because Safari on iOS delays the click event on
|
|
405
|
+
// non-form elements without certain ARIA roles (for hover emulation).
|
|
406
|
+
// https://github.com/WebKit/WebKit/blob/dccfae42bb29bd4bdef052e469f604a9387241c0/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm#L875-L892
|
|
407
|
+
let clicked = false;
|
|
408
|
+
let timeout = setTimeout(() => {
|
|
409
|
+
if (state.isPressed && state.target instanceof HTMLElement) {
|
|
410
|
+
if (clicked) {
|
|
411
|
+
cancelEvent(e);
|
|
412
|
+
} else {
|
|
413
|
+
focusWithoutScrolling(state.target);
|
|
414
|
+
state.target.click();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}, 80);
|
|
418
|
+
// Use a capturing listener to track if a click occurred.
|
|
419
|
+
// If stopPropagation is called it may never reach our handler.
|
|
420
|
+
addGlobalListener(e.currentTarget as Document, 'click', () => clicked = true, true);
|
|
421
|
+
state.disposables.push(() => clearTimeout(timeout));
|
|
422
|
+
} else {
|
|
423
|
+
cancelEvent(e);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Ignore subsequent onPointerLeave event before onClick on touch devices.
|
|
427
|
+
state.isOverTarget = false;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
let onPointerCancel = (e: PointerEvent) => {
|
|
432
|
+
cancelEvent(e);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
addGlobalListener(getOwnerDocument(state.target), 'pointerup', onPointerUp, false);
|
|
436
|
+
addGlobalListener(getOwnerDocument(state.target), 'pointercancel', onPointerCancel, false);
|
|
437
|
+
return () => {
|
|
438
|
+
removeGlobalListener(getOwnerDocument(state.target), 'pointerup', onPointerUp, false);
|
|
439
|
+
removeGlobalListener(getOwnerDocument(state.target), 'pointercancel', onPointerCancel, false);
|
|
440
|
+
};
|
|
441
|
+
} else if (isPointerPressed === 'mouse' && process.env.NODE_ENV === 'test') {
|
|
442
|
+
let onMouseUp = (e: MouseEvent) => {
|
|
443
|
+
// Only handle left clicks
|
|
444
|
+
if (e.button !== 0) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (state.ignoreEmulatedMouseEvents) {
|
|
449
|
+
state.ignoreEmulatedMouseEvents = false;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (state.target && state.target.contains(e.target as Element) && state.pointerType != null) {
|
|
454
|
+
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
455
|
+
// is mutated between onMouseUp and onClick, and is more compatible with third party libraries.
|
|
456
|
+
} else {
|
|
457
|
+
cancelEvent(e);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
state.isOverTarget = false;
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
addGlobalListener(getOwnerDocument(state.target), 'mouseup', onMouseUp, false);
|
|
464
|
+
return () => {
|
|
465
|
+
removeGlobalListener(getOwnerDocument(state.target), 'mouseup', onMouseUp, false);
|
|
466
|
+
};
|
|
467
|
+
} else if (isPointerPressed === 'touch' && process.env.NODE_ENV === 'test') {
|
|
468
|
+
let onScroll = (e: Event) => {
|
|
469
|
+
if (state.isPressed && nodeContains(getEventTarget(e), state.target)) {
|
|
470
|
+
cancelEvent({
|
|
471
|
+
currentTarget: state.target,
|
|
472
|
+
shiftKey: false,
|
|
473
|
+
ctrlKey: false,
|
|
474
|
+
metaKey: false,
|
|
475
|
+
altKey: false
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
addGlobalListener(getOwnerWindow(state.target), 'scroll', onScroll, true);
|
|
481
|
+
return () => {
|
|
482
|
+
removeGlobalListener(getOwnerWindow(state.target), 'scroll', onScroll, true);
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}, [isPointerPressed, addGlobalListener, removeGlobalListener]);
|
|
316
486
|
|
|
317
487
|
let pressProps = useMemo(() => {
|
|
318
488
|
let state = ref.current;
|
|
@@ -330,20 +500,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
330
500
|
if (!state.isPressed && !e.repeat) {
|
|
331
501
|
state.target = e.currentTarget;
|
|
332
502
|
state.isPressed = true;
|
|
503
|
+
setIsElemKeyPressed(true);
|
|
333
504
|
state.pointerType = 'keyboard';
|
|
334
505
|
shouldStopPropagation = triggerPressStart(e, 'keyboard');
|
|
335
|
-
|
|
336
|
-
// Focus may move before the key up event, so register the event on the document
|
|
337
|
-
// instead of the same element where the key down event occurred. Make it capturing so that it will trigger
|
|
338
|
-
// before stopPropagation from useKeyboard on a child element may happen and thus we can still call triggerPress for the parent element.
|
|
339
|
-
let originalTarget = e.currentTarget;
|
|
340
|
-
let pressUp = (e) => {
|
|
341
|
-
if (isValidKeyboardEvent(e, originalTarget) && !e.repeat && nodeContains(originalTarget, getEventTarget(e)) && state.target) {
|
|
342
|
-
triggerPressUp(createEvent(state.target, e), 'keyboard');
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'keyup', chain(pressUp, onKeyUp), true);
|
|
347
506
|
}
|
|
348
507
|
|
|
349
508
|
if (shouldStopPropagation) {
|
|
@@ -374,7 +533,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
374
533
|
if (isDisabled) {
|
|
375
534
|
e.preventDefault();
|
|
376
535
|
}
|
|
377
|
-
|
|
536
|
+
|
|
378
537
|
// If triggered from a screen reader or by using element.click(),
|
|
379
538
|
// trigger as if it were a keyboard click.
|
|
380
539
|
if (!state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) {
|
|
@@ -401,44 +560,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
401
560
|
}
|
|
402
561
|
};
|
|
403
562
|
|
|
404
|
-
let onKeyUp = (e: KeyboardEvent) => {
|
|
405
|
-
if (state.isPressed && state.target && isValidKeyboardEvent(e, state.target)) {
|
|
406
|
-
if (shouldPreventDefaultKeyboard(getEventTarget(e), e.key)) {
|
|
407
|
-
e.preventDefault();
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
let target = getEventTarget(e);
|
|
411
|
-
let wasPressed = nodeContains(state.target, getEventTarget(e));
|
|
412
|
-
triggerPressEnd(createEvent(state.target, e), 'keyboard', wasPressed);
|
|
413
|
-
if (wasPressed) {
|
|
414
|
-
triggerSyntheticClick(e, state.target);
|
|
415
|
-
}
|
|
416
|
-
removeAllGlobalListeners();
|
|
417
|
-
|
|
418
|
-
// If a link was triggered with a key other than Enter, open the URL ourselves.
|
|
419
|
-
// This means the link has a role override, and the default browser behavior
|
|
420
|
-
// only applies when using the Enter key.
|
|
421
|
-
if (e.key !== 'Enter' && isHTMLAnchorLink(state.target) && nodeContains(state.target, target) && !e[LINK_CLICKED]) {
|
|
422
|
-
// Store a hidden property on the event so we only trigger link click once,
|
|
423
|
-
// even if there are multiple usePress instances attached to the element.
|
|
424
|
-
e[LINK_CLICKED] = true;
|
|
425
|
-
openLink(state.target, e, false);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
state.isPressed = false;
|
|
429
|
-
state.metaKeyEvents?.delete(e.key);
|
|
430
|
-
} else if (e.key === 'Meta' && state.metaKeyEvents?.size) {
|
|
431
|
-
// If we recorded keydown events that occurred while the Meta key was pressed,
|
|
432
|
-
// and those haven't received keyup events already, fire keyup events ourselves.
|
|
433
|
-
// See comment above for more info about the macOS bug causing this.
|
|
434
|
-
let events = state.metaKeyEvents;
|
|
435
|
-
state.metaKeyEvents = undefined;
|
|
436
|
-
for (let event of events.values()) {
|
|
437
|
-
state.target?.dispatchEvent(new KeyboardEvent('keyup', event));
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
|
|
442
563
|
if (typeof PointerEvent !== 'undefined') {
|
|
443
564
|
pressProps.onPointerDown = (e) => {
|
|
444
565
|
// Only handle left clicks, and ignore events that bubbled through portals.
|
|
@@ -460,6 +581,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
460
581
|
let shouldStopPropagation = true;
|
|
461
582
|
if (!state.isPressed) {
|
|
462
583
|
state.isPressed = true;
|
|
584
|
+
setIsPointerPressed('pointer');
|
|
463
585
|
state.isOverTarget = true;
|
|
464
586
|
state.activePointerId = e.pointerId;
|
|
465
587
|
state.target = e.currentTarget as FocusableElement;
|
|
@@ -476,9 +598,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
476
598
|
if ('releasePointerCapture' in target) {
|
|
477
599
|
target.releasePointerCapture(e.pointerId);
|
|
478
600
|
}
|
|
479
|
-
|
|
480
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointerup', onPointerUp, false);
|
|
481
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointercancel', onPointerCancel, false);
|
|
482
601
|
}
|
|
483
602
|
|
|
484
603
|
if (shouldStopPropagation) {
|
|
@@ -530,46 +649,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
530
649
|
}
|
|
531
650
|
};
|
|
532
651
|
|
|
533
|
-
let onPointerUp = (e: PointerEvent) => {
|
|
534
|
-
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
|
|
535
|
-
if (nodeContains(state.target, getEventTarget(e)) && state.pointerType != null) {
|
|
536
|
-
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
537
|
-
// is mutated between onPointerUp and onClick, and is more compatible with third party libraries.
|
|
538
|
-
// https://github.com/adobe/react-spectrum/issues/1513
|
|
539
|
-
// https://issues.chromium.org/issues/40732224
|
|
540
|
-
// However, iOS and Android do not focus or fire onClick after a long press.
|
|
541
|
-
// We work around this by triggering a click ourselves after a timeout.
|
|
542
|
-
// This timeout is canceled during the click event in case the real one fires first.
|
|
543
|
-
// The timeout must be at least 32ms, because Safari on iOS delays the click event on
|
|
544
|
-
// non-form elements without certain ARIA roles (for hover emulation).
|
|
545
|
-
// https://github.com/WebKit/WebKit/blob/dccfae42bb29bd4bdef052e469f604a9387241c0/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm#L875-L892
|
|
546
|
-
let clicked = false;
|
|
547
|
-
let timeout = setTimeout(() => {
|
|
548
|
-
if (state.isPressed && state.target instanceof HTMLElement) {
|
|
549
|
-
if (clicked) {
|
|
550
|
-
cancel(e);
|
|
551
|
-
} else {
|
|
552
|
-
focusWithoutScrolling(state.target);
|
|
553
|
-
state.target.click();
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}, 80);
|
|
557
|
-
// Use a capturing listener to track if a click occurred.
|
|
558
|
-
// If stopPropagation is called it may never reach our handler.
|
|
559
|
-
addGlobalListener(e.currentTarget as Document, 'click', () => clicked = true, true);
|
|
560
|
-
state.disposables.push(() => clearTimeout(timeout));
|
|
561
|
-
} else {
|
|
562
|
-
cancel(e);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Ignore subsequent onPointerLeave event before onClick on touch devices.
|
|
566
|
-
state.isOverTarget = false;
|
|
567
|
-
}
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
let onPointerCancel = (e: PointerEvent) => {
|
|
571
|
-
cancel(e);
|
|
572
|
-
};
|
|
573
652
|
|
|
574
653
|
pressProps.onDragStart = (e) => {
|
|
575
654
|
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
@@ -595,6 +674,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
595
674
|
}
|
|
596
675
|
|
|
597
676
|
state.isPressed = true;
|
|
677
|
+
setIsPointerPressed('mouse');
|
|
598
678
|
state.isOverTarget = true;
|
|
599
679
|
state.target = e.currentTarget;
|
|
600
680
|
state.pointerType = isVirtualClick(e.nativeEvent) ? 'virtual' : 'mouse';
|
|
@@ -611,8 +691,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
611
691
|
state.disposables.push(dispose);
|
|
612
692
|
}
|
|
613
693
|
}
|
|
614
|
-
|
|
615
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'mouseup', onMouseUp, false);
|
|
616
694
|
};
|
|
617
695
|
|
|
618
696
|
pressProps.onMouseEnter = (e) => {
|
|
@@ -658,27 +736,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
658
736
|
}
|
|
659
737
|
};
|
|
660
738
|
|
|
661
|
-
let onMouseUp = (e: MouseEvent) => {
|
|
662
|
-
// Only handle left clicks
|
|
663
|
-
if (e.button !== 0) {
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (state.ignoreEmulatedMouseEvents) {
|
|
668
|
-
state.ignoreEmulatedMouseEvents = false;
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (state.target && state.target.contains(e.target as Element) && state.pointerType != null) {
|
|
673
|
-
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
674
|
-
// is mutated between onMouseUp and onClick, and is more compatible with third party libraries.
|
|
675
|
-
} else {
|
|
676
|
-
cancel(e);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
state.isOverTarget = false;
|
|
680
|
-
};
|
|
681
|
-
|
|
682
739
|
pressProps.onTouchStart = (e) => {
|
|
683
740
|
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
684
741
|
return;
|
|
@@ -692,6 +749,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
692
749
|
state.ignoreEmulatedMouseEvents = true;
|
|
693
750
|
state.isOverTarget = true;
|
|
694
751
|
state.isPressed = true;
|
|
752
|
+
setIsPointerPressed('touch');
|
|
695
753
|
state.target = e.currentTarget;
|
|
696
754
|
state.pointerType = 'touch';
|
|
697
755
|
|
|
@@ -703,8 +761,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
703
761
|
if (shouldStopPropagation) {
|
|
704
762
|
e.stopPropagation();
|
|
705
763
|
}
|
|
706
|
-
|
|
707
|
-
addGlobalListener(getOwnerWindow(e.currentTarget), 'scroll', onScroll, true);
|
|
708
764
|
};
|
|
709
765
|
|
|
710
766
|
pressProps.onTouchMove = (e) => {
|
|
@@ -760,6 +816,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
760
816
|
}
|
|
761
817
|
|
|
762
818
|
state.isPressed = false;
|
|
819
|
+
setIsPointerPressed(null);
|
|
763
820
|
state.activePointerId = null;
|
|
764
821
|
state.isOverTarget = false;
|
|
765
822
|
state.ignoreEmulatedMouseEvents = true;
|
|
@@ -780,18 +837,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
780
837
|
}
|
|
781
838
|
};
|
|
782
839
|
|
|
783
|
-
let onScroll = (e: Event) => {
|
|
784
|
-
if (state.isPressed && nodeContains(getEventTarget(e), state.target)) {
|
|
785
|
-
cancel({
|
|
786
|
-
currentTarget: state.target,
|
|
787
|
-
shiftKey: false,
|
|
788
|
-
ctrlKey: false,
|
|
789
|
-
metaKey: false,
|
|
790
|
-
altKey: false
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
};
|
|
794
|
-
|
|
795
840
|
pressProps.onDragStart = (e) => {
|
|
796
841
|
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
797
842
|
return;
|
|
@@ -803,7 +848,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
803
848
|
|
|
804
849
|
return pressProps;
|
|
805
850
|
}, [
|
|
806
|
-
addGlobalListener,
|
|
807
851
|
isDisabled,
|
|
808
852
|
preventFocusOnPress,
|
|
809
853
|
removeAllGlobalListeners,
|
package/src/utils.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {FocusableElement} from '@react-types/shared';
|
|
14
|
-
import {focusWithoutScrolling, getOwnerWindow, isFocusable,
|
|
14
|
+
import {focusWithoutScrolling, getOwnerWindow, isFocusable, useLayoutEffect} from '@react-aria/utils';
|
|
15
15
|
import {FocusEvent as ReactFocusEvent, SyntheticEvent, useCallback, useRef} from 'react';
|
|
16
16
|
|
|
17
17
|
// Turn a native event into a React synthetic event.
|
|
@@ -48,10 +48,6 @@ export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur:
|
|
|
48
48
|
};
|
|
49
49
|
}, []);
|
|
50
50
|
|
|
51
|
-
let dispatchBlur = useEffectEvent((e: ReactFocusEvent<Target>) => {
|
|
52
|
-
onBlur?.(e);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
51
|
// This function is called during a React onFocus event.
|
|
56
52
|
return useCallback((e: ReactFocusEvent<Target>) => {
|
|
57
53
|
// React does not fire onBlur when an element is disabled. https://github.com/facebook/react/issues/9142
|
|
@@ -73,7 +69,7 @@ export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur:
|
|
|
73
69
|
if (target.disabled) {
|
|
74
70
|
// For backward compatibility, dispatch a (fake) React synthetic event.
|
|
75
71
|
let event = createSyntheticEvent<ReactFocusEvent<Target>>(e);
|
|
76
|
-
|
|
72
|
+
onBlur?.(event);
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
// We no longer need the MutationObserver once the target is blurred.
|
|
@@ -96,7 +92,7 @@ export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur:
|
|
|
96
92
|
|
|
97
93
|
stateRef.current.observer.observe(target, {attributes: true, attributeFilter: ['disabled']});
|
|
98
94
|
}
|
|
99
|
-
}, [
|
|
95
|
+
}, [onBlur]);
|
|
100
96
|
}
|
|
101
97
|
|
|
102
98
|
export let ignoreFocusEvent = false;
|