@react-aria/interactions 3.26.0 → 3.27.1
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 +1 -0
- package/dist/focusSafely.main.js.map +1 -1
- package/dist/focusSafely.mjs +1 -0
- package/dist/focusSafely.module.js +1 -0
- package/dist/focusSafely.module.js.map +1 -1
- package/dist/main.js.map +1 -1
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/useFocus.main.js +4 -3
- package/dist/useFocus.main.js.map +1 -1
- package/dist/useFocus.mjs +5 -4
- package/dist/useFocus.module.js +5 -4
- package/dist/useFocus.module.js.map +1 -1
- package/dist/useFocusVisible.main.js +16 -13
- package/dist/useFocusVisible.main.js.map +1 -1
- package/dist/useFocusVisible.mjs +18 -15
- package/dist/useFocusVisible.module.js +18 -15
- package/dist/useFocusVisible.module.js.map +1 -1
- package/dist/useFocusWithin.main.js +9 -7
- package/dist/useFocusWithin.main.js.map +1 -1
- package/dist/useFocusWithin.mjs +10 -8
- package/dist/useFocusWithin.module.js +10 -8
- package/dist/useFocusWithin.module.js.map +1 -1
- package/dist/useHover.main.js +5 -5
- package/dist/useHover.main.js.map +1 -1
- package/dist/useHover.mjs +6 -6
- package/dist/useHover.module.js +6 -6
- package/dist/useHover.module.js.map +1 -1
- package/dist/useInteractOutside.main.js +5 -4
- package/dist/useInteractOutside.main.js.map +1 -1
- package/dist/useInteractOutside.mjs +6 -5
- package/dist/useInteractOutside.module.js +6 -5
- package/dist/useInteractOutside.module.js.map +1 -1
- package/dist/usePress.main.js +31 -23
- package/dist/usePress.main.js.map +1 -1
- package/dist/usePress.mjs +31 -23
- package/dist/usePress.module.js +31 -23
- package/dist/usePress.module.js.map +1 -1
- package/dist/utils.main.js +8 -7
- package/dist/utils.main.js.map +1 -1
- package/dist/utils.mjs +9 -8
- package/dist/utils.module.js +9 -8
- package/dist/utils.module.js.map +1 -1
- package/package.json +4 -4
- package/src/PressResponder.tsx +3 -4
- package/src/focusSafely.ts +4 -0
- package/src/index.ts +1 -1
- package/src/useFocus.ts +4 -3
- package/src/useFocusVisible.ts +18 -12
- package/src/useFocusWithin.ts +9 -7
- package/src/useHover.ts +6 -6
- package/src/useInteractOutside.ts +6 -5
- package/src/usePress.ts +46 -32
- package/src/utils.ts +12 -11
package/src/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type {HoverProps, HoverResult} from './useHover';
|
|
|
41
41
|
export type {InteractOutsideProps} from './useInteractOutside';
|
|
42
42
|
export type {KeyboardProps, KeyboardResult} from './useKeyboard';
|
|
43
43
|
export type {PressProps, PressHookProps, PressResult} from './usePress';
|
|
44
|
-
export type {PressEvent, PressEvents, MoveStartEvent, MoveMoveEvent, MoveEndEvent, MoveEvents, HoverEvent, HoverEvents, FocusEvents, KeyboardEvents} from '@react-types/shared';
|
|
44
|
+
export type {PressEvent, PressEvents, LongPressEvent, MoveStartEvent, MoveMoveEvent, MoveEndEvent, MoveEvent, MoveEvents, HoverEvent, HoverEvents, FocusEvents, KeyboardEvents} from '@react-types/shared';
|
|
45
45
|
export type {MoveResult} from './useMove';
|
|
46
46
|
export type {LongPressProps, LongPressResult} from './useLongPress';
|
|
47
47
|
export type {ScrollWheelProps} from './useScrollWheel';
|
package/src/useFocus.ts
CHANGED
|
@@ -43,7 +43,7 @@ export function useFocus<Target extends FocusableElement = FocusableElement>(pro
|
|
|
43
43
|
} = props;
|
|
44
44
|
|
|
45
45
|
const onBlur: FocusProps<Target>['onBlur'] = useCallback((e: FocusEvent<Target>) => {
|
|
46
|
-
if (e
|
|
46
|
+
if (getEventTarget(e) === e.currentTarget) {
|
|
47
47
|
if (onBlurProp) {
|
|
48
48
|
onBlurProp(e);
|
|
49
49
|
}
|
|
@@ -63,9 +63,10 @@ export function useFocus<Target extends FocusableElement = FocusableElement>(pro
|
|
|
63
63
|
// Double check that document.activeElement actually matches e.target in case a previously chained
|
|
64
64
|
// focus handler already moved focus somewhere else.
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
let eventTarget = getEventTarget(e);
|
|
67
|
+
const ownerDocument = getOwnerDocument(eventTarget);
|
|
67
68
|
const activeElement = ownerDocument ? getActiveElement(ownerDocument) : getActiveElement();
|
|
68
|
-
if (
|
|
69
|
+
if (eventTarget === e.currentTarget && eventTarget === activeElement) {
|
|
69
70
|
if (onFocusProp) {
|
|
70
71
|
onFocusProp(e);
|
|
71
72
|
}
|
package/src/useFocusVisible.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// NOTICE file in the root directory of this source tree.
|
|
16
16
|
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
|
|
17
17
|
|
|
18
|
-
import {getOwnerDocument, getOwnerWindow, isMac, isVirtualClick, openLink} from '@react-aria/utils';
|
|
18
|
+
import {getActiveElement, getEventTarget, getOwnerDocument, getOwnerWindow, isMac, isVirtualClick, openLink} from '@react-aria/utils';
|
|
19
19
|
import {ignoreFocusEvent} from './utils';
|
|
20
20
|
import {PointerType} from '@react-types/shared';
|
|
21
21
|
import {useEffect, useState} from 'react';
|
|
@@ -39,7 +39,7 @@ export interface FocusVisibleResult {
|
|
|
39
39
|
|
|
40
40
|
let currentModality: null | Modality = null;
|
|
41
41
|
let currentPointerType: PointerType = 'keyboard';
|
|
42
|
-
|
|
42
|
+
export const changeHandlers = new Set<Handler>();
|
|
43
43
|
interface GlobalListenerData {
|
|
44
44
|
focus: () => void
|
|
45
45
|
}
|
|
@@ -98,7 +98,7 @@ function handleFocusEvent(e: FocusEvent) {
|
|
|
98
98
|
// Firefox fires two extra focus events when the user first clicks into an iframe:
|
|
99
99
|
// first on the window, then on the document. We ignore these events so they don't
|
|
100
100
|
// cause keyboard focus rings to appear.
|
|
101
|
-
if (e
|
|
101
|
+
if (getEventTarget(e) === window || getEventTarget(e) === document || ignoreFocusEvent || !e.isTrusted) {
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -302,18 +302,20 @@ const nonTextInputTypes = new Set([
|
|
|
302
302
|
* focus visible style can be properly set.
|
|
303
303
|
*/
|
|
304
304
|
function isKeyboardFocusEvent(isTextInput: boolean, modality: Modality, e: HandlerEvent) {
|
|
305
|
-
let document = getOwnerDocument(e
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const
|
|
305
|
+
let document = getOwnerDocument(e ? getEventTarget(e) as Element : undefined);
|
|
306
|
+
let eventTarget = e ? getEventTarget(e) as Element : undefined;
|
|
307
|
+
const IHTMLInputElement = typeof window !== 'undefined' ? getOwnerWindow(eventTarget).HTMLInputElement : HTMLInputElement;
|
|
308
|
+
const IHTMLTextAreaElement = typeof window !== 'undefined' ? getOwnerWindow(eventTarget).HTMLTextAreaElement : HTMLTextAreaElement;
|
|
309
|
+
const IHTMLElement = typeof window !== 'undefined' ? getOwnerWindow(eventTarget).HTMLElement : HTMLElement;
|
|
310
|
+
const IKeyboardEvent = typeof window !== 'undefined' ? getOwnerWindow(eventTarget).KeyboardEvent : KeyboardEvent;
|
|
310
311
|
|
|
311
312
|
// For keyboard events that occur on a non-input element that will move focus into input element (aka ArrowLeft going from Datepicker button to the main input group)
|
|
312
313
|
// we need to rely on the user passing isTextInput into here. This way we can skip toggling focus visiblity for said input element
|
|
314
|
+
let activeElement = getActiveElement(document);
|
|
313
315
|
isTextInput = isTextInput ||
|
|
314
|
-
(
|
|
315
|
-
|
|
316
|
-
(
|
|
316
|
+
(activeElement instanceof IHTMLInputElement && !nonTextInputTypes.has(activeElement.type)) ||
|
|
317
|
+
activeElement instanceof IHTMLTextAreaElement ||
|
|
318
|
+
(activeElement instanceof IHTMLElement && activeElement.isContentEditable);
|
|
317
319
|
return !(isTextInput && modality === 'keyboard' && e instanceof IKeyboardEvent && !FOCUS_VISIBLE_INPUT_KEYS[e.key]);
|
|
318
320
|
}
|
|
319
321
|
|
|
@@ -333,10 +335,13 @@ export function useFocusVisible(props: FocusVisibleProps = {}): FocusVisibleResu
|
|
|
333
335
|
/**
|
|
334
336
|
* Listens for trigger change and reports if focus is visible (i.e., modality is not pointer).
|
|
335
337
|
*/
|
|
336
|
-
export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyArray<any>, opts?: {isTextInput?: boolean}): void {
|
|
338
|
+
export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyArray<any>, opts?: {enabled?: boolean, isTextInput?: boolean}): void {
|
|
337
339
|
setupGlobalFocusEvents();
|
|
338
340
|
|
|
339
341
|
useEffect(() => {
|
|
342
|
+
if (opts?.enabled === false) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
340
345
|
let handler = (modality: Modality, e: HandlerEvent) => {
|
|
341
346
|
// We want to early return for any keyboard events that occur inside text inputs EXCEPT for Tab and Escape
|
|
342
347
|
if (!isKeyboardFocusEvent(!!(opts?.isTextInput), modality, e)) {
|
|
@@ -351,3 +356,4 @@ export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyA
|
|
|
351
356
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
352
357
|
}, deps);
|
|
353
358
|
}
|
|
359
|
+
|
package/src/useFocusWithin.ts
CHANGED
|
@@ -54,14 +54,14 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
|
|
|
54
54
|
|
|
55
55
|
let onBlur = useCallback((e: FocusEvent) => {
|
|
56
56
|
// Ignore events bubbling through portals.
|
|
57
|
-
if (!e.currentTarget
|
|
57
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// We don't want to trigger onBlurWithin and then immediately onFocusWithin again
|
|
62
62
|
// when moving focus inside the element. Only trigger if the currentTarget doesn't
|
|
63
63
|
// include the relatedTarget (where focus is moving).
|
|
64
|
-
if (state.current.isFocusWithin && !(e.currentTarget as Element
|
|
64
|
+
if (state.current.isFocusWithin && !nodeContains(e.currentTarget as Element, e.relatedTarget as Element)) {
|
|
65
65
|
state.current.isFocusWithin = false;
|
|
66
66
|
removeAllGlobalListeners();
|
|
67
67
|
|
|
@@ -78,15 +78,16 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
|
|
|
78
78
|
let onSyntheticFocus = useSyntheticBlurEvent(onBlur);
|
|
79
79
|
let onFocus = useCallback((e: FocusEvent) => {
|
|
80
80
|
// Ignore events bubbling through portals.
|
|
81
|
-
if (!e.currentTarget
|
|
81
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Double check that document.activeElement actually matches e.target in case a previously chained
|
|
86
86
|
// focus handler already moved focus somewhere else.
|
|
87
|
-
|
|
87
|
+
let eventTarget = getEventTarget(e);
|
|
88
|
+
const ownerDocument = getOwnerDocument(eventTarget);
|
|
88
89
|
const activeElement = getActiveElement(ownerDocument);
|
|
89
|
-
if (!state.current.isFocusWithin && activeElement ===
|
|
90
|
+
if (!state.current.isFocusWithin && activeElement === eventTarget) {
|
|
90
91
|
if (onFocusWithin) {
|
|
91
92
|
onFocusWithin(e);
|
|
92
93
|
}
|
|
@@ -103,8 +104,9 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
|
|
|
103
104
|
// can manually fire onBlur.
|
|
104
105
|
let currentTarget = e.currentTarget;
|
|
105
106
|
addGlobalListener(ownerDocument, 'focus', e => {
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
let eventTarget = getEventTarget(e);
|
|
108
|
+
if (state.current.isFocusWithin && !nodeContains(currentTarget, eventTarget as Element)) {
|
|
109
|
+
let nativeEvent = new ownerDocument.defaultView!.FocusEvent('blur', {relatedTarget: eventTarget});
|
|
108
110
|
setEventTarget(nativeEvent, currentTarget);
|
|
109
111
|
let event = createSyntheticEvent<FocusEvent>(nativeEvent);
|
|
110
112
|
onBlur(event);
|
package/src/useHover.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
|
|
17
17
|
|
|
18
18
|
import {DOMAttributes, HoverEvents} from '@react-types/shared';
|
|
19
|
-
import {getOwnerDocument, nodeContains, useGlobalListeners} from '@react-aria/utils';
|
|
19
|
+
import {getEventTarget, getOwnerDocument, nodeContains, useGlobalListeners} from '@react-aria/utils';
|
|
20
20
|
import {useEffect, useMemo, useRef, useState} from 'react';
|
|
21
21
|
|
|
22
22
|
export interface HoverProps extends HoverEvents {
|
|
@@ -108,7 +108,7 @@ export function useHover(props: HoverProps): HoverResult {
|
|
|
108
108
|
let {hoverProps, triggerHoverEnd} = useMemo(() => {
|
|
109
109
|
let triggerHoverStart = (event, pointerType) => {
|
|
110
110
|
state.pointerType = pointerType;
|
|
111
|
-
if (isDisabled || pointerType === 'touch' || state.isHovered || !event.currentTarget
|
|
111
|
+
if (isDisabled || pointerType === 'touch' || state.isHovered || !nodeContains(event.currentTarget, getEventTarget(event) as Element)) {
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -120,8 +120,8 @@ export function useHover(props: HoverProps): HoverResult {
|
|
|
120
120
|
// even though the originally hovered target may have shrunk in size so it is no longer hovered.
|
|
121
121
|
// However, a pointerover event will be fired on the new target the mouse is over.
|
|
122
122
|
// In Chrome this happens immediately. In Safari and Firefox, it happens upon moving the mouse one pixel.
|
|
123
|
-
addGlobalListener(getOwnerDocument(event
|
|
124
|
-
if (state.isHovered && state.target && !nodeContains(state.target, e
|
|
123
|
+
addGlobalListener(getOwnerDocument(getEventTarget(event) as Element), 'pointerover', e => {
|
|
124
|
+
if (state.isHovered && state.target && !nodeContains(state.target, getEventTarget(e) as Element)) {
|
|
125
125
|
triggerHoverEnd(e, e.pointerType);
|
|
126
126
|
}
|
|
127
127
|
}, {capture: true});
|
|
@@ -180,7 +180,7 @@ export function useHover(props: HoverProps): HoverResult {
|
|
|
180
180
|
};
|
|
181
181
|
|
|
182
182
|
hoverProps.onPointerLeave = (e) => {
|
|
183
|
-
if (!isDisabled && e.currentTarget
|
|
183
|
+
if (!isDisabled && nodeContains(e.currentTarget, getEventTarget(e) as Element)) {
|
|
184
184
|
triggerHoverEnd(e, e.pointerType);
|
|
185
185
|
}
|
|
186
186
|
};
|
|
@@ -198,7 +198,7 @@ export function useHover(props: HoverProps): HoverResult {
|
|
|
198
198
|
};
|
|
199
199
|
|
|
200
200
|
hoverProps.onMouseLeave = (e) => {
|
|
201
|
-
if (!isDisabled && e.currentTarget
|
|
201
|
+
if (!isDisabled && nodeContains(e.currentTarget, getEventTarget(e) as Element)) {
|
|
202
202
|
triggerHoverEnd(e, 'mouse');
|
|
203
203
|
}
|
|
204
204
|
};
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// NOTICE file in the root directory of this source tree.
|
|
16
16
|
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
|
|
17
17
|
|
|
18
|
-
import {getOwnerDocument, useEffectEvent} from '@react-aria/utils';
|
|
18
|
+
import {getEventTarget, getOwnerDocument, nodeContains, useEffectEvent} from '@react-aria/utils';
|
|
19
19
|
import {RefObject} from '@react-types/shared';
|
|
20
20
|
import {useEffect, useRef} from 'react';
|
|
21
21
|
|
|
@@ -118,14 +118,15 @@ function isValidEvent(event, ref) {
|
|
|
118
118
|
if (event.button > 0) {
|
|
119
119
|
return false;
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
let target = getEventTarget(event) as Element;
|
|
122
|
+
if (target) {
|
|
122
123
|
// if the event target is no longer in the document, ignore
|
|
123
|
-
const ownerDocument =
|
|
124
|
-
if (!ownerDocument || !ownerDocument.documentElement
|
|
124
|
+
const ownerDocument = target.ownerDocument;
|
|
125
|
+
if (!ownerDocument || !nodeContains(ownerDocument.documentElement, target)) {
|
|
125
126
|
return false;
|
|
126
127
|
}
|
|
127
128
|
// If the target is within a top layer element (e.g. toasts), ignore.
|
|
128
|
-
if (
|
|
129
|
+
if (target.closest('[data-react-aria-top-layer]')) {
|
|
129
130
|
return false;
|
|
130
131
|
}
|
|
131
132
|
}
|
package/src/usePress.ts
CHANGED
|
@@ -72,7 +72,7 @@ interface PressState {
|
|
|
72
72
|
isOverTarget: boolean,
|
|
73
73
|
pointerType: PointerType | null,
|
|
74
74
|
userSelect?: string,
|
|
75
|
-
metaKeyEvents?: Map<string, KeyboardEvent>,
|
|
75
|
+
metaKeyEvents?: Map<string, globalThis.KeyboardEvent>,
|
|
76
76
|
disposables: Array<() => void>
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -84,7 +84,8 @@ interface EventBase {
|
|
|
84
84
|
altKey: boolean,
|
|
85
85
|
clientX?: number,
|
|
86
86
|
clientY?: number,
|
|
87
|
-
targetTouches?: Array<{clientX?: number, clientY?: number}
|
|
87
|
+
targetTouches?: Array<{clientX?: number, clientY?: number}>,
|
|
88
|
+
key?: string
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
export interface PressResult {
|
|
@@ -98,7 +99,9 @@ function usePressResponderContext(props: PressHookProps): PressHookProps {
|
|
|
98
99
|
// Consume context from <PressResponder> and merge with props.
|
|
99
100
|
let context = useContext(PressResponderContext);
|
|
100
101
|
if (context) {
|
|
101
|
-
|
|
102
|
+
// Prevent mergeProps from merging ref.
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
104
|
+
let {register, ref, ...contextProps} = context;
|
|
102
105
|
props = mergeProps(contextProps, props) as PressHookProps;
|
|
103
106
|
register();
|
|
104
107
|
}
|
|
@@ -117,6 +120,7 @@ class PressEvent implements IPressEvent {
|
|
|
117
120
|
altKey: boolean;
|
|
118
121
|
x: number;
|
|
119
122
|
y: number;
|
|
123
|
+
key: string | undefined;
|
|
120
124
|
#shouldStopPropagation = true;
|
|
121
125
|
|
|
122
126
|
constructor(type: IPressEvent['type'], pointerType: PointerType, originalEvent: EventBase, state?: PressState) {
|
|
@@ -146,6 +150,7 @@ class PressEvent implements IPressEvent {
|
|
|
146
150
|
this.altKey = originalEvent.altKey;
|
|
147
151
|
this.x = x;
|
|
148
152
|
this.y = y;
|
|
153
|
+
this.key = originalEvent.key;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
continuePropagation() {
|
|
@@ -334,12 +339,12 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
334
339
|
if (isElemKeyPressed) {
|
|
335
340
|
let onKeyUp = (e: KeyboardEvent) => {
|
|
336
341
|
if (state.isPressed && state.target && isValidKeyboardEvent(e, state.target)) {
|
|
337
|
-
if (shouldPreventDefaultKeyboard(getEventTarget(e), e.key)) {
|
|
342
|
+
if (shouldPreventDefaultKeyboard(getEventTarget(e) as Element, e.key)) {
|
|
338
343
|
e.preventDefault();
|
|
339
344
|
}
|
|
340
345
|
|
|
341
|
-
let target = getEventTarget(e);
|
|
342
|
-
let wasPressed = nodeContains(state.target,
|
|
346
|
+
let target = getEventTarget(e) as Element;
|
|
347
|
+
let wasPressed = nodeContains(state.target, target);
|
|
343
348
|
triggerPressEndEvent(createEvent(state.target, e), 'keyboard', wasPressed);
|
|
344
349
|
if (wasPressed) {
|
|
345
350
|
triggerSyntheticClickEvent(e, state.target);
|
|
@@ -374,8 +379,8 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
374
379
|
// instead of the same element where the key down event occurred. Make it capturing so that it will trigger
|
|
375
380
|
// before stopPropagation from useKeyboard on a child element may happen and thus we can still call triggerPress for the parent element.
|
|
376
381
|
let originalTarget = state.target;
|
|
377
|
-
let pressUp = (e) => {
|
|
378
|
-
if (originalTarget && isValidKeyboardEvent(e, originalTarget) && !e.repeat && nodeContains(originalTarget, getEventTarget(e)) && state.target) {
|
|
382
|
+
let pressUp = (e: KeyboardEvent) => {
|
|
383
|
+
if (originalTarget && isValidKeyboardEvent(e, originalTarget) && !e.repeat && nodeContains(originalTarget, getEventTarget(e) as Element) && state.target) {
|
|
379
384
|
triggerPressUpEvent(createEvent(state.target, e), 'keyboard');
|
|
380
385
|
}
|
|
381
386
|
};
|
|
@@ -393,7 +398,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
393
398
|
if (isPointerPressed === 'pointer') {
|
|
394
399
|
let onPointerUp = (e: PointerEvent) => {
|
|
395
400
|
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
|
|
396
|
-
if (nodeContains(state.target, getEventTarget(e)) && state.pointerType != null) {
|
|
401
|
+
if (nodeContains(state.target, getEventTarget(e) as Element) && state.pointerType != null) {
|
|
397
402
|
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
398
403
|
// is mutated between onPointerUp and onClick, and is more compatible with third party libraries.
|
|
399
404
|
// https://github.com/adobe/react-spectrum/issues/1513
|
|
@@ -417,7 +422,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
417
422
|
}, 80);
|
|
418
423
|
// Use a capturing listener to track if a click occurred.
|
|
419
424
|
// If stopPropagation is called it may never reach our handler.
|
|
420
|
-
|
|
425
|
+
if (e.currentTarget) {
|
|
426
|
+
addGlobalListener(e.currentTarget, 'click', () => clicked = true, true);
|
|
427
|
+
}
|
|
421
428
|
state.disposables.push(() => clearTimeout(timeout));
|
|
422
429
|
} else {
|
|
423
430
|
cancelEvent(e);
|
|
@@ -450,7 +457,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
450
457
|
return;
|
|
451
458
|
}
|
|
452
459
|
|
|
453
|
-
if (state.target && state.target
|
|
460
|
+
if (state.target && nodeContains(state.target, e.target as Element) && state.pointerType != null) {
|
|
454
461
|
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
455
462
|
// is mutated between onMouseUp and onClick, and is more compatible with third party libraries.
|
|
456
463
|
} else {
|
|
@@ -466,7 +473,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
466
473
|
};
|
|
467
474
|
} else if (isPointerPressed === 'touch' && process.env.NODE_ENV === 'test') {
|
|
468
475
|
let onScroll = (e: Event) => {
|
|
469
|
-
if (state.isPressed && nodeContains(getEventTarget(e), state.target)) {
|
|
476
|
+
if (state.isPressed && nodeContains(getEventTarget(e) as Element, state.target)) {
|
|
470
477
|
cancelEvent({
|
|
471
478
|
currentTarget: state.target,
|
|
472
479
|
shiftKey: false,
|
|
@@ -488,8 +495,8 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
488
495
|
let state = ref.current;
|
|
489
496
|
let pressProps: DOMAttributes = {
|
|
490
497
|
onKeyDown(e) {
|
|
491
|
-
if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && nodeContains(e.currentTarget, getEventTarget(e
|
|
492
|
-
if (shouldPreventDefaultKeyboard(getEventTarget(e
|
|
498
|
+
if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget as Element) && nodeContains(e.currentTarget as Element, getEventTarget(e) as Element)) {
|
|
499
|
+
if (shouldPreventDefaultKeyboard(getEventTarget(e) as Element, e.key)) {
|
|
493
500
|
e.preventDefault();
|
|
494
501
|
}
|
|
495
502
|
|
|
@@ -524,7 +531,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
524
531
|
}
|
|
525
532
|
},
|
|
526
533
|
onClick(e) {
|
|
527
|
-
if (e && !nodeContains(e.currentTarget, getEventTarget(e
|
|
534
|
+
if (e && !nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
528
535
|
return;
|
|
529
536
|
}
|
|
530
537
|
|
|
@@ -563,7 +570,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
563
570
|
if (typeof PointerEvent !== 'undefined') {
|
|
564
571
|
pressProps.onPointerDown = (e) => {
|
|
565
572
|
// Only handle left clicks, and ignore events that bubbled through portals.
|
|
566
|
-
if (e.button !== 0 || !nodeContains(e.currentTarget, getEventTarget(e
|
|
573
|
+
if (e.button !== 0 || !nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
567
574
|
return;
|
|
568
575
|
}
|
|
569
576
|
|
|
@@ -594,9 +601,15 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
594
601
|
|
|
595
602
|
// Release pointer capture so that touch interactions can leave the original target.
|
|
596
603
|
// This enables onPointerLeave and onPointerEnter to fire.
|
|
597
|
-
let target = getEventTarget(e
|
|
604
|
+
let target = getEventTarget(e);
|
|
598
605
|
if ('releasePointerCapture' in target) {
|
|
599
|
-
target
|
|
606
|
+
if ('hasPointerCapture' in target) {
|
|
607
|
+
if (target.hasPointerCapture(e.pointerId)) {
|
|
608
|
+
target.releasePointerCapture(e.pointerId);
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
(target as Element).releasePointerCapture(e.pointerId);
|
|
612
|
+
}
|
|
600
613
|
}
|
|
601
614
|
}
|
|
602
615
|
|
|
@@ -606,7 +619,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
606
619
|
};
|
|
607
620
|
|
|
608
621
|
pressProps.onMouseDown = (e) => {
|
|
609
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
622
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
610
623
|
return;
|
|
611
624
|
}
|
|
612
625
|
|
|
@@ -624,7 +637,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
624
637
|
|
|
625
638
|
pressProps.onPointerUp = (e) => {
|
|
626
639
|
// iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
|
|
627
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
640
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e)) || state.pointerType === 'virtual') {
|
|
628
641
|
return;
|
|
629
642
|
}
|
|
630
643
|
|
|
@@ -651,7 +664,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
651
664
|
|
|
652
665
|
|
|
653
666
|
pressProps.onDragStart = (e) => {
|
|
654
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
667
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
655
668
|
return;
|
|
656
669
|
}
|
|
657
670
|
|
|
@@ -664,7 +677,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
664
677
|
|
|
665
678
|
pressProps.onMouseDown = (e) => {
|
|
666
679
|
// Only handle left clicks
|
|
667
|
-
if (e.button !== 0 || !nodeContains(e.currentTarget, getEventTarget(e
|
|
680
|
+
if (e.button !== 0 || !nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
668
681
|
return;
|
|
669
682
|
}
|
|
670
683
|
|
|
@@ -694,7 +707,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
694
707
|
};
|
|
695
708
|
|
|
696
709
|
pressProps.onMouseEnter = (e) => {
|
|
697
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
710
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
698
711
|
return;
|
|
699
712
|
}
|
|
700
713
|
|
|
@@ -710,7 +723,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
710
723
|
};
|
|
711
724
|
|
|
712
725
|
pressProps.onMouseLeave = (e) => {
|
|
713
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
726
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
714
727
|
return;
|
|
715
728
|
}
|
|
716
729
|
|
|
@@ -727,7 +740,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
727
740
|
};
|
|
728
741
|
|
|
729
742
|
pressProps.onMouseUp = (e) => {
|
|
730
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
743
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
731
744
|
return;
|
|
732
745
|
}
|
|
733
746
|
|
|
@@ -737,7 +750,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
737
750
|
};
|
|
738
751
|
|
|
739
752
|
pressProps.onTouchStart = (e) => {
|
|
740
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
753
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
741
754
|
return;
|
|
742
755
|
}
|
|
743
756
|
|
|
@@ -764,7 +777,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
764
777
|
};
|
|
765
778
|
|
|
766
779
|
pressProps.onTouchMove = (e) => {
|
|
767
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
780
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
768
781
|
return;
|
|
769
782
|
}
|
|
770
783
|
|
|
@@ -792,7 +805,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
792
805
|
};
|
|
793
806
|
|
|
794
807
|
pressProps.onTouchEnd = (e) => {
|
|
795
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
808
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
796
809
|
return;
|
|
797
810
|
}
|
|
798
811
|
|
|
@@ -827,7 +840,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
827
840
|
};
|
|
828
841
|
|
|
829
842
|
pressProps.onTouchCancel = (e) => {
|
|
830
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
843
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
831
844
|
return;
|
|
832
845
|
}
|
|
833
846
|
|
|
@@ -838,7 +851,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
838
851
|
};
|
|
839
852
|
|
|
840
853
|
pressProps.onDragStart = (e) => {
|
|
841
|
-
if (!nodeContains(e.currentTarget, getEventTarget(e
|
|
854
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
842
855
|
return;
|
|
843
856
|
}
|
|
844
857
|
|
|
@@ -911,7 +924,7 @@ function isHTMLAnchorLink(target: Element): target is HTMLAnchorElement {
|
|
|
911
924
|
return target.tagName === 'A' && target.hasAttribute('href');
|
|
912
925
|
}
|
|
913
926
|
|
|
914
|
-
function isValidKeyboardEvent(event: KeyboardEvent, currentTarget: Element): boolean {
|
|
927
|
+
function isValidKeyboardEvent(event: KeyboardEvent | globalThis.KeyboardEvent, currentTarget: Element): boolean {
|
|
915
928
|
const {key, code} = event;
|
|
916
929
|
const element = currentTarget as HTMLElement;
|
|
917
930
|
const role = element.getAttribute('role');
|
|
@@ -977,7 +990,8 @@ function createEvent(target: FocusableElement, e: EventBase): EventBase {
|
|
|
977
990
|
metaKey: e.metaKey,
|
|
978
991
|
altKey: e.altKey,
|
|
979
992
|
clientX,
|
|
980
|
-
clientY
|
|
993
|
+
clientY,
|
|
994
|
+
key: e.key
|
|
981
995
|
};
|
|
982
996
|
}
|
|
983
997
|
|
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, useLayoutEffect} from '@react-aria/utils';
|
|
14
|
+
import {focusWithoutScrolling, getActiveElement, getEventTarget, 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.
|
|
@@ -54,15 +54,16 @@ export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur:
|
|
|
54
54
|
// Most browsers fire a native focusout event in this case, except for Firefox. In that case, we use a
|
|
55
55
|
// MutationObserver to watch for the disabled attribute, and dispatch these events ourselves.
|
|
56
56
|
// For browsers that do, focusout fires before the MutationObserver, so onBlur should not fire twice.
|
|
57
|
+
let eventTarget = getEventTarget(e);
|
|
57
58
|
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
eventTarget instanceof HTMLButtonElement ||
|
|
60
|
+
eventTarget instanceof HTMLInputElement ||
|
|
61
|
+
eventTarget instanceof HTMLTextAreaElement ||
|
|
62
|
+
eventTarget instanceof HTMLSelectElement
|
|
62
63
|
) {
|
|
63
64
|
stateRef.current.isFocused = true;
|
|
64
65
|
|
|
65
|
-
let target =
|
|
66
|
+
let target = eventTarget;
|
|
66
67
|
let onBlurHandler: EventListenerOrEventListenerObject | null = (e) => {
|
|
67
68
|
stateRef.current.isFocused = false;
|
|
68
69
|
|
|
@@ -84,7 +85,7 @@ export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur:
|
|
|
84
85
|
stateRef.current.observer = new MutationObserver(() => {
|
|
85
86
|
if (stateRef.current.isFocused && target.disabled) {
|
|
86
87
|
stateRef.current.observer?.disconnect();
|
|
87
|
-
let relatedTargetEl = target ===
|
|
88
|
+
let relatedTargetEl = target === getActiveElement() ? null : getActiveElement();
|
|
88
89
|
target.dispatchEvent(new FocusEvent('blur', {relatedTarget: relatedTargetEl}));
|
|
89
90
|
target.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget: relatedTargetEl}));
|
|
90
91
|
}
|
|
@@ -117,13 +118,13 @@ export function preventFocus(target: FocusableElement | null): (() => void) | un
|
|
|
117
118
|
ignoreFocusEvent = true;
|
|
118
119
|
let isRefocusing = false;
|
|
119
120
|
let onBlur = (e: FocusEvent) => {
|
|
120
|
-
if (e
|
|
121
|
+
if (getEventTarget(e) === activeElement || isRefocusing) {
|
|
121
122
|
e.stopImmediatePropagation();
|
|
122
123
|
}
|
|
123
124
|
};
|
|
124
125
|
|
|
125
126
|
let onFocusOut = (e: FocusEvent) => {
|
|
126
|
-
if (e
|
|
127
|
+
if (getEventTarget(e) === activeElement || isRefocusing) {
|
|
127
128
|
e.stopImmediatePropagation();
|
|
128
129
|
|
|
129
130
|
// If there was no focusable ancestor, we don't expect a focus event.
|
|
@@ -137,13 +138,13 @@ export function preventFocus(target: FocusableElement | null): (() => void) | un
|
|
|
137
138
|
};
|
|
138
139
|
|
|
139
140
|
let onFocus = (e: FocusEvent) => {
|
|
140
|
-
if (e
|
|
141
|
+
if (getEventTarget(e) === target || isRefocusing) {
|
|
141
142
|
e.stopImmediatePropagation();
|
|
142
143
|
}
|
|
143
144
|
};
|
|
144
145
|
|
|
145
146
|
let onFocusIn = (e: FocusEvent) => {
|
|
146
|
-
if (e
|
|
147
|
+
if (getEventTarget(e) === target || isRefocusing) {
|
|
147
148
|
e.stopImmediatePropagation();
|
|
148
149
|
|
|
149
150
|
if (!isRefocusing) {
|