@react-aria/interactions 3.25.6 → 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 +186 -114
- package/dist/usePress.main.js.map +1 -1
- package/dist/usePress.mjs +188 -116
- package/dist/usePress.module.js +188 -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 +3 -3
- 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 +183 -147
- 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,23 +293,24 @@ 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]);
|
|
299
304
|
|
|
300
|
-
let triggerClick =
|
|
305
|
+
let triggerClick = useCallback((e: RMouseEvent<FocusableElement>) => {
|
|
301
306
|
if (isDisabled) {
|
|
302
307
|
return;
|
|
303
308
|
}
|
|
304
309
|
|
|
305
310
|
onClick?.(e);
|
|
306
|
-
});
|
|
311
|
+
}, [isDisabled, onClick]);
|
|
307
312
|
|
|
308
|
-
let triggerSyntheticClick =
|
|
313
|
+
let triggerSyntheticClick = useCallback((e: KeyboardEvent | TouchEvent, target: FocusableElement) => {
|
|
309
314
|
if (isDisabled) {
|
|
310
315
|
return;
|
|
311
316
|
}
|
|
@@ -320,7 +325,164 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
320
325
|
setEventTarget(event, target);
|
|
321
326
|
onClick(createSyntheticEvent(event));
|
|
322
327
|
}
|
|
323
|
-
});
|
|
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]);
|
|
324
486
|
|
|
325
487
|
let pressProps = useMemo(() => {
|
|
326
488
|
let state = ref.current;
|
|
@@ -338,20 +500,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
338
500
|
if (!state.isPressed && !e.repeat) {
|
|
339
501
|
state.target = e.currentTarget;
|
|
340
502
|
state.isPressed = true;
|
|
503
|
+
setIsElemKeyPressed(true);
|
|
341
504
|
state.pointerType = 'keyboard';
|
|
342
505
|
shouldStopPropagation = triggerPressStart(e, 'keyboard');
|
|
343
|
-
|
|
344
|
-
// Focus may move before the key up event, so register the event on the document
|
|
345
|
-
// instead of the same element where the key down event occurred. Make it capturing so that it will trigger
|
|
346
|
-
// before stopPropagation from useKeyboard on a child element may happen and thus we can still call triggerPress for the parent element.
|
|
347
|
-
let originalTarget = e.currentTarget;
|
|
348
|
-
let pressUp = (e) => {
|
|
349
|
-
if (isValidKeyboardEvent(e, originalTarget) && !e.repeat && nodeContains(originalTarget, getEventTarget(e)) && state.target) {
|
|
350
|
-
triggerPressUp(createEvent(state.target, e), 'keyboard');
|
|
351
|
-
}
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'keyup', chain(pressUp, onKeyUp), true);
|
|
355
506
|
}
|
|
356
507
|
|
|
357
508
|
if (shouldStopPropagation) {
|
|
@@ -409,44 +560,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
409
560
|
}
|
|
410
561
|
};
|
|
411
562
|
|
|
412
|
-
let onKeyUp = (e: KeyboardEvent) => {
|
|
413
|
-
if (state.isPressed && state.target && isValidKeyboardEvent(e, state.target)) {
|
|
414
|
-
if (shouldPreventDefaultKeyboard(getEventTarget(e), e.key)) {
|
|
415
|
-
e.preventDefault();
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
let target = getEventTarget(e);
|
|
419
|
-
let wasPressed = nodeContains(state.target, getEventTarget(e));
|
|
420
|
-
triggerPressEnd(createEvent(state.target, e), 'keyboard', wasPressed);
|
|
421
|
-
if (wasPressed) {
|
|
422
|
-
triggerSyntheticClick(e, state.target);
|
|
423
|
-
}
|
|
424
|
-
removeAllGlobalListeners();
|
|
425
|
-
|
|
426
|
-
// If a link was triggered with a key other than Enter, open the URL ourselves.
|
|
427
|
-
// This means the link has a role override, and the default browser behavior
|
|
428
|
-
// only applies when using the Enter key.
|
|
429
|
-
if (e.key !== 'Enter' && isHTMLAnchorLink(state.target) && nodeContains(state.target, target) && !e[LINK_CLICKED]) {
|
|
430
|
-
// Store a hidden property on the event so we only trigger link click once,
|
|
431
|
-
// even if there are multiple usePress instances attached to the element.
|
|
432
|
-
e[LINK_CLICKED] = true;
|
|
433
|
-
openLink(state.target, e, false);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
state.isPressed = false;
|
|
437
|
-
state.metaKeyEvents?.delete(e.key);
|
|
438
|
-
} else if (e.key === 'Meta' && state.metaKeyEvents?.size) {
|
|
439
|
-
// If we recorded keydown events that occurred while the Meta key was pressed,
|
|
440
|
-
// and those haven't received keyup events already, fire keyup events ourselves.
|
|
441
|
-
// See comment above for more info about the macOS bug causing this.
|
|
442
|
-
let events = state.metaKeyEvents;
|
|
443
|
-
state.metaKeyEvents = undefined;
|
|
444
|
-
for (let event of events.values()) {
|
|
445
|
-
state.target?.dispatchEvent(new KeyboardEvent('keyup', event));
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
|
-
|
|
450
563
|
if (typeof PointerEvent !== 'undefined') {
|
|
451
564
|
pressProps.onPointerDown = (e) => {
|
|
452
565
|
// Only handle left clicks, and ignore events that bubbled through portals.
|
|
@@ -468,6 +581,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
468
581
|
let shouldStopPropagation = true;
|
|
469
582
|
if (!state.isPressed) {
|
|
470
583
|
state.isPressed = true;
|
|
584
|
+
setIsPointerPressed('pointer');
|
|
471
585
|
state.isOverTarget = true;
|
|
472
586
|
state.activePointerId = e.pointerId;
|
|
473
587
|
state.target = e.currentTarget as FocusableElement;
|
|
@@ -484,9 +598,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
484
598
|
if ('releasePointerCapture' in target) {
|
|
485
599
|
target.releasePointerCapture(e.pointerId);
|
|
486
600
|
}
|
|
487
|
-
|
|
488
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointerup', onPointerUp, false);
|
|
489
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointercancel', onPointerCancel, false);
|
|
490
601
|
}
|
|
491
602
|
|
|
492
603
|
if (shouldStopPropagation) {
|
|
@@ -538,46 +649,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
538
649
|
}
|
|
539
650
|
};
|
|
540
651
|
|
|
541
|
-
let onPointerUp = (e: PointerEvent) => {
|
|
542
|
-
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
|
|
543
|
-
if (nodeContains(state.target, getEventTarget(e)) && state.pointerType != null) {
|
|
544
|
-
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
545
|
-
// is mutated between onPointerUp and onClick, and is more compatible with third party libraries.
|
|
546
|
-
// https://github.com/adobe/react-spectrum/issues/1513
|
|
547
|
-
// https://issues.chromium.org/issues/40732224
|
|
548
|
-
// However, iOS and Android do not focus or fire onClick after a long press.
|
|
549
|
-
// We work around this by triggering a click ourselves after a timeout.
|
|
550
|
-
// This timeout is canceled during the click event in case the real one fires first.
|
|
551
|
-
// The timeout must be at least 32ms, because Safari on iOS delays the click event on
|
|
552
|
-
// non-form elements without certain ARIA roles (for hover emulation).
|
|
553
|
-
// https://github.com/WebKit/WebKit/blob/dccfae42bb29bd4bdef052e469f604a9387241c0/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm#L875-L892
|
|
554
|
-
let clicked = false;
|
|
555
|
-
let timeout = setTimeout(() => {
|
|
556
|
-
if (state.isPressed && state.target instanceof HTMLElement) {
|
|
557
|
-
if (clicked) {
|
|
558
|
-
cancel(e);
|
|
559
|
-
} else {
|
|
560
|
-
focusWithoutScrolling(state.target);
|
|
561
|
-
state.target.click();
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}, 80);
|
|
565
|
-
// Use a capturing listener to track if a click occurred.
|
|
566
|
-
// If stopPropagation is called it may never reach our handler.
|
|
567
|
-
addGlobalListener(e.currentTarget as Document, 'click', () => clicked = true, true);
|
|
568
|
-
state.disposables.push(() => clearTimeout(timeout));
|
|
569
|
-
} else {
|
|
570
|
-
cancel(e);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Ignore subsequent onPointerLeave event before onClick on touch devices.
|
|
574
|
-
state.isOverTarget = false;
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
let onPointerCancel = (e: PointerEvent) => {
|
|
579
|
-
cancel(e);
|
|
580
|
-
};
|
|
581
652
|
|
|
582
653
|
pressProps.onDragStart = (e) => {
|
|
583
654
|
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
@@ -603,6 +674,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
603
674
|
}
|
|
604
675
|
|
|
605
676
|
state.isPressed = true;
|
|
677
|
+
setIsPointerPressed('mouse');
|
|
606
678
|
state.isOverTarget = true;
|
|
607
679
|
state.target = e.currentTarget;
|
|
608
680
|
state.pointerType = isVirtualClick(e.nativeEvent) ? 'virtual' : 'mouse';
|
|
@@ -619,8 +691,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
619
691
|
state.disposables.push(dispose);
|
|
620
692
|
}
|
|
621
693
|
}
|
|
622
|
-
|
|
623
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'mouseup', onMouseUp, false);
|
|
624
694
|
};
|
|
625
695
|
|
|
626
696
|
pressProps.onMouseEnter = (e) => {
|
|
@@ -666,27 +736,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
666
736
|
}
|
|
667
737
|
};
|
|
668
738
|
|
|
669
|
-
let onMouseUp = (e: MouseEvent) => {
|
|
670
|
-
// Only handle left clicks
|
|
671
|
-
if (e.button !== 0) {
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (state.ignoreEmulatedMouseEvents) {
|
|
676
|
-
state.ignoreEmulatedMouseEvents = false;
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
if (state.target && state.target.contains(e.target as Element) && state.pointerType != null) {
|
|
681
|
-
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
682
|
-
// is mutated between onMouseUp and onClick, and is more compatible with third party libraries.
|
|
683
|
-
} else {
|
|
684
|
-
cancel(e);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
state.isOverTarget = false;
|
|
688
|
-
};
|
|
689
|
-
|
|
690
739
|
pressProps.onTouchStart = (e) => {
|
|
691
740
|
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
692
741
|
return;
|
|
@@ -700,6 +749,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
700
749
|
state.ignoreEmulatedMouseEvents = true;
|
|
701
750
|
state.isOverTarget = true;
|
|
702
751
|
state.isPressed = true;
|
|
752
|
+
setIsPointerPressed('touch');
|
|
703
753
|
state.target = e.currentTarget;
|
|
704
754
|
state.pointerType = 'touch';
|
|
705
755
|
|
|
@@ -711,8 +761,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
711
761
|
if (shouldStopPropagation) {
|
|
712
762
|
e.stopPropagation();
|
|
713
763
|
}
|
|
714
|
-
|
|
715
|
-
addGlobalListener(getOwnerWindow(e.currentTarget), 'scroll', onScroll, true);
|
|
716
764
|
};
|
|
717
765
|
|
|
718
766
|
pressProps.onTouchMove = (e) => {
|
|
@@ -768,6 +816,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
768
816
|
}
|
|
769
817
|
|
|
770
818
|
state.isPressed = false;
|
|
819
|
+
setIsPointerPressed(null);
|
|
771
820
|
state.activePointerId = null;
|
|
772
821
|
state.isOverTarget = false;
|
|
773
822
|
state.ignoreEmulatedMouseEvents = true;
|
|
@@ -788,18 +837,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
788
837
|
}
|
|
789
838
|
};
|
|
790
839
|
|
|
791
|
-
let onScroll = (e: Event) => {
|
|
792
|
-
if (state.isPressed && nodeContains(getEventTarget(e), state.target)) {
|
|
793
|
-
cancel({
|
|
794
|
-
currentTarget: state.target,
|
|
795
|
-
shiftKey: false,
|
|
796
|
-
ctrlKey: false,
|
|
797
|
-
metaKey: false,
|
|
798
|
-
altKey: false
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
};
|
|
802
|
-
|
|
803
840
|
pressProps.onDragStart = (e) => {
|
|
804
841
|
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
805
842
|
return;
|
|
@@ -811,7 +848,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
811
848
|
|
|
812
849
|
return pressProps;
|
|
813
850
|
}, [
|
|
814
|
-
addGlobalListener,
|
|
815
851
|
isDisabled,
|
|
816
852
|
preventFocusOnPress,
|
|
817
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;
|