@react-aria/interactions
Advanced tools
Comparing version 3.0.0-nightly-c904e066c-240917 to 3.0.0-nightly-cdba74876-250117
@@ -30,6 +30,10 @@ | ||
stopPropagation () { | ||
console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.'); | ||
if (shouldStopPropagation) console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.'); | ||
else shouldStopPropagation = true; | ||
}, | ||
continuePropagation () { | ||
shouldStopPropagation = false; | ||
}, | ||
isPropagationStopped () { | ||
return shouldStopPropagation; | ||
} | ||
@@ -36,0 +40,0 @@ }; |
@@ -24,6 +24,10 @@ /* | ||
stopPropagation () { | ||
console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.'); | ||
if (shouldStopPropagation) console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.'); | ||
else shouldStopPropagation = true; | ||
}, | ||
continuePropagation () { | ||
shouldStopPropagation = false; | ||
}, | ||
isPropagationStopped () { | ||
return shouldStopPropagation; | ||
} | ||
@@ -30,0 +34,0 @@ }; |
@@ -29,3 +29,2 @@ var $20aJV$reactariautils = require("@react-aria/utils"); | ||
if ($f7e14e656343df57$var$state === 'default') { | ||
// eslint-disable-next-line no-restricted-globals | ||
const documentObject = (0, $20aJV$reactariautils.getOwnerDocument)(target); | ||
@@ -57,3 +56,2 @@ $f7e14e656343df57$var$savedUserSelect = documentObject.documentElement.style.webkitUserSelect; | ||
if ($f7e14e656343df57$var$state === 'restoring') { | ||
// eslint-disable-next-line no-restricted-globals | ||
const documentObject = (0, $20aJV$reactariautils.getOwnerDocument)(target); | ||
@@ -60,0 +58,0 @@ if (documentObject.documentElement.style.webkitUserSelect === 'none') documentObject.documentElement.style.webkitUserSelect = $f7e14e656343df57$var$savedUserSelect || ''; |
@@ -22,3 +22,2 @@ import {isIOS as $7R18e$isIOS, getOwnerDocument as $7R18e$getOwnerDocument, runAfterTransition as $7R18e$runAfterTransition} from "@react-aria/utils"; | ||
if ($14c0b72509d70225$var$state === 'default') { | ||
// eslint-disable-next-line no-restricted-globals | ||
const documentObject = (0, $7R18e$getOwnerDocument)(target); | ||
@@ -50,3 +49,2 @@ $14c0b72509d70225$var$savedUserSelect = documentObject.documentElement.style.webkitUserSelect; | ||
if ($14c0b72509d70225$var$state === 'restoring') { | ||
// eslint-disable-next-line no-restricted-globals | ||
const documentObject = (0, $7R18e$getOwnerDocument)(target); | ||
@@ -53,0 +51,0 @@ if (documentObject.documentElement.style.webkitUserSelect === 'none') documentObject.documentElement.style.webkitUserSelect = $14c0b72509d70225$var$savedUserSelect || ''; |
@@ -0,1 +1,2 @@ | ||
var $625cf83917e112ad$exports = require("./utils.main.js"); | ||
var $cR3F8$reactariautils = require("@react-aria/utils"); | ||
@@ -34,2 +35,3 @@ var $cR3F8$react = require("react"); | ||
let $e77252a287ef94ab$var$currentModality = null; | ||
@@ -78,3 +80,3 @@ let $e77252a287ef94ab$var$changeHandlers = new Set(); | ||
// cause keyboard focus rings to appear. | ||
if (e.target === window || e.target === document) return; | ||
if (e.target === window || e.target === document || (0, $625cf83917e112ad$exports.ignoreFocusEvent)) return; | ||
// If a focus event occurs without a preceding keyboard or pointer event, switch to virtual modality. | ||
@@ -90,2 +92,3 @@ // This occurs, for example, when navigating a form with the next/previous buttons on iOS. | ||
function $e77252a287ef94ab$var$handleWindowBlur() { | ||
if (0, $625cf83917e112ad$exports.ignoreFocusEvent) return; | ||
// When the window is blurred, reset state. This is necessary when tabbing out of the window, | ||
@@ -92,0 +95,0 @@ // for example, since a subsequent focus event won't be fired. |
@@ -0,1 +1,2 @@ | ||
import {ignoreFocusEvent as $8a9cb279dc87e130$export$fda7da73ab5d4c48} from "./utils.module.js"; | ||
import {isMac as $28AnR$isMac, isVirtualClick as $28AnR$isVirtualClick, getOwnerWindow as $28AnR$getOwnerWindow, getOwnerDocument as $28AnR$getOwnerDocument} from "@react-aria/utils"; | ||
@@ -22,2 +23,3 @@ import {useState as $28AnR$useState, useEffect as $28AnR$useEffect} from "react"; | ||
let $507fabe10e71c6fb$var$currentModality = null; | ||
@@ -66,3 +68,3 @@ let $507fabe10e71c6fb$var$changeHandlers = new Set(); | ||
// cause keyboard focus rings to appear. | ||
if (e.target === window || e.target === document) return; | ||
if (e.target === window || e.target === document || (0, $8a9cb279dc87e130$export$fda7da73ab5d4c48)) return; | ||
// If a focus event occurs without a preceding keyboard or pointer event, switch to virtual modality. | ||
@@ -78,2 +80,3 @@ // This occurs, for example, when navigating a form with the next/previous buttons on iOS. | ||
function $507fabe10e71c6fb$var$handleWindowBlur() { | ||
if (0, $8a9cb279dc87e130$export$fda7da73ab5d4c48) return; | ||
// When the window is blurred, reset state. This is necessary when tabbing out of the window, | ||
@@ -80,0 +83,0 @@ // for example, since a subsequent focus event won't be fired. |
@@ -43,2 +43,4 @@ var $0294ea432cd92340$exports = require("./usePress.main.js"); | ||
})); | ||
// Ensure target is focused. On touch devices, browsers typically focus on pointer up. | ||
if ((0, $5sxTM$reactariautils.getOwnerDocument)(e.target).activeElement !== e.target) (0, $5sxTM$reactariautils.focusWithoutScrolling)(e.target); | ||
if (onLongPress) onLongPress({ | ||
@@ -45,0 +47,0 @@ ...e, |
import {usePress as $f6c31cce2adf654f$export$45712eceda6fad21} from "./usePress.module.js"; | ||
import {useGlobalListeners as $4k2kv$useGlobalListeners, useDescription as $4k2kv$useDescription, mergeProps as $4k2kv$mergeProps} from "@react-aria/utils"; | ||
import {useGlobalListeners as $4k2kv$useGlobalListeners, getOwnerDocument as $4k2kv$getOwnerDocument, focusWithoutScrolling as $4k2kv$focusWithoutScrolling, useDescription as $4k2kv$useDescription, mergeProps as $4k2kv$mergeProps} from "@react-aria/utils"; | ||
import {useRef as $4k2kv$useRef} from "react"; | ||
@@ -37,2 +37,4 @@ | ||
})); | ||
// Ensure target is focused. On touch devices, browsers typically focus on pointer up. | ||
if ((0, $4k2kv$getOwnerDocument)(e.target).activeElement !== e.target) (0, $4k2kv$focusWithoutScrolling)(e.target); | ||
if (onLongPress) onLongPress({ | ||
@@ -39,0 +41,0 @@ ...e, |
var $f7e14e656343df57$exports = require("./textSelection.main.js"); | ||
var $01d3f539e91688c8$exports = require("./context.main.js"); | ||
var $625cf83917e112ad$exports = require("./utils.main.js"); | ||
var $bBqCQ$swchelperscjs_class_private_field_getcjs = require("@swc/helpers/cjs/_class_private_field_get.cjs"); | ||
@@ -7,2 +8,3 @@ var $bBqCQ$swchelperscjs_class_private_field_initcjs = require("@swc/helpers/cjs/_class_private_field_init.cjs"); | ||
var $bBqCQ$reactariautils = require("@react-aria/utils"); | ||
var $bBqCQ$reactdom = require("react-dom"); | ||
var $bBqCQ$react = require("react"); | ||
@@ -37,2 +39,4 @@ | ||
function $0294ea432cd92340$var$usePressResponderContext(props) { | ||
@@ -100,3 +104,2 @@ // Consume context from <PressResponder> and merge with props. | ||
ignoreEmulatedMouseEvents: false, | ||
ignoreClickAfterPress: false, | ||
didFirePressStart: false, | ||
@@ -107,3 +110,4 @@ isTriggeringEvent: false, | ||
isOverTarget: false, | ||
pointerType: null | ||
pointerType: null, | ||
disposables: [] | ||
}); | ||
@@ -130,3 +134,2 @@ let { addGlobalListener: addGlobalListener, removeAllGlobalListeners: removeAllGlobalListeners } = (0, $bBqCQ$reactariautils.useGlobalListeners)(); | ||
if (!state.didFirePressStart) return false; | ||
state.ignoreClickAfterPress = true; | ||
state.didFirePressStart = false; | ||
@@ -165,3 +168,3 @@ state.isTriggeringEvent = true; | ||
if (state.isPressed && state.target) { | ||
if (state.isOverTarget && state.pointerType != null) triggerPressEnd($0294ea432cd92340$var$createEvent(state.target, e), state.pointerType, false); | ||
if (state.didFirePressStart && state.pointerType != null) triggerPressEnd($0294ea432cd92340$var$createEvent(state.target, e), state.pointerType, false); | ||
state.isPressed = false; | ||
@@ -173,2 +176,4 @@ state.isOverTarget = false; | ||
if (!allowTextSelectionOnPress) (0, $f7e14e656343df57$exports.restoreTextSelection)(state.target); | ||
for (let dispose of state.disposables)dispose(); | ||
state.disposables = []; | ||
} | ||
@@ -193,2 +198,3 @@ }); | ||
state.isPressed = true; | ||
state.pointerType = 'keyboard'; | ||
shouldStopPropagation = triggerPressStart(e, 'keyboard'); | ||
@@ -222,5 +228,3 @@ // Focus may move before the key up event, so register the event on the document | ||
// trigger as if it were a keyboard click. | ||
if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || (0, $bBqCQ$reactariautils.isVirtualClick)(e.nativeEvent))) { | ||
// Ensure the element receives focus (VoiceOver on iOS does not do this) | ||
if (!isDisabled && !preventFocusOnPress) (0, $bBqCQ$reactariautils.focusWithoutScrolling)(e.currentTarget); | ||
if (!state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || (0, $bBqCQ$reactariautils.isVirtualClick)(e.nativeEvent))) { | ||
let stopPressStart = triggerPressStart(e, 'virtual'); | ||
@@ -230,5 +234,9 @@ let stopPressUp = triggerPressUp(e, 'virtual'); | ||
shouldStopPropagation = stopPressStart && stopPressUp && stopPressEnd; | ||
} else if (state.isPressed && state.pointerType !== 'keyboard') { | ||
let pointerType = state.pointerType || e.nativeEvent.pointerType || 'virtual'; | ||
shouldStopPropagation = triggerPressEnd($0294ea432cd92340$var$createEvent(e.currentTarget, e), pointerType, true); | ||
state.isOverTarget = false; | ||
cancel(e); | ||
} | ||
state.ignoreEmulatedMouseEvents = false; | ||
state.ignoreClickAfterPress = false; | ||
if (shouldStopPropagation) e.stopPropagation(); | ||
@@ -279,5 +287,2 @@ } | ||
} | ||
// Due to browser inconsistencies, especially on mobile browsers, we prevent | ||
// default on pointer down and handle focusing the pressable element ourselves. | ||
if ($0294ea432cd92340$var$shouldPreventDefaultDown(e.currentTarget)) e.preventDefault(); | ||
state.pointerType = e.pointerType; | ||
@@ -290,6 +295,8 @@ let shouldStopPropagation = true; | ||
state.target = e.currentTarget; | ||
if (!isDisabled && !preventFocusOnPress) (0, $bBqCQ$reactariautils.focusWithoutScrolling)(e.currentTarget); | ||
if (!allowTextSelectionOnPress) (0, $f7e14e656343df57$exports.disableTextSelection)(state.target); | ||
shouldStopPropagation = triggerPressStart(e, state.pointerType); | ||
addGlobalListener((0, $bBqCQ$reactariautils.getOwnerDocument)(e.currentTarget), 'pointermove', onPointerMove, false); | ||
// Release pointer capture so that touch interactions can leave the original target. | ||
// This enables onPointerLeave and onPointerEnter to fire. | ||
let target = e.target; | ||
if ('releasePointerCapture' in target) target.releasePointerCapture(e.pointerId); | ||
addGlobalListener((0, $bBqCQ$reactariautils.getOwnerDocument)(e.currentTarget), 'pointerup', onPointerUp, false); | ||
@@ -303,6 +310,6 @@ addGlobalListener((0, $bBqCQ$reactariautils.getOwnerDocument)(e.currentTarget), 'pointercancel', onPointerCancel, false); | ||
if (e.button === 0) { | ||
// Chrome and Firefox on touch Windows devices require mouse down events | ||
// to be canceled in addition to pointer events, or an extra asynchronous | ||
// focus event will be fired. | ||
if ($0294ea432cd92340$var$shouldPreventDefaultDown(e.currentTarget)) e.preventDefault(); | ||
if (preventFocusOnPress) { | ||
let dispose = (0, $625cf83917e112ad$exports.preventFocus)(e.target); | ||
if (dispose) state.disposables.push(dispose); | ||
} | ||
e.stopPropagation(); | ||
@@ -315,17 +322,12 @@ } | ||
// Only handle left clicks | ||
// Safari on iOS sometimes fires pointerup events, even | ||
// when the touch isn't over the target, so double check. | ||
if (e.button === 0 && $0294ea432cd92340$var$isOverTarget(e, e.currentTarget)) triggerPressUp(e, state.pointerType || e.pointerType); | ||
if (e.button === 0) triggerPressUp(e, state.pointerType || e.pointerType); | ||
}; | ||
// Safari on iOS < 13.2 does not implement pointerenter/pointerleave events correctly. | ||
// Use pointer move events instead to implement our own hit testing. | ||
// See https://bugs.webkit.org/show_bug.cgi?id=199803 | ||
let onPointerMove = (e)=>{ | ||
if (e.pointerId !== state.activePointerId) return; | ||
if (state.target && $0294ea432cd92340$var$isOverTarget(e, state.target)) { | ||
if (!state.isOverTarget && state.pointerType != null) { | ||
state.isOverTarget = true; | ||
triggerPressStart($0294ea432cd92340$var$createEvent(state.target, e), state.pointerType); | ||
} | ||
} else if (state.target && state.isOverTarget && state.pointerType != null) { | ||
pressProps.onPointerEnter = (e)=>{ | ||
if (e.pointerId === state.activePointerId && state.target && !state.isOverTarget && state.pointerType != null) { | ||
state.isOverTarget = true; | ||
triggerPressStart($0294ea432cd92340$var$createEvent(state.target, e), state.pointerType); | ||
} | ||
}; | ||
pressProps.onPointerLeave = (e)=>{ | ||
if (e.pointerId === state.activePointerId && state.target && state.isOverTarget && state.pointerType != null) { | ||
state.isOverTarget = false; | ||
@@ -338,27 +340,30 @@ triggerPressEnd($0294ea432cd92340$var$createEvent(state.target, e), state.pointerType, false); | ||
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) { | ||
if ($0294ea432cd92340$var$isOverTarget(e, state.target) && state.pointerType != null) triggerPressEnd($0294ea432cd92340$var$createEvent(state.target, e), state.pointerType); | ||
else if (state.isOverTarget && state.pointerType != null) triggerPressEnd($0294ea432cd92340$var$createEvent(state.target, e), state.pointerType, false); | ||
state.isPressed = false; | ||
if (state.target.contains(e.target) && state.pointerType != null) { | ||
// Wait for onClick to fire onPress. This avoids browser issues when the DOM | ||
// is mutated between onPointerUp and onClick, and is more compatible with third party libraries. | ||
// https://github.com/adobe/react-spectrum/issues/1513 | ||
// https://issues.chromium.org/issues/40732224 | ||
// However, iOS and Android do not focus or fire onClick after a long press. | ||
// We work around this by triggering a click ourselves after a timeout. | ||
// This timeout is canceled during the click event in case the real one fires first. | ||
// In testing, a 0ms delay is too short. 5ms seems long enough for the browser to fire the real events. | ||
let clicked = false; | ||
let timeout = setTimeout(()=>{ | ||
if (state.isPressed && state.target instanceof HTMLElement) { | ||
if (clicked) cancel(e); | ||
else { | ||
(0, $bBqCQ$reactariautils.focusWithoutScrolling)(state.target); | ||
state.target.click(); | ||
} | ||
} | ||
}, 5); | ||
// Use a capturing listener to track if a click occurred. | ||
// If stopPropagation is called it may never reach our handler. | ||
addGlobalListener(e.currentTarget, 'click', ()=>clicked = true, true); | ||
state.disposables.push(()=>clearTimeout(timeout)); | ||
} else cancel(e); | ||
// Ignore subsequent onPointerLeave event before onClick on touch devices. | ||
state.isOverTarget = false; | ||
state.activePointerId = null; | ||
state.pointerType = null; | ||
removeAllGlobalListeners(); | ||
if (!allowTextSelectionOnPress) (0, $f7e14e656343df57$exports.restoreTextSelection)(state.target); | ||
// Prevent subsequent touchend event from triggering onClick on unrelated elements on Android. See below. | ||
// Both 'touch' and 'pen' pointerTypes trigger onTouchEnd, but 'mouse' does not. | ||
if ('ontouchend' in state.target && e.pointerType !== 'mouse') addGlobalListener(state.target, 'touchend', onTouchEnd, { | ||
once: true | ||
}); | ||
} | ||
}; | ||
// This is a workaround for an Android Chrome/Firefox issue where click events are fired on an incorrect element | ||
// if the original target is removed during onPointerUp (before onClick). | ||
// https://github.com/adobe/react-spectrum/issues/1513 | ||
// https://issues.chromium.org/issues/40732224 | ||
// Note: this event must be registered directly on the element, not via React props in order to work. | ||
// https://github.com/facebook/react/issues/9809 | ||
let onTouchEnd = (e)=>{ | ||
// Don't preventDefault if we actually want the default (e.g. submit/link click). | ||
if ($0294ea432cd92340$var$shouldPreventDefaultUp(e.target)) e.preventDefault(); | ||
}; | ||
let onPointerCancel = (e)=>{ | ||
@@ -373,8 +378,7 @@ cancel(e); | ||
} else { | ||
// NOTE: this fallback branch is almost entirely used by unit tests. | ||
// All browsers now support pointer events, but JSDOM still does not. | ||
pressProps.onMouseDown = (e)=>{ | ||
// Only handle left clicks | ||
if (e.button !== 0 || !e.currentTarget.contains(e.target)) return; | ||
// Due to browser inconsistencies, especially on mobile browsers, we prevent | ||
// default on mouse down and handle focusing the pressable element ourselves. | ||
if ($0294ea432cd92340$var$shouldPreventDefaultDown(e.currentTarget)) e.preventDefault(); | ||
if (state.ignoreEmulatedMouseEvents) { | ||
@@ -388,5 +392,9 @@ e.stopPropagation(); | ||
state.pointerType = (0, $bBqCQ$reactariautils.isVirtualClick)(e.nativeEvent) ? 'virtual' : 'mouse'; | ||
if (!isDisabled && !preventFocusOnPress) (0, $bBqCQ$reactariautils.focusWithoutScrolling)(e.currentTarget); | ||
let shouldStopPropagation = triggerPressStart(e, state.pointerType); | ||
// Flush sync so that focus moved during react re-renders occurs before we yield back to the browser. | ||
let shouldStopPropagation = (0, $bBqCQ$reactdom.flushSync)(()=>triggerPressStart(e, state.pointerType)); | ||
if (shouldStopPropagation) e.stopPropagation(); | ||
if (preventFocusOnPress) { | ||
let dispose = (0, $625cf83917e112ad$exports.preventFocus)(e.target); | ||
if (dispose) state.disposables.push(dispose); | ||
} | ||
addGlobalListener((0, $bBqCQ$reactariautils.getOwnerDocument)(e.currentTarget), 'mouseup', onMouseUp, false); | ||
@@ -420,4 +428,2 @@ }; | ||
if (e.button !== 0) return; | ||
state.isPressed = false; | ||
removeAllGlobalListeners(); | ||
if (state.ignoreEmulatedMouseEvents) { | ||
@@ -427,4 +433,4 @@ state.ignoreEmulatedMouseEvents = false; | ||
} | ||
if (state.target && $0294ea432cd92340$var$isOverTarget(e, state.target) && state.pointerType != null) triggerPressEnd($0294ea432cd92340$var$createEvent(state.target, e), state.pointerType); | ||
else if (state.target && state.isOverTarget && state.pointerType != null) triggerPressEnd($0294ea432cd92340$var$createEvent(state.target, e), state.pointerType, false); | ||
if (state.target && state.target.contains(e.target) && state.pointerType != null) ; | ||
else cancel(e); | ||
state.isOverTarget = false; | ||
@@ -442,5 +448,2 @@ }; | ||
state.pointerType = 'touch'; | ||
// Due to browser inconsistencies, especially on mobile browsers, we prevent default | ||
// on the emulated mouse event and handle focusing the pressable element ourselves. | ||
if (!isDisabled && !preventFocusOnPress) (0, $bBqCQ$reactariautils.focusWithoutScrolling)(e.currentTarget); | ||
if (!allowTextSelectionOnPress) (0, $f7e14e656343df57$exports.disableTextSelection)(state.target); | ||
@@ -524,8 +527,9 @@ let shouldStopPropagation = triggerPressStart($0294ea432cd92340$var$createTouchEvent(state.target, e), state.pointerType); | ||
// Remove user-select: none in case component unmounts immediately after pressStart | ||
// eslint-disable-next-line arrow-body-style | ||
(0, $bBqCQ$react.useEffect)(()=>{ | ||
let state = ref.current; | ||
return ()=>{ | ||
var _ref_current_target; | ||
if (!allowTextSelectionOnPress) // eslint-disable-next-line react-hooks/exhaustive-deps | ||
(0, $f7e14e656343df57$exports.restoreTextSelection)((_ref_current_target = ref.current.target) !== null && _ref_current_target !== void 0 ? _ref_current_target : undefined); | ||
var _state_target; | ||
if (!allowTextSelectionOnPress) (0, $f7e14e656343df57$exports.restoreTextSelection)((_state_target = state.target) !== null && _state_target !== void 0 ? _state_target : undefined); | ||
for (let dispose of state.disposables)dispose(); | ||
state.disposables = []; | ||
}; | ||
@@ -621,6 +625,2 @@ }, [ | ||
} | ||
function $0294ea432cd92340$var$shouldPreventDefaultDown(target) { | ||
// We cannot prevent default if the target is a draggable element. | ||
return !(target instanceof HTMLElement) || !target.hasAttribute('draggable'); | ||
} | ||
function $0294ea432cd92340$var$shouldPreventDefaultUp(target) { | ||
@@ -627,0 +627,0 @@ if (target instanceof HTMLInputElement) return false; |
import {disableTextSelection as $14c0b72509d70225$export$16a4697467175487, restoreTextSelection as $14c0b72509d70225$export$b0d6fa1ab32e3295} from "./textSelection.module.js"; | ||
import {PressResponderContext as $ae1eeba8b9eafd08$export$5165eccb35aaadb5} from "./context.module.js"; | ||
import {preventFocus as $8a9cb279dc87e130$export$cabe61c495ee3649} from "./utils.module.js"; | ||
import {_ as $7mdmh$_} from "@swc/helpers/_/_class_private_field_get"; | ||
import {_ as $7mdmh$_1} from "@swc/helpers/_/_class_private_field_init"; | ||
import {_ as $7mdmh$_2} from "@swc/helpers/_/_class_private_field_set"; | ||
import {mergeProps as $7mdmh$mergeProps, useSyncRef as $7mdmh$useSyncRef, useGlobalListeners as $7mdmh$useGlobalListeners, useEffectEvent as $7mdmh$useEffectEvent, getOwnerDocument as $7mdmh$getOwnerDocument, chain as $7mdmh$chain, isMac as $7mdmh$isMac, openLink as $7mdmh$openLink, isVirtualClick as $7mdmh$isVirtualClick, focusWithoutScrolling as $7mdmh$focusWithoutScrolling, isVirtualPointerEvent as $7mdmh$isVirtualPointerEvent, getOwnerWindow as $7mdmh$getOwnerWindow} from "@react-aria/utils"; | ||
import {mergeProps as $7mdmh$mergeProps, useSyncRef as $7mdmh$useSyncRef, useGlobalListeners as $7mdmh$useGlobalListeners, useEffectEvent as $7mdmh$useEffectEvent, getOwnerDocument as $7mdmh$getOwnerDocument, chain as $7mdmh$chain, isMac as $7mdmh$isMac, openLink as $7mdmh$openLink, isVirtualClick as $7mdmh$isVirtualClick, isVirtualPointerEvent as $7mdmh$isVirtualPointerEvent, focusWithoutScrolling as $7mdmh$focusWithoutScrolling, getOwnerWindow as $7mdmh$getOwnerWindow} from "@react-aria/utils"; | ||
import {flushSync as $7mdmh$flushSync} from "react-dom"; | ||
import {useContext as $7mdmh$useContext, useState as $7mdmh$useState, useRef as $7mdmh$useRef, useMemo as $7mdmh$useMemo, useEffect as $7mdmh$useEffect} from "react"; | ||
@@ -30,2 +32,4 @@ | ||
function $f6c31cce2adf654f$var$usePressResponderContext(props) { | ||
@@ -93,3 +97,2 @@ // Consume context from <PressResponder> and merge with props. | ||
ignoreEmulatedMouseEvents: false, | ||
ignoreClickAfterPress: false, | ||
didFirePressStart: false, | ||
@@ -100,3 +103,4 @@ isTriggeringEvent: false, | ||
isOverTarget: false, | ||
pointerType: null | ||
pointerType: null, | ||
disposables: [] | ||
}); | ||
@@ -123,3 +127,2 @@ let { addGlobalListener: addGlobalListener, removeAllGlobalListeners: removeAllGlobalListeners } = (0, $7mdmh$useGlobalListeners)(); | ||
if (!state.didFirePressStart) return false; | ||
state.ignoreClickAfterPress = true; | ||
state.didFirePressStart = false; | ||
@@ -158,3 +161,3 @@ state.isTriggeringEvent = true; | ||
if (state.isPressed && state.target) { | ||
if (state.isOverTarget && state.pointerType != null) triggerPressEnd($f6c31cce2adf654f$var$createEvent(state.target, e), state.pointerType, false); | ||
if (state.didFirePressStart && state.pointerType != null) triggerPressEnd($f6c31cce2adf654f$var$createEvent(state.target, e), state.pointerType, false); | ||
state.isPressed = false; | ||
@@ -166,2 +169,4 @@ state.isOverTarget = false; | ||
if (!allowTextSelectionOnPress) (0, $14c0b72509d70225$export$b0d6fa1ab32e3295)(state.target); | ||
for (let dispose of state.disposables)dispose(); | ||
state.disposables = []; | ||
} | ||
@@ -186,2 +191,3 @@ }); | ||
state.isPressed = true; | ||
state.pointerType = 'keyboard'; | ||
shouldStopPropagation = triggerPressStart(e, 'keyboard'); | ||
@@ -215,5 +221,3 @@ // Focus may move before the key up event, so register the event on the document | ||
// trigger as if it were a keyboard click. | ||
if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || (0, $7mdmh$isVirtualClick)(e.nativeEvent))) { | ||
// Ensure the element receives focus (VoiceOver on iOS does not do this) | ||
if (!isDisabled && !preventFocusOnPress) (0, $7mdmh$focusWithoutScrolling)(e.currentTarget); | ||
if (!state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || (0, $7mdmh$isVirtualClick)(e.nativeEvent))) { | ||
let stopPressStart = triggerPressStart(e, 'virtual'); | ||
@@ -223,5 +227,9 @@ let stopPressUp = triggerPressUp(e, 'virtual'); | ||
shouldStopPropagation = stopPressStart && stopPressUp && stopPressEnd; | ||
} else if (state.isPressed && state.pointerType !== 'keyboard') { | ||
let pointerType = state.pointerType || e.nativeEvent.pointerType || 'virtual'; | ||
shouldStopPropagation = triggerPressEnd($f6c31cce2adf654f$var$createEvent(e.currentTarget, e), pointerType, true); | ||
state.isOverTarget = false; | ||
cancel(e); | ||
} | ||
state.ignoreEmulatedMouseEvents = false; | ||
state.ignoreClickAfterPress = false; | ||
if (shouldStopPropagation) e.stopPropagation(); | ||
@@ -272,5 +280,2 @@ } | ||
} | ||
// Due to browser inconsistencies, especially on mobile browsers, we prevent | ||
// default on pointer down and handle focusing the pressable element ourselves. | ||
if ($f6c31cce2adf654f$var$shouldPreventDefaultDown(e.currentTarget)) e.preventDefault(); | ||
state.pointerType = e.pointerType; | ||
@@ -283,6 +288,8 @@ let shouldStopPropagation = true; | ||
state.target = e.currentTarget; | ||
if (!isDisabled && !preventFocusOnPress) (0, $7mdmh$focusWithoutScrolling)(e.currentTarget); | ||
if (!allowTextSelectionOnPress) (0, $14c0b72509d70225$export$16a4697467175487)(state.target); | ||
shouldStopPropagation = triggerPressStart(e, state.pointerType); | ||
addGlobalListener((0, $7mdmh$getOwnerDocument)(e.currentTarget), 'pointermove', onPointerMove, false); | ||
// Release pointer capture so that touch interactions can leave the original target. | ||
// This enables onPointerLeave and onPointerEnter to fire. | ||
let target = e.target; | ||
if ('releasePointerCapture' in target) target.releasePointerCapture(e.pointerId); | ||
addGlobalListener((0, $7mdmh$getOwnerDocument)(e.currentTarget), 'pointerup', onPointerUp, false); | ||
@@ -296,6 +303,6 @@ addGlobalListener((0, $7mdmh$getOwnerDocument)(e.currentTarget), 'pointercancel', onPointerCancel, false); | ||
if (e.button === 0) { | ||
// Chrome and Firefox on touch Windows devices require mouse down events | ||
// to be canceled in addition to pointer events, or an extra asynchronous | ||
// focus event will be fired. | ||
if ($f6c31cce2adf654f$var$shouldPreventDefaultDown(e.currentTarget)) e.preventDefault(); | ||
if (preventFocusOnPress) { | ||
let dispose = (0, $8a9cb279dc87e130$export$cabe61c495ee3649)(e.target); | ||
if (dispose) state.disposables.push(dispose); | ||
} | ||
e.stopPropagation(); | ||
@@ -308,17 +315,12 @@ } | ||
// Only handle left clicks | ||
// Safari on iOS sometimes fires pointerup events, even | ||
// when the touch isn't over the target, so double check. | ||
if (e.button === 0 && $f6c31cce2adf654f$var$isOverTarget(e, e.currentTarget)) triggerPressUp(e, state.pointerType || e.pointerType); | ||
if (e.button === 0) triggerPressUp(e, state.pointerType || e.pointerType); | ||
}; | ||
// Safari on iOS < 13.2 does not implement pointerenter/pointerleave events correctly. | ||
// Use pointer move events instead to implement our own hit testing. | ||
// See https://bugs.webkit.org/show_bug.cgi?id=199803 | ||
let onPointerMove = (e)=>{ | ||
if (e.pointerId !== state.activePointerId) return; | ||
if (state.target && $f6c31cce2adf654f$var$isOverTarget(e, state.target)) { | ||
if (!state.isOverTarget && state.pointerType != null) { | ||
state.isOverTarget = true; | ||
triggerPressStart($f6c31cce2adf654f$var$createEvent(state.target, e), state.pointerType); | ||
} | ||
} else if (state.target && state.isOverTarget && state.pointerType != null) { | ||
pressProps.onPointerEnter = (e)=>{ | ||
if (e.pointerId === state.activePointerId && state.target && !state.isOverTarget && state.pointerType != null) { | ||
state.isOverTarget = true; | ||
triggerPressStart($f6c31cce2adf654f$var$createEvent(state.target, e), state.pointerType); | ||
} | ||
}; | ||
pressProps.onPointerLeave = (e)=>{ | ||
if (e.pointerId === state.activePointerId && state.target && state.isOverTarget && state.pointerType != null) { | ||
state.isOverTarget = false; | ||
@@ -331,27 +333,30 @@ triggerPressEnd($f6c31cce2adf654f$var$createEvent(state.target, e), state.pointerType, false); | ||
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) { | ||
if ($f6c31cce2adf654f$var$isOverTarget(e, state.target) && state.pointerType != null) triggerPressEnd($f6c31cce2adf654f$var$createEvent(state.target, e), state.pointerType); | ||
else if (state.isOverTarget && state.pointerType != null) triggerPressEnd($f6c31cce2adf654f$var$createEvent(state.target, e), state.pointerType, false); | ||
state.isPressed = false; | ||
if (state.target.contains(e.target) && state.pointerType != null) { | ||
// Wait for onClick to fire onPress. This avoids browser issues when the DOM | ||
// is mutated between onPointerUp and onClick, and is more compatible with third party libraries. | ||
// https://github.com/adobe/react-spectrum/issues/1513 | ||
// https://issues.chromium.org/issues/40732224 | ||
// However, iOS and Android do not focus or fire onClick after a long press. | ||
// We work around this by triggering a click ourselves after a timeout. | ||
// This timeout is canceled during the click event in case the real one fires first. | ||
// In testing, a 0ms delay is too short. 5ms seems long enough for the browser to fire the real events. | ||
let clicked = false; | ||
let timeout = setTimeout(()=>{ | ||
if (state.isPressed && state.target instanceof HTMLElement) { | ||
if (clicked) cancel(e); | ||
else { | ||
(0, $7mdmh$focusWithoutScrolling)(state.target); | ||
state.target.click(); | ||
} | ||
} | ||
}, 5); | ||
// Use a capturing listener to track if a click occurred. | ||
// If stopPropagation is called it may never reach our handler. | ||
addGlobalListener(e.currentTarget, 'click', ()=>clicked = true, true); | ||
state.disposables.push(()=>clearTimeout(timeout)); | ||
} else cancel(e); | ||
// Ignore subsequent onPointerLeave event before onClick on touch devices. | ||
state.isOverTarget = false; | ||
state.activePointerId = null; | ||
state.pointerType = null; | ||
removeAllGlobalListeners(); | ||
if (!allowTextSelectionOnPress) (0, $14c0b72509d70225$export$b0d6fa1ab32e3295)(state.target); | ||
// Prevent subsequent touchend event from triggering onClick on unrelated elements on Android. See below. | ||
// Both 'touch' and 'pen' pointerTypes trigger onTouchEnd, but 'mouse' does not. | ||
if ('ontouchend' in state.target && e.pointerType !== 'mouse') addGlobalListener(state.target, 'touchend', onTouchEnd, { | ||
once: true | ||
}); | ||
} | ||
}; | ||
// This is a workaround for an Android Chrome/Firefox issue where click events are fired on an incorrect element | ||
// if the original target is removed during onPointerUp (before onClick). | ||
// https://github.com/adobe/react-spectrum/issues/1513 | ||
// https://issues.chromium.org/issues/40732224 | ||
// Note: this event must be registered directly on the element, not via React props in order to work. | ||
// https://github.com/facebook/react/issues/9809 | ||
let onTouchEnd = (e)=>{ | ||
// Don't preventDefault if we actually want the default (e.g. submit/link click). | ||
if ($f6c31cce2adf654f$var$shouldPreventDefaultUp(e.target)) e.preventDefault(); | ||
}; | ||
let onPointerCancel = (e)=>{ | ||
@@ -366,8 +371,7 @@ cancel(e); | ||
} else { | ||
// NOTE: this fallback branch is almost entirely used by unit tests. | ||
// All browsers now support pointer events, but JSDOM still does not. | ||
pressProps.onMouseDown = (e)=>{ | ||
// Only handle left clicks | ||
if (e.button !== 0 || !e.currentTarget.contains(e.target)) return; | ||
// Due to browser inconsistencies, especially on mobile browsers, we prevent | ||
// default on mouse down and handle focusing the pressable element ourselves. | ||
if ($f6c31cce2adf654f$var$shouldPreventDefaultDown(e.currentTarget)) e.preventDefault(); | ||
if (state.ignoreEmulatedMouseEvents) { | ||
@@ -381,5 +385,9 @@ e.stopPropagation(); | ||
state.pointerType = (0, $7mdmh$isVirtualClick)(e.nativeEvent) ? 'virtual' : 'mouse'; | ||
if (!isDisabled && !preventFocusOnPress) (0, $7mdmh$focusWithoutScrolling)(e.currentTarget); | ||
let shouldStopPropagation = triggerPressStart(e, state.pointerType); | ||
// Flush sync so that focus moved during react re-renders occurs before we yield back to the browser. | ||
let shouldStopPropagation = (0, $7mdmh$flushSync)(()=>triggerPressStart(e, state.pointerType)); | ||
if (shouldStopPropagation) e.stopPropagation(); | ||
if (preventFocusOnPress) { | ||
let dispose = (0, $8a9cb279dc87e130$export$cabe61c495ee3649)(e.target); | ||
if (dispose) state.disposables.push(dispose); | ||
} | ||
addGlobalListener((0, $7mdmh$getOwnerDocument)(e.currentTarget), 'mouseup', onMouseUp, false); | ||
@@ -413,4 +421,2 @@ }; | ||
if (e.button !== 0) return; | ||
state.isPressed = false; | ||
removeAllGlobalListeners(); | ||
if (state.ignoreEmulatedMouseEvents) { | ||
@@ -420,4 +426,4 @@ state.ignoreEmulatedMouseEvents = false; | ||
} | ||
if (state.target && $f6c31cce2adf654f$var$isOverTarget(e, state.target) && state.pointerType != null) triggerPressEnd($f6c31cce2adf654f$var$createEvent(state.target, e), state.pointerType); | ||
else if (state.target && state.isOverTarget && state.pointerType != null) triggerPressEnd($f6c31cce2adf654f$var$createEvent(state.target, e), state.pointerType, false); | ||
if (state.target && state.target.contains(e.target) && state.pointerType != null) ; | ||
else cancel(e); | ||
state.isOverTarget = false; | ||
@@ -435,5 +441,2 @@ }; | ||
state.pointerType = 'touch'; | ||
// Due to browser inconsistencies, especially on mobile browsers, we prevent default | ||
// on the emulated mouse event and handle focusing the pressable element ourselves. | ||
if (!isDisabled && !preventFocusOnPress) (0, $7mdmh$focusWithoutScrolling)(e.currentTarget); | ||
if (!allowTextSelectionOnPress) (0, $14c0b72509d70225$export$16a4697467175487)(state.target); | ||
@@ -517,8 +520,9 @@ let shouldStopPropagation = triggerPressStart($f6c31cce2adf654f$var$createTouchEvent(state.target, e), state.pointerType); | ||
// Remove user-select: none in case component unmounts immediately after pressStart | ||
// eslint-disable-next-line arrow-body-style | ||
(0, $7mdmh$useEffect)(()=>{ | ||
let state = ref.current; | ||
return ()=>{ | ||
var _ref_current_target; | ||
if (!allowTextSelectionOnPress) // eslint-disable-next-line react-hooks/exhaustive-deps | ||
(0, $14c0b72509d70225$export$b0d6fa1ab32e3295)((_ref_current_target = ref.current.target) !== null && _ref_current_target !== void 0 ? _ref_current_target : undefined); | ||
var _state_target; | ||
if (!allowTextSelectionOnPress) (0, $14c0b72509d70225$export$b0d6fa1ab32e3295)((_state_target = state.target) !== null && _state_target !== void 0 ? _state_target : undefined); | ||
for (let dispose of state.disposables)dispose(); | ||
state.disposables = []; | ||
}; | ||
@@ -614,6 +618,2 @@ }, [ | ||
} | ||
function $f6c31cce2adf654f$var$shouldPreventDefaultDown(target) { | ||
// We cannot prevent default if the target is a draggable element. | ||
return !(target instanceof HTMLElement) || !target.hasAttribute('draggable'); | ||
} | ||
function $f6c31cce2adf654f$var$shouldPreventDefaultUp(target) { | ||
@@ -620,0 +620,0 @@ if (target instanceof HTMLInputElement) return false; |
@@ -0,3 +1,3 @@ | ||
var $iJhOP$reactariautils = require("@react-aria/utils"); | ||
var $iJhOP$react = require("react"); | ||
var $iJhOP$reactariautils = require("@react-aria/utils"); | ||
@@ -10,2 +10,4 @@ | ||
$parcel$export(module.exports, "useSyntheticBlurEvent", () => $625cf83917e112ad$export$715c682d09d639cc); | ||
$parcel$export(module.exports, "ignoreFocusEvent", () => $625cf83917e112ad$export$fda7da73ab5d4c48); | ||
$parcel$export(module.exports, "preventFocus", () => $625cf83917e112ad$export$cabe61c495ee3649); | ||
/* | ||
@@ -59,3 +61,2 @@ * Copyright 2020 Adobe. All rights reserved. | ||
// Clean up MutationObserver on unmount. See below. | ||
// eslint-disable-next-line arrow-body-style | ||
(0, $iJhOP$reactariautils.useLayoutEffect)(()=>{ | ||
@@ -120,4 +121,57 @@ const state = stateRef.current; | ||
} | ||
let $625cf83917e112ad$export$fda7da73ab5d4c48 = false; | ||
function $625cf83917e112ad$export$cabe61c495ee3649(target) { | ||
// The browser will focus the nearest focusable ancestor of our target. | ||
while(target && !(0, $iJhOP$reactariautils.isFocusable)(target))target = target.parentElement; | ||
let window = (0, $iJhOP$reactariautils.getOwnerWindow)(target); | ||
let activeElement = window.document.activeElement; | ||
if (!activeElement || activeElement === target) return; | ||
$625cf83917e112ad$export$fda7da73ab5d4c48 = true; | ||
let isRefocusing = false; | ||
let onBlur = (e)=>{ | ||
if (e.target === activeElement || isRefocusing) e.stopImmediatePropagation(); | ||
}; | ||
let onFocusOut = (e)=>{ | ||
if (e.target === activeElement || isRefocusing) { | ||
e.stopImmediatePropagation(); | ||
// If there was no focusable ancestor, we don't expect a focus event. | ||
// Re-focus the original active element here. | ||
if (!target && !isRefocusing) { | ||
isRefocusing = true; | ||
(0, $iJhOP$reactariautils.focusWithoutScrolling)(activeElement); | ||
cleanup(); | ||
} | ||
} | ||
}; | ||
let onFocus = (e)=>{ | ||
if (e.target === target || isRefocusing) e.stopImmediatePropagation(); | ||
}; | ||
let onFocusIn = (e)=>{ | ||
if (e.target === target || isRefocusing) { | ||
e.stopImmediatePropagation(); | ||
if (!isRefocusing) { | ||
isRefocusing = true; | ||
(0, $iJhOP$reactariautils.focusWithoutScrolling)(activeElement); | ||
cleanup(); | ||
} | ||
} | ||
}; | ||
window.addEventListener('blur', onBlur, true); | ||
window.addEventListener('focusout', onFocusOut, true); | ||
window.addEventListener('focusin', onFocusIn, true); | ||
window.addEventListener('focus', onFocus, true); | ||
let cleanup = ()=>{ | ||
cancelAnimationFrame(raf); | ||
window.removeEventListener('blur', onBlur, true); | ||
window.removeEventListener('focusout', onFocusOut, true); | ||
window.removeEventListener('focusin', onFocusIn, true); | ||
window.removeEventListener('focus', onFocus, true); | ||
$625cf83917e112ad$export$fda7da73ab5d4c48 = false; | ||
isRefocusing = false; | ||
}; | ||
let raf = requestAnimationFrame(cleanup); | ||
return cleanup; | ||
} | ||
//# sourceMappingURL=utils.main.js.map |
@@ -0,3 +1,3 @@ | ||
import {useLayoutEffect as $6dfIe$useLayoutEffect, useEffectEvent as $6dfIe$useEffectEvent, isFocusable as $6dfIe$isFocusable, getOwnerWindow as $6dfIe$getOwnerWindow, focusWithoutScrolling as $6dfIe$focusWithoutScrolling} from "@react-aria/utils"; | ||
import {useRef as $6dfIe$useRef, useCallback as $6dfIe$useCallback} from "react"; | ||
import {useLayoutEffect as $6dfIe$useLayoutEffect, useEffectEvent as $6dfIe$useEffectEvent} from "@react-aria/utils"; | ||
@@ -52,3 +52,2 @@ /* | ||
// Clean up MutationObserver on unmount. See below. | ||
// eslint-disable-next-line arrow-body-style | ||
(0, $6dfIe$useLayoutEffect)(()=>{ | ||
@@ -113,5 +112,58 @@ const state = stateRef.current; | ||
} | ||
let $8a9cb279dc87e130$export$fda7da73ab5d4c48 = false; | ||
function $8a9cb279dc87e130$export$cabe61c495ee3649(target) { | ||
// The browser will focus the nearest focusable ancestor of our target. | ||
while(target && !(0, $6dfIe$isFocusable)(target))target = target.parentElement; | ||
let window = (0, $6dfIe$getOwnerWindow)(target); | ||
let activeElement = window.document.activeElement; | ||
if (!activeElement || activeElement === target) return; | ||
$8a9cb279dc87e130$export$fda7da73ab5d4c48 = true; | ||
let isRefocusing = false; | ||
let onBlur = (e)=>{ | ||
if (e.target === activeElement || isRefocusing) e.stopImmediatePropagation(); | ||
}; | ||
let onFocusOut = (e)=>{ | ||
if (e.target === activeElement || isRefocusing) { | ||
e.stopImmediatePropagation(); | ||
// If there was no focusable ancestor, we don't expect a focus event. | ||
// Re-focus the original active element here. | ||
if (!target && !isRefocusing) { | ||
isRefocusing = true; | ||
(0, $6dfIe$focusWithoutScrolling)(activeElement); | ||
cleanup(); | ||
} | ||
} | ||
}; | ||
let onFocus = (e)=>{ | ||
if (e.target === target || isRefocusing) e.stopImmediatePropagation(); | ||
}; | ||
let onFocusIn = (e)=>{ | ||
if (e.target === target || isRefocusing) { | ||
e.stopImmediatePropagation(); | ||
if (!isRefocusing) { | ||
isRefocusing = true; | ||
(0, $6dfIe$focusWithoutScrolling)(activeElement); | ||
cleanup(); | ||
} | ||
} | ||
}; | ||
window.addEventListener('blur', onBlur, true); | ||
window.addEventListener('focusout', onFocusOut, true); | ||
window.addEventListener('focusin', onFocusIn, true); | ||
window.addEventListener('focus', onFocus, true); | ||
let cleanup = ()=>{ | ||
cancelAnimationFrame(raf); | ||
window.removeEventListener('blur', onBlur, true); | ||
window.removeEventListener('focusout', onFocusOut, true); | ||
window.removeEventListener('focusin', onFocusIn, true); | ||
window.removeEventListener('focus', onFocus, true); | ||
$8a9cb279dc87e130$export$fda7da73ab5d4c48 = false; | ||
isRefocusing = false; | ||
}; | ||
let raf = requestAnimationFrame(cleanup); | ||
return cleanup; | ||
} | ||
export {$8a9cb279dc87e130$export$905e7fc544a71f36 as SyntheticFocusEvent, $8a9cb279dc87e130$export$715c682d09d639cc as useSyntheticBlurEvent}; | ||
export {$8a9cb279dc87e130$export$905e7fc544a71f36 as SyntheticFocusEvent, $8a9cb279dc87e130$export$715c682d09d639cc as useSyntheticBlurEvent, $8a9cb279dc87e130$export$fda7da73ab5d4c48 as ignoreFocusEvent, $8a9cb279dc87e130$export$cabe61c495ee3649 as preventFocus}; | ||
//# sourceMappingURL=utils.module.js.map |
{ | ||
"name": "@react-aria/interactions", | ||
"version": "3.0.0-nightly-c904e066c-240917", | ||
"version": "3.0.0-nightly-cdba74876-250117", | ||
"description": "Spectrum UI components in React", | ||
@@ -25,14 +25,14 @@ "license": "Apache-2.0", | ||
"dependencies": { | ||
"@react-aria/ssr": "^3.0.0-nightly-c904e066c-240917", | ||
"@react-aria/utils": "^3.0.0-nightly-c904e066c-240917", | ||
"@react-types/shared": "^3.0.0-nightly-c904e066c-240917", | ||
"@react-aria/ssr": "3.0.0-nightly-cdba74876-250117", | ||
"@react-aria/utils": "3.0.0-nightly-cdba74876-250117", | ||
"@react-types/shared": "3.0.0-nightly-cdba74876-250117", | ||
"@swc/helpers": "^0.5.0" | ||
}, | ||
"peerDependencies": { | ||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" | ||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", | ||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"stableVersion": "3.22.2" | ||
} | ||
} |
@@ -35,6 +35,13 @@ /* | ||
stopPropagation() { | ||
console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.'); | ||
if (shouldStopPropagation) { | ||
console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.'); | ||
} else { | ||
shouldStopPropagation = true; | ||
} | ||
}, | ||
continuePropagation() { | ||
shouldStopPropagation = false; | ||
}, | ||
isPropagationStopped() { | ||
return shouldStopPropagation; | ||
} | ||
@@ -41,0 +48,0 @@ }; |
@@ -39,3 +39,3 @@ /* | ||
if (state === 'default') { | ||
// eslint-disable-next-line no-restricted-globals | ||
const documentObject = getOwnerDocument(target); | ||
@@ -73,3 +73,3 @@ savedUserSelect = documentObject.documentElement.style.webkitUserSelect; | ||
if (state === 'restoring') { | ||
// eslint-disable-next-line no-restricted-globals | ||
const documentObject = getOwnerDocument(target); | ||
@@ -76,0 +76,0 @@ if (documentObject.documentElement.style.webkitUserSelect === 'none') { |
@@ -19,2 +19,3 @@ /* | ||
import {getOwnerDocument, getOwnerWindow, isMac, isVirtualClick} from '@react-aria/utils'; | ||
import {ignoreFocusEvent} from './utils'; | ||
import {useEffect, useState} from 'react'; | ||
@@ -96,3 +97,3 @@ import {useIsSSR} from '@react-aria/ssr'; | ||
// cause keyboard focus rings to appear. | ||
if (e.target === window || e.target === document) { | ||
if (e.target === window || e.target === document || ignoreFocusEvent) { | ||
return; | ||
@@ -113,2 +114,6 @@ } | ||
function handleWindowBlur() { | ||
if (ignoreFocusEvent) { | ||
return; | ||
} | ||
// When the window is blurred, reset state. This is necessary when tabbing out of the window, | ||
@@ -115,0 +120,0 @@ // for example, since a subsequent focus event won't be fired. |
@@ -13,4 +13,4 @@ /* | ||
import {DOMAttributes, LongPressEvent} from '@react-types/shared'; | ||
import {mergeProps, useDescription, useGlobalListeners} from '@react-aria/utils'; | ||
import {DOMAttributes, FocusableElement, LongPressEvent} from '@react-types/shared'; | ||
import {focusWithoutScrolling, getOwnerDocument, mergeProps, useDescription, useGlobalListeners} from '@react-aria/utils'; | ||
import {usePress} from './usePress'; | ||
@@ -85,2 +85,8 @@ import {useRef} from 'react'; | ||
e.target.dispatchEvent(new PointerEvent('pointercancel', {bubbles: true})); | ||
// Ensure target is focused. On touch devices, browsers typically focus on pointer up. | ||
if (getOwnerDocument(e.target).activeElement !== e.target) { | ||
focusWithoutScrolling(e.target as FocusableElement); | ||
} | ||
if (onLongPress) { | ||
@@ -87,0 +93,0 @@ onLongPress({ |
@@ -21,3 +21,5 @@ /* | ||
import {DOMAttributes, FocusableElement, PressEvent as IPressEvent, PointerType, PressEvents, RefObject} from '@react-types/shared'; | ||
import {flushSync} from 'react-dom'; | ||
import {PressResponderContext} from './context'; | ||
import {preventFocus} from './utils'; | ||
import {TouchEvent as RTouchEvent, useContext, useEffect, useMemo, useRef, useState} from 'react'; | ||
@@ -51,3 +53,2 @@ | ||
ignoreEmulatedMouseEvents: boolean, | ||
ignoreClickAfterPress: boolean, | ||
didFirePressStart: boolean, | ||
@@ -60,3 +61,4 @@ isTriggeringEvent: boolean, | ||
userSelect?: string, | ||
metaKeyEvents?: Map<string, KeyboardEvent> | ||
metaKeyEvents?: Map<string, KeyboardEvent>, | ||
disposables: Array<() => void> | ||
} | ||
@@ -173,3 +175,2 @@ | ||
ignoreEmulatedMouseEvents: false, | ||
ignoreClickAfterPress: false, | ||
didFirePressStart: false, | ||
@@ -180,3 +181,4 @@ isTriggeringEvent: false, | ||
isOverTarget: false, | ||
pointerType: null | ||
pointerType: null, | ||
disposables: [] | ||
}); | ||
@@ -216,3 +218,2 @@ | ||
state.ignoreClickAfterPress = true; | ||
state.didFirePressStart = false; | ||
@@ -264,3 +265,3 @@ state.isTriggeringEvent = true; | ||
if (state.isPressed && state.target) { | ||
if (state.isOverTarget && state.pointerType != null) { | ||
if (state.didFirePressStart && state.pointerType != null) { | ||
triggerPressEnd(createEvent(state.target, e), state.pointerType, false); | ||
@@ -276,2 +277,6 @@ } | ||
} | ||
for (let dispose of state.disposables) { | ||
dispose(); | ||
} | ||
state.disposables = []; | ||
} | ||
@@ -302,2 +307,3 @@ }); | ||
state.isPressed = true; | ||
state.pointerType = 'keyboard'; | ||
shouldStopPropagation = triggerPressStart(e, 'keyboard'); | ||
@@ -346,11 +352,6 @@ | ||
} | ||
// If triggered from a screen reader or by using element.click(), | ||
// trigger as if it were a keyboard click. | ||
if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) { | ||
// Ensure the element receives focus (VoiceOver on iOS does not do this) | ||
if (!isDisabled && !preventFocusOnPress) { | ||
focusWithoutScrolling(e.currentTarget); | ||
} | ||
if (!state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) { | ||
let stopPressStart = triggerPressStart(e, 'virtual'); | ||
@@ -360,6 +361,10 @@ let stopPressUp = triggerPressUp(e, 'virtual'); | ||
shouldStopPropagation = stopPressStart && stopPressUp && stopPressEnd; | ||
} else if (state.isPressed && state.pointerType !== 'keyboard') { | ||
let pointerType = state.pointerType || (e.nativeEvent as PointerEvent).pointerType as PointerType || 'virtual'; | ||
shouldStopPropagation = triggerPressEnd(createEvent(e.currentTarget, e), pointerType, true); | ||
state.isOverTarget = false; | ||
cancel(e); | ||
} | ||
state.ignoreEmulatedMouseEvents = false; | ||
state.ignoreClickAfterPress = false; | ||
if (shouldStopPropagation) { | ||
@@ -422,8 +427,2 @@ e.stopPropagation(); | ||
// Due to browser inconsistencies, especially on mobile browsers, we prevent | ||
// default on pointer down and handle focusing the pressable element ourselves. | ||
if (shouldPreventDefaultDown(e.currentTarget as Element)) { | ||
e.preventDefault(); | ||
} | ||
state.pointerType = e.pointerType; | ||
@@ -438,6 +437,2 @@ | ||
if (!isDisabled && !preventFocusOnPress) { | ||
focusWithoutScrolling(e.currentTarget); | ||
} | ||
if (!allowTextSelectionOnPress) { | ||
@@ -449,3 +444,9 @@ disableTextSelection(state.target); | ||
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointermove', onPointerMove, false); | ||
// Release pointer capture so that touch interactions can leave the original target. | ||
// This enables onPointerLeave and onPointerEnter to fire. | ||
let target = e.target as Element; | ||
if ('releasePointerCapture' in target) { | ||
target.releasePointerCapture(e.pointerId); | ||
} | ||
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointerup', onPointerUp, false); | ||
@@ -466,7 +467,7 @@ addGlobalListener(getOwnerDocument(e.currentTarget), 'pointercancel', onPointerCancel, false); | ||
if (e.button === 0) { | ||
// Chrome and Firefox on touch Windows devices require mouse down events | ||
// to be canceled in addition to pointer events, or an extra asynchronous | ||
// focus event will be fired. | ||
if (shouldPreventDefaultDown(e.currentTarget as Element)) { | ||
e.preventDefault(); | ||
if (preventFocusOnPress) { | ||
let dispose = preventFocus(e.target as FocusableElement); | ||
if (dispose) { | ||
state.disposables.push(dispose); | ||
} | ||
} | ||
@@ -485,5 +486,3 @@ | ||
// Only handle left clicks | ||
// Safari on iOS sometimes fires pointerup events, even | ||
// when the touch isn't over the target, so double check. | ||
if (e.button === 0 && isOverTarget(e, e.currentTarget)) { | ||
if (e.button === 0) { | ||
triggerPressUp(e, state.pointerType || e.pointerType); | ||
@@ -493,16 +492,11 @@ } | ||
// Safari on iOS < 13.2 does not implement pointerenter/pointerleave events correctly. | ||
// Use pointer move events instead to implement our own hit testing. | ||
// See https://bugs.webkit.org/show_bug.cgi?id=199803 | ||
let onPointerMove = (e: PointerEvent) => { | ||
if (e.pointerId !== state.activePointerId) { | ||
return; | ||
pressProps.onPointerEnter = (e) => { | ||
if (e.pointerId === state.activePointerId && state.target && !state.isOverTarget && state.pointerType != null) { | ||
state.isOverTarget = true; | ||
triggerPressStart(createEvent(state.target, e), state.pointerType); | ||
} | ||
}; | ||
if (state.target && isOverTarget(e, state.target)) { | ||
if (!state.isOverTarget && state.pointerType != null) { | ||
state.isOverTarget = true; | ||
triggerPressStart(createEvent(state.target, e), state.pointerType); | ||
} | ||
} else if (state.target && state.isOverTarget && state.pointerType != null) { | ||
pressProps.onPointerLeave = (e) => { | ||
if (e.pointerId === state.activePointerId && state.target && state.isOverTarget && state.pointerType != null) { | ||
state.isOverTarget = false; | ||
@@ -516,38 +510,35 @@ triggerPressEnd(createEvent(state.target, e), state.pointerType, false); | ||
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) { | ||
if (isOverTarget(e, state.target) && state.pointerType != null) { | ||
triggerPressEnd(createEvent(state.target, e), state.pointerType); | ||
} else if (state.isOverTarget && state.pointerType != null) { | ||
triggerPressEnd(createEvent(state.target, e), state.pointerType, false); | ||
if (state.target.contains(e.target as Element) && state.pointerType != null) { | ||
// Wait for onClick to fire onPress. This avoids browser issues when the DOM | ||
// is mutated between onPointerUp and onClick, and is more compatible with third party libraries. | ||
// https://github.com/adobe/react-spectrum/issues/1513 | ||
// https://issues.chromium.org/issues/40732224 | ||
// However, iOS and Android do not focus or fire onClick after a long press. | ||
// We work around this by triggering a click ourselves after a timeout. | ||
// This timeout is canceled during the click event in case the real one fires first. | ||
// In testing, a 0ms delay is too short. 5ms seems long enough for the browser to fire the real events. | ||
let clicked = false; | ||
let timeout = setTimeout(() => { | ||
if (state.isPressed && state.target instanceof HTMLElement) { | ||
if (clicked) { | ||
cancel(e); | ||
} else { | ||
focusWithoutScrolling(state.target); | ||
state.target.click(); | ||
} | ||
} | ||
}, 5); | ||
// Use a capturing listener to track if a click occurred. | ||
// If stopPropagation is called it may never reach our handler. | ||
addGlobalListener(e.currentTarget as Document, 'click', () => clicked = true, true); | ||
state.disposables.push(() => clearTimeout(timeout)); | ||
} else { | ||
cancel(e); | ||
} | ||
state.isPressed = false; | ||
// Ignore subsequent onPointerLeave event before onClick on touch devices. | ||
state.isOverTarget = false; | ||
state.activePointerId = null; | ||
state.pointerType = null; | ||
removeAllGlobalListeners(); | ||
if (!allowTextSelectionOnPress) { | ||
restoreTextSelection(state.target); | ||
} | ||
// Prevent subsequent touchend event from triggering onClick on unrelated elements on Android. See below. | ||
// Both 'touch' and 'pen' pointerTypes trigger onTouchEnd, but 'mouse' does not. | ||
if ('ontouchend' in state.target && e.pointerType !== 'mouse') { | ||
addGlobalListener(state.target, 'touchend', onTouchEnd, {once: true}); | ||
} | ||
} | ||
}; | ||
// This is a workaround for an Android Chrome/Firefox issue where click events are fired on an incorrect element | ||
// if the original target is removed during onPointerUp (before onClick). | ||
// https://github.com/adobe/react-spectrum/issues/1513 | ||
// https://issues.chromium.org/issues/40732224 | ||
// Note: this event must be registered directly on the element, not via React props in order to work. | ||
// https://github.com/facebook/react/issues/9809 | ||
let onTouchEnd = (e: TouchEvent) => { | ||
// Don't preventDefault if we actually want the default (e.g. submit/link click). | ||
if (shouldPreventDefaultUp(e.target as Element)) { | ||
e.preventDefault(); | ||
} | ||
}; | ||
let onPointerCancel = (e: PointerEvent) => { | ||
@@ -566,2 +557,5 @@ cancel(e); | ||
} else { | ||
// NOTE: this fallback branch is almost entirely used by unit tests. | ||
// All browsers now support pointer events, but JSDOM still does not. | ||
pressProps.onMouseDown = (e) => { | ||
@@ -573,8 +567,2 @@ // Only handle left clicks | ||
// Due to browser inconsistencies, especially on mobile browsers, we prevent | ||
// default on mouse down and handle focusing the pressable element ourselves. | ||
if (shouldPreventDefaultDown(e.currentTarget)) { | ||
e.preventDefault(); | ||
} | ||
if (state.ignoreEmulatedMouseEvents) { | ||
@@ -590,7 +578,4 @@ e.stopPropagation(); | ||
if (!isDisabled && !preventFocusOnPress) { | ||
focusWithoutScrolling(e.currentTarget); | ||
} | ||
let shouldStopPropagation = triggerPressStart(e, state.pointerType); | ||
// Flush sync so that focus moved during react re-renders occurs before we yield back to the browser. | ||
let shouldStopPropagation = flushSync(() => triggerPressStart(e, state.pointerType!)); | ||
if (shouldStopPropagation) { | ||
@@ -600,2 +585,9 @@ e.stopPropagation(); | ||
if (preventFocusOnPress) { | ||
let dispose = preventFocus(e.target as FocusableElement); | ||
if (dispose) { | ||
state.disposables.push(dispose); | ||
} | ||
} | ||
addGlobalListener(getOwnerDocument(e.currentTarget), 'mouseup', onMouseUp, false); | ||
@@ -653,5 +645,2 @@ }; | ||
state.isPressed = false; | ||
removeAllGlobalListeners(); | ||
if (state.ignoreEmulatedMouseEvents) { | ||
@@ -662,6 +651,7 @@ state.ignoreEmulatedMouseEvents = false; | ||
if (state.target && isOverTarget(e, state.target) && state.pointerType != null) { | ||
triggerPressEnd(createEvent(state.target, e), state.pointerType); | ||
} else if (state.target && state.isOverTarget && state.pointerType != null) { | ||
triggerPressEnd(createEvent(state.target, e), state.pointerType, false); | ||
if (state.target && state.target.contains(e.target as Element) && state.pointerType != null) { | ||
// Wait for onClick to fire onPress. This avoids browser issues when the DOM | ||
// is mutated between onMouseUp and onClick, and is more compatible with third party libraries. | ||
} else { | ||
cancel(e); | ||
} | ||
@@ -688,8 +678,2 @@ | ||
// Due to browser inconsistencies, especially on mobile browsers, we prevent default | ||
// on the emulated mouse event and handle focusing the pressable element ourselves. | ||
if (!isDisabled && !preventFocusOnPress) { | ||
focusWithoutScrolling(e.currentTarget); | ||
} | ||
if (!allowTextSelectionOnPress) { | ||
@@ -815,9 +799,14 @@ disableTextSelection(state.target); | ||
// Remove user-select: none in case component unmounts immediately after pressStart | ||
// eslint-disable-next-line arrow-body-style | ||
useEffect(() => { | ||
let state = ref.current; | ||
return () => { | ||
if (!allowTextSelectionOnPress) { | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
restoreTextSelection(ref.current.target ?? undefined); | ||
restoreTextSelection(state.target ?? undefined); | ||
} | ||
for (let dispose of state.disposables) { | ||
dispose(); | ||
} | ||
state.disposables = []; | ||
}; | ||
@@ -962,7 +951,2 @@ }, [allowTextSelectionOnPress]); | ||
function shouldPreventDefaultDown(target: Element) { | ||
// We cannot prevent default if the target is a draggable element. | ||
return !(target instanceof HTMLElement) || !target.hasAttribute('draggable'); | ||
} | ||
function shouldPreventDefaultUp(target: Element) { | ||
@@ -972,3 +956,3 @@ if (target instanceof HTMLInputElement) { | ||
} | ||
if (target instanceof HTMLButtonElement) { | ||
@@ -975,0 +959,0 @@ return target.type !== 'submit' && target.type !== 'reset'; |
@@ -13,4 +13,5 @@ /* | ||
import {FocusableElement} from '@react-types/shared'; | ||
import {focusWithoutScrolling, getOwnerWindow, isFocusable, useEffectEvent, useLayoutEffect} from '@react-aria/utils'; | ||
import {FocusEvent as ReactFocusEvent, useCallback, useRef} from 'react'; | ||
import {useEffectEvent, useLayoutEffect} from '@react-aria/utils'; | ||
@@ -72,3 +73,3 @@ export class SyntheticFocusEvent<Target = Element> implements ReactFocusEvent<Target> { | ||
// Clean up MutationObserver on unmount. See below. | ||
// eslint-disable-next-line arrow-body-style | ||
useLayoutEffect(() => { | ||
@@ -133,1 +134,79 @@ const state = stateRef.current; | ||
} | ||
export let ignoreFocusEvent = false; | ||
/** | ||
* This function prevents the next focus event fired on `target`, without using `event.preventDefault()`. | ||
* It works by waiting for the series of focus events to occur, and reverts focus back to where it was before. | ||
* It also makes these events mostly non-observable by using a capturing listener on the window and stopping propagation. | ||
*/ | ||
export function preventFocus(target: FocusableElement | null) { | ||
// The browser will focus the nearest focusable ancestor of our target. | ||
while (target && !isFocusable(target)) { | ||
target = target.parentElement; | ||
} | ||
let window = getOwnerWindow(target); | ||
let activeElement = window.document.activeElement as FocusableElement | null; | ||
if (!activeElement || activeElement === target) { | ||
return; | ||
} | ||
ignoreFocusEvent = true; | ||
let isRefocusing = false; | ||
let onBlur = (e: FocusEvent) => { | ||
if (e.target === activeElement || isRefocusing) { | ||
e.stopImmediatePropagation(); | ||
} | ||
}; | ||
let onFocusOut = (e: FocusEvent) => { | ||
if (e.target === activeElement || isRefocusing) { | ||
e.stopImmediatePropagation(); | ||
// If there was no focusable ancestor, we don't expect a focus event. | ||
// Re-focus the original active element here. | ||
if (!target && !isRefocusing) { | ||
isRefocusing = true; | ||
focusWithoutScrolling(activeElement); | ||
cleanup(); | ||
} | ||
} | ||
}; | ||
let onFocus = (e: FocusEvent) => { | ||
if (e.target === target || isRefocusing) { | ||
e.stopImmediatePropagation(); | ||
} | ||
}; | ||
let onFocusIn = (e: FocusEvent) => { | ||
if (e.target === target || isRefocusing) { | ||
e.stopImmediatePropagation(); | ||
if (!isRefocusing) { | ||
isRefocusing = true; | ||
focusWithoutScrolling(activeElement); | ||
cleanup(); | ||
} | ||
} | ||
}; | ||
window.addEventListener('blur', onBlur, true); | ||
window.addEventListener('focusout', onFocusOut, true); | ||
window.addEventListener('focusin', onFocusIn, true); | ||
window.addEventListener('focus', onFocus, true); | ||
let cleanup = () => { | ||
cancelAnimationFrame(raf); | ||
window.removeEventListener('blur', onBlur, true); | ||
window.removeEventListener('focusout', onFocusOut, true); | ||
window.removeEventListener('focusin', onFocusIn, true); | ||
window.removeEventListener('focus', onFocus, true); | ||
ignoreFocusEvent = false; | ||
isRefocusing = false; | ||
}; | ||
let raf = requestAnimationFrame(cleanup); | ||
return cleanup; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
729136
8676
6
+ Added@react-aria/ssr@3.0.0-nightly-cdba74876-250117(transitive)
+ Added@react-aria/utils@3.0.0-nightly-cdba74876-250117(transitive)
+ Added@react-stately/utils@3.0.0-nightly-cdba74876-250117(transitive)
+ Added@react-types/shared@3.0.0-nightly-cdba74876-250117(transitive)
- Removed@react-aria/ssr@3.9.7(transitive)
- Removed@react-aria/utils@3.27.0(transitive)
- Removed@react-stately/utils@3.10.5(transitive)
- Removed@react-types/shared@3.27.0(transitive)