@react-aria/interactions 3.25.6 → 3.27.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/PressResponder.main.js +2 -3
- package/dist/PressResponder.main.js.map +1 -1
- package/dist/PressResponder.mjs +3 -4
- package/dist/PressResponder.module.js +3 -4
- package/dist/PressResponder.module.js.map +1 -1
- 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 +4 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/useFocusVisible.main.js +19 -8
- package/dist/useFocusVisible.main.js.map +1 -1
- package/dist/useFocusVisible.mjs +20 -10
- package/dist/useFocusVisible.module.js +20 -10
- package/dist/useFocusVisible.module.js.map +1 -1
- package/dist/useFocusWithin.main.js +3 -3
- package/dist/useFocusWithin.main.js.map +1 -1
- package/dist/useFocusWithin.mjs +4 -4
- package/dist/useFocusWithin.module.js +4 -4
- package/dist/useFocusWithin.module.js.map +1 -1
- package/dist/useHover.main.js +3 -3
- package/dist/useHover.main.js.map +1 -1
- package/dist/useHover.mjs +4 -4
- package/dist/useHover.module.js +4 -4
- package/dist/useHover.module.js.map +1 -1
- package/dist/useInteractOutside.main.js +2 -4
- package/dist/useInteractOutside.main.js.map +1 -1
- package/dist/useInteractOutside.mjs +3 -5
- package/dist/useInteractOutside.module.js +3 -5
- 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 +197 -117
- package/dist/usePress.main.js.map +1 -1
- package/dist/usePress.mjs +199 -119
- package/dist/usePress.module.js +199 -119
- 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/PressResponder.tsx +3 -4
- package/src/focusSafely.ts +4 -4
- package/src/index.ts +1 -0
- package/src/useFocusVisible.ts +21 -5
- package/src/useFocusWithin.ts +3 -3
- package/src/useHover.ts +3 -3
- package/src/useInteractOutside.ts +3 -3
- package/src/useMove.ts +85 -57
- package/src/usePress.ts +199 -151
- 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). */
|
|
@@ -83,7 +84,8 @@ interface EventBase {
|
|
|
83
84
|
altKey: boolean,
|
|
84
85
|
clientX?: number,
|
|
85
86
|
clientY?: number,
|
|
86
|
-
targetTouches?: Array<{clientX?: number, clientY?: number}
|
|
87
|
+
targetTouches?: Array<{clientX?: number, clientY?: number}>,
|
|
88
|
+
key?: string
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
export interface PressResult {
|
|
@@ -97,7 +99,9 @@ function usePressResponderContext(props: PressHookProps): PressHookProps {
|
|
|
97
99
|
// Consume context from <PressResponder> and merge with props.
|
|
98
100
|
let context = useContext(PressResponderContext);
|
|
99
101
|
if (context) {
|
|
100
|
-
|
|
102
|
+
// Prevent mergeProps from merging ref.
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
104
|
+
let {register, ref, ...contextProps} = context;
|
|
101
105
|
props = mergeProps(contextProps, props) as PressHookProps;
|
|
102
106
|
register();
|
|
103
107
|
}
|
|
@@ -116,6 +120,7 @@ class PressEvent implements IPressEvent {
|
|
|
116
120
|
altKey: boolean;
|
|
117
121
|
x: number;
|
|
118
122
|
y: number;
|
|
123
|
+
key: string | undefined;
|
|
119
124
|
#shouldStopPropagation = true;
|
|
120
125
|
|
|
121
126
|
constructor(type: IPressEvent['type'], pointerType: PointerType, originalEvent: EventBase, state?: PressState) {
|
|
@@ -145,6 +150,7 @@ class PressEvent implements IPressEvent {
|
|
|
145
150
|
this.altKey = originalEvent.altKey;
|
|
146
151
|
this.x = x;
|
|
147
152
|
this.y = y;
|
|
153
|
+
this.key = originalEvent.key;
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
continuePropagation() {
|
|
@@ -195,9 +201,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
195
201
|
disposables: []
|
|
196
202
|
});
|
|
197
203
|
|
|
198
|
-
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
|
|
204
|
+
let {addGlobalListener, removeAllGlobalListeners, removeGlobalListener} = useGlobalListeners();
|
|
199
205
|
|
|
200
|
-
let triggerPressStart =
|
|
206
|
+
let triggerPressStart = useCallback((originalEvent: EventBase, pointerType: PointerType) => {
|
|
201
207
|
let state = ref.current;
|
|
202
208
|
if (isDisabled || state.didFirePressStart) {
|
|
203
209
|
return false;
|
|
@@ -219,9 +225,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
219
225
|
state.didFirePressStart = true;
|
|
220
226
|
setPressed(true);
|
|
221
227
|
return shouldStopPropagation;
|
|
222
|
-
});
|
|
228
|
+
}, [isDisabled, onPressStart, onPressChange]);
|
|
223
229
|
|
|
224
|
-
let triggerPressEnd =
|
|
230
|
+
let triggerPressEnd = useCallback((originalEvent: EventBase, pointerType: PointerType, wasPressed = true) => {
|
|
225
231
|
let state = ref.current;
|
|
226
232
|
if (!state.didFirePressStart) {
|
|
227
233
|
return false;
|
|
@@ -251,9 +257,10 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
251
257
|
|
|
252
258
|
state.isTriggeringEvent = false;
|
|
253
259
|
return shouldStopPropagation;
|
|
254
|
-
});
|
|
260
|
+
}, [isDisabled, onPressEnd, onPressChange, onPress]);
|
|
261
|
+
let triggerPressEndEvent = useEffectEvent(triggerPressEnd);
|
|
255
262
|
|
|
256
|
-
let triggerPressUp =
|
|
263
|
+
let triggerPressUp = useCallback((originalEvent: EventBase, pointerType: PointerType) => {
|
|
257
264
|
let state = ref.current;
|
|
258
265
|
if (isDisabled) {
|
|
259
266
|
return false;
|
|
@@ -268,15 +275,17 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
return true;
|
|
271
|
-
});
|
|
278
|
+
}, [isDisabled, onPressUp]);
|
|
279
|
+
let triggerPressUpEvent = useEffectEvent(triggerPressUp);
|
|
272
280
|
|
|
273
|
-
let cancel =
|
|
281
|
+
let cancel = useCallback((e: EventBase) => {
|
|
274
282
|
let state = ref.current;
|
|
275
283
|
if (state.isPressed && state.target) {
|
|
276
284
|
if (state.didFirePressStart && state.pointerType != null) {
|
|
277
285
|
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
278
286
|
}
|
|
279
287
|
state.isPressed = false;
|
|
288
|
+
setIsPointerPressed(null);
|
|
280
289
|
state.isOverTarget = false;
|
|
281
290
|
state.activePointerId = null;
|
|
282
291
|
state.pointerType = null;
|
|
@@ -289,23 +298,24 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
289
298
|
}
|
|
290
299
|
state.disposables = [];
|
|
291
300
|
}
|
|
292
|
-
});
|
|
301
|
+
}, [allowTextSelectionOnPress, removeAllGlobalListeners, triggerPressEnd]);
|
|
302
|
+
let cancelEvent = useEffectEvent(cancel);
|
|
293
303
|
|
|
294
|
-
let cancelOnPointerExit =
|
|
304
|
+
let cancelOnPointerExit = useCallback((e: EventBase) => {
|
|
295
305
|
if (shouldCancelOnPointerExit) {
|
|
296
306
|
cancel(e);
|
|
297
307
|
}
|
|
298
|
-
});
|
|
308
|
+
}, [shouldCancelOnPointerExit, cancel]);
|
|
299
309
|
|
|
300
|
-
let triggerClick =
|
|
310
|
+
let triggerClick = useCallback((e: RMouseEvent<FocusableElement>) => {
|
|
301
311
|
if (isDisabled) {
|
|
302
312
|
return;
|
|
303
313
|
}
|
|
304
314
|
|
|
305
315
|
onClick?.(e);
|
|
306
|
-
});
|
|
316
|
+
}, [isDisabled, onClick]);
|
|
307
317
|
|
|
308
|
-
let triggerSyntheticClick =
|
|
318
|
+
let triggerSyntheticClick = useCallback((e: KeyboardEvent | TouchEvent, target: FocusableElement) => {
|
|
309
319
|
if (isDisabled) {
|
|
310
320
|
return;
|
|
311
321
|
}
|
|
@@ -320,7 +330,164 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
320
330
|
setEventTarget(event, target);
|
|
321
331
|
onClick(createSyntheticEvent(event));
|
|
322
332
|
}
|
|
323
|
-
});
|
|
333
|
+
}, [isDisabled, onClick]);
|
|
334
|
+
let triggerSyntheticClickEvent = useEffectEvent(triggerSyntheticClick);
|
|
335
|
+
|
|
336
|
+
let [isElemKeyPressed, setIsElemKeyPressed] = useState<boolean>(false);
|
|
337
|
+
useLayoutEffect(() => {
|
|
338
|
+
let state = ref.current;
|
|
339
|
+
if (isElemKeyPressed) {
|
|
340
|
+
let onKeyUp = (e: KeyboardEvent) => {
|
|
341
|
+
if (state.isPressed && state.target && isValidKeyboardEvent(e, state.target)) {
|
|
342
|
+
if (shouldPreventDefaultKeyboard(getEventTarget(e), e.key)) {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let target = getEventTarget(e);
|
|
347
|
+
let wasPressed = nodeContains(state.target, getEventTarget(e));
|
|
348
|
+
triggerPressEndEvent(createEvent(state.target, e), 'keyboard', wasPressed);
|
|
349
|
+
if (wasPressed) {
|
|
350
|
+
triggerSyntheticClickEvent(e, state.target);
|
|
351
|
+
}
|
|
352
|
+
removeAllGlobalListeners();
|
|
353
|
+
|
|
354
|
+
// If a link was triggered with a key other than Enter, open the URL ourselves.
|
|
355
|
+
// This means the link has a role override, and the default browser behavior
|
|
356
|
+
// only applies when using the Enter key.
|
|
357
|
+
if (e.key !== 'Enter' && isHTMLAnchorLink(state.target) && nodeContains(state.target, target) && !e[LINK_CLICKED]) {
|
|
358
|
+
// Store a hidden property on the event so we only trigger link click once,
|
|
359
|
+
// even if there are multiple usePress instances attached to the element.
|
|
360
|
+
e[LINK_CLICKED] = true;
|
|
361
|
+
openLink(state.target, e, false);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
state.isPressed = false;
|
|
365
|
+
setIsElemKeyPressed(false);
|
|
366
|
+
state.metaKeyEvents?.delete(e.key);
|
|
367
|
+
} else if (e.key === 'Meta' && state.metaKeyEvents?.size) {
|
|
368
|
+
// If we recorded keydown events that occurred while the Meta key was pressed,
|
|
369
|
+
// and those haven't received keyup events already, fire keyup events ourselves.
|
|
370
|
+
// See comment above for more info about the macOS bug causing this.
|
|
371
|
+
let events = state.metaKeyEvents;
|
|
372
|
+
state.metaKeyEvents = undefined;
|
|
373
|
+
for (let event of events.values()) {
|
|
374
|
+
state.target?.dispatchEvent(new KeyboardEvent('keyup', event));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
// Focus may move before the key up event, so register the event on the document
|
|
379
|
+
// instead of the same element where the key down event occurred. Make it capturing so that it will trigger
|
|
380
|
+
// before stopPropagation from useKeyboard on a child element may happen and thus we can still call triggerPress for the parent element.
|
|
381
|
+
let originalTarget = state.target;
|
|
382
|
+
let pressUp = (e) => {
|
|
383
|
+
if (originalTarget && isValidKeyboardEvent(e, originalTarget) && !e.repeat && nodeContains(originalTarget, getEventTarget(e)) && state.target) {
|
|
384
|
+
triggerPressUpEvent(createEvent(state.target, e), 'keyboard');
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
let listener = chain(pressUp, onKeyUp);
|
|
388
|
+
addGlobalListener(getOwnerDocument(state.target), 'keyup', listener, true);
|
|
389
|
+
return () => {
|
|
390
|
+
removeGlobalListener(getOwnerDocument(state.target), 'keyup', listener, true);
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}, [isElemKeyPressed, addGlobalListener, removeAllGlobalListeners, removeGlobalListener]);
|
|
394
|
+
|
|
395
|
+
let [isPointerPressed, setIsPointerPressed] = useState<'pointer' | 'mouse' | 'touch' | null>(null);
|
|
396
|
+
useLayoutEffect(() => {
|
|
397
|
+
let state = ref.current;
|
|
398
|
+
if (isPointerPressed === 'pointer') {
|
|
399
|
+
let onPointerUp = (e: PointerEvent) => {
|
|
400
|
+
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
|
|
401
|
+
if (nodeContains(state.target, getEventTarget(e)) && state.pointerType != null) {
|
|
402
|
+
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
403
|
+
// is mutated between onPointerUp and onClick, and is more compatible with third party libraries.
|
|
404
|
+
// https://github.com/adobe/react-spectrum/issues/1513
|
|
405
|
+
// https://issues.chromium.org/issues/40732224
|
|
406
|
+
// However, iOS and Android do not focus or fire onClick after a long press.
|
|
407
|
+
// We work around this by triggering a click ourselves after a timeout.
|
|
408
|
+
// This timeout is canceled during the click event in case the real one fires first.
|
|
409
|
+
// The timeout must be at least 32ms, because Safari on iOS delays the click event on
|
|
410
|
+
// non-form elements without certain ARIA roles (for hover emulation).
|
|
411
|
+
// https://github.com/WebKit/WebKit/blob/dccfae42bb29bd4bdef052e469f604a9387241c0/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm#L875-L892
|
|
412
|
+
let clicked = false;
|
|
413
|
+
let timeout = setTimeout(() => {
|
|
414
|
+
if (state.isPressed && state.target instanceof HTMLElement) {
|
|
415
|
+
if (clicked) {
|
|
416
|
+
cancelEvent(e);
|
|
417
|
+
} else {
|
|
418
|
+
focusWithoutScrolling(state.target);
|
|
419
|
+
state.target.click();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}, 80);
|
|
423
|
+
// Use a capturing listener to track if a click occurred.
|
|
424
|
+
// If stopPropagation is called it may never reach our handler.
|
|
425
|
+
addGlobalListener(e.currentTarget as Document, 'click', () => clicked = true, true);
|
|
426
|
+
state.disposables.push(() => clearTimeout(timeout));
|
|
427
|
+
} else {
|
|
428
|
+
cancelEvent(e);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Ignore subsequent onPointerLeave event before onClick on touch devices.
|
|
432
|
+
state.isOverTarget = false;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
let onPointerCancel = (e: PointerEvent) => {
|
|
437
|
+
cancelEvent(e);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
addGlobalListener(getOwnerDocument(state.target), 'pointerup', onPointerUp, false);
|
|
441
|
+
addGlobalListener(getOwnerDocument(state.target), 'pointercancel', onPointerCancel, false);
|
|
442
|
+
return () => {
|
|
443
|
+
removeGlobalListener(getOwnerDocument(state.target), 'pointerup', onPointerUp, false);
|
|
444
|
+
removeGlobalListener(getOwnerDocument(state.target), 'pointercancel', onPointerCancel, false);
|
|
445
|
+
};
|
|
446
|
+
} else if (isPointerPressed === 'mouse' && process.env.NODE_ENV === 'test') {
|
|
447
|
+
let onMouseUp = (e: MouseEvent) => {
|
|
448
|
+
// Only handle left clicks
|
|
449
|
+
if (e.button !== 0) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (state.ignoreEmulatedMouseEvents) {
|
|
454
|
+
state.ignoreEmulatedMouseEvents = false;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (state.target && nodeContains(state.target, e.target as Element) && state.pointerType != null) {
|
|
459
|
+
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
460
|
+
// is mutated between onMouseUp and onClick, and is more compatible with third party libraries.
|
|
461
|
+
} else {
|
|
462
|
+
cancelEvent(e);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
state.isOverTarget = false;
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
addGlobalListener(getOwnerDocument(state.target), 'mouseup', onMouseUp, false);
|
|
469
|
+
return () => {
|
|
470
|
+
removeGlobalListener(getOwnerDocument(state.target), 'mouseup', onMouseUp, false);
|
|
471
|
+
};
|
|
472
|
+
} else if (isPointerPressed === 'touch' && process.env.NODE_ENV === 'test') {
|
|
473
|
+
let onScroll = (e: Event) => {
|
|
474
|
+
if (state.isPressed && nodeContains(getEventTarget(e), state.target)) {
|
|
475
|
+
cancelEvent({
|
|
476
|
+
currentTarget: state.target,
|
|
477
|
+
shiftKey: false,
|
|
478
|
+
ctrlKey: false,
|
|
479
|
+
metaKey: false,
|
|
480
|
+
altKey: false
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
addGlobalListener(getOwnerWindow(state.target), 'scroll', onScroll, true);
|
|
486
|
+
return () => {
|
|
487
|
+
removeGlobalListener(getOwnerWindow(state.target), 'scroll', onScroll, true);
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}, [isPointerPressed, addGlobalListener, removeGlobalListener]);
|
|
324
491
|
|
|
325
492
|
let pressProps = useMemo(() => {
|
|
326
493
|
let state = ref.current;
|
|
@@ -338,20 +505,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
338
505
|
if (!state.isPressed && !e.repeat) {
|
|
339
506
|
state.target = e.currentTarget;
|
|
340
507
|
state.isPressed = true;
|
|
508
|
+
setIsElemKeyPressed(true);
|
|
341
509
|
state.pointerType = 'keyboard';
|
|
342
510
|
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
511
|
}
|
|
356
512
|
|
|
357
513
|
if (shouldStopPropagation) {
|
|
@@ -409,44 +565,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
409
565
|
}
|
|
410
566
|
};
|
|
411
567
|
|
|
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
568
|
if (typeof PointerEvent !== 'undefined') {
|
|
451
569
|
pressProps.onPointerDown = (e) => {
|
|
452
570
|
// Only handle left clicks, and ignore events that bubbled through portals.
|
|
@@ -468,6 +586,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
468
586
|
let shouldStopPropagation = true;
|
|
469
587
|
if (!state.isPressed) {
|
|
470
588
|
state.isPressed = true;
|
|
589
|
+
setIsPointerPressed('pointer');
|
|
471
590
|
state.isOverTarget = true;
|
|
472
591
|
state.activePointerId = e.pointerId;
|
|
473
592
|
state.target = e.currentTarget as FocusableElement;
|
|
@@ -482,11 +601,14 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
482
601
|
// This enables onPointerLeave and onPointerEnter to fire.
|
|
483
602
|
let target = getEventTarget(e.nativeEvent);
|
|
484
603
|
if ('releasePointerCapture' in target) {
|
|
485
|
-
target
|
|
604
|
+
if ('hasPointerCapture' in target) {
|
|
605
|
+
if (target.hasPointerCapture(e.pointerId)) {
|
|
606
|
+
target.releasePointerCapture(e.pointerId);
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
(target as Element).releasePointerCapture(e.pointerId);
|
|
610
|
+
}
|
|
486
611
|
}
|
|
487
|
-
|
|
488
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointerup', onPointerUp, false);
|
|
489
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointercancel', onPointerCancel, false);
|
|
490
612
|
}
|
|
491
613
|
|
|
492
614
|
if (shouldStopPropagation) {
|
|
@@ -538,46 +660,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
538
660
|
}
|
|
539
661
|
};
|
|
540
662
|
|
|
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
663
|
|
|
582
664
|
pressProps.onDragStart = (e) => {
|
|
583
665
|
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
@@ -603,6 +685,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
603
685
|
}
|
|
604
686
|
|
|
605
687
|
state.isPressed = true;
|
|
688
|
+
setIsPointerPressed('mouse');
|
|
606
689
|
state.isOverTarget = true;
|
|
607
690
|
state.target = e.currentTarget;
|
|
608
691
|
state.pointerType = isVirtualClick(e.nativeEvent) ? 'virtual' : 'mouse';
|
|
@@ -619,8 +702,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
619
702
|
state.disposables.push(dispose);
|
|
620
703
|
}
|
|
621
704
|
}
|
|
622
|
-
|
|
623
|
-
addGlobalListener(getOwnerDocument(e.currentTarget), 'mouseup', onMouseUp, false);
|
|
624
705
|
};
|
|
625
706
|
|
|
626
707
|
pressProps.onMouseEnter = (e) => {
|
|
@@ -666,27 +747,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
666
747
|
}
|
|
667
748
|
};
|
|
668
749
|
|
|
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
750
|
pressProps.onTouchStart = (e) => {
|
|
691
751
|
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
692
752
|
return;
|
|
@@ -700,6 +760,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
700
760
|
state.ignoreEmulatedMouseEvents = true;
|
|
701
761
|
state.isOverTarget = true;
|
|
702
762
|
state.isPressed = true;
|
|
763
|
+
setIsPointerPressed('touch');
|
|
703
764
|
state.target = e.currentTarget;
|
|
704
765
|
state.pointerType = 'touch';
|
|
705
766
|
|
|
@@ -711,8 +772,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
711
772
|
if (shouldStopPropagation) {
|
|
712
773
|
e.stopPropagation();
|
|
713
774
|
}
|
|
714
|
-
|
|
715
|
-
addGlobalListener(getOwnerWindow(e.currentTarget), 'scroll', onScroll, true);
|
|
716
775
|
};
|
|
717
776
|
|
|
718
777
|
pressProps.onTouchMove = (e) => {
|
|
@@ -768,6 +827,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
768
827
|
}
|
|
769
828
|
|
|
770
829
|
state.isPressed = false;
|
|
830
|
+
setIsPointerPressed(null);
|
|
771
831
|
state.activePointerId = null;
|
|
772
832
|
state.isOverTarget = false;
|
|
773
833
|
state.ignoreEmulatedMouseEvents = true;
|
|
@@ -788,18 +848,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
788
848
|
}
|
|
789
849
|
};
|
|
790
850
|
|
|
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
851
|
pressProps.onDragStart = (e) => {
|
|
804
852
|
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
805
853
|
return;
|
|
@@ -811,7 +859,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
811
859
|
|
|
812
860
|
return pressProps;
|
|
813
861
|
}, [
|
|
814
|
-
addGlobalListener,
|
|
815
862
|
isDisabled,
|
|
816
863
|
preventFocusOnPress,
|
|
817
864
|
removeAllGlobalListeners,
|
|
@@ -941,7 +988,8 @@ function createEvent(target: FocusableElement, e: EventBase): EventBase {
|
|
|
941
988
|
metaKey: e.metaKey,
|
|
942
989
|
altKey: e.altKey,
|
|
943
990
|
clientX,
|
|
944
|
-
clientY
|
|
991
|
+
clientY,
|
|
992
|
+
key: e.key
|
|
945
993
|
};
|
|
946
994
|
}
|
|
947
995
|
|
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;
|