Skip to content

Commit

Permalink
refactor: Remove custom hit testing in usePress
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Nov 22, 2024
1 parent ab9fd5c commit 8503b04
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 18 deletions.
33 changes: 16 additions & 17 deletions packages/@react-aria/interactions/src/usePress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,13 @@ export function usePress(props: PressHookProps): PressResult {

shouldStopPropagation = triggerPressStart(e, state.pointerType);

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);
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointercancel', onPointerCancel, false);
}
Expand Down Expand Up @@ -467,27 +473,20 @@ export function usePress(props: PressHookProps): PressResult {
}

// 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);
}
};

// 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;
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
cancelOnPointerExit(e);
Expand All @@ -496,7 +495,7 @@ export function usePress(props: PressHookProps): PressResult {

let onPointerUp = (e: PointerEvent) => {
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
if (isOverTarget(e, state.target) && state.pointerType != null) {
if (state.target.contains(e.target as Element) && 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);
Expand Down
12 changes: 11 additions & 1 deletion packages/@react-aria/interactions/test/usePress.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,15 @@ describe('usePress', function () {
);

let el = res.getByText('test');
el.releasePointerCapture = jest.fn();
fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
expect(el.releasePointerCapture).toHaveBeenCalled();
fireEvent(el, pointerEvent('pointermove', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
fireEvent(el, pointerEvent('pointerup', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
// react listens for pointerout and pointerover instead of pointerleave and pointerenter...
fireEvent(el, pointerEvent('pointerout', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
fireEvent(document, pointerEvent('pointerup', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
fireEvent(el, pointerEvent('pointermove', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
fireEvent(el, pointerEvent('pointerover', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));

expect(events).toEqual([
{
Expand Down Expand Up @@ -182,7 +187,10 @@ describe('usePress', function () {
events = [];
fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
fireEvent(el, pointerEvent('pointermove', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
// react listens for pointerout and pointerover instead of pointerleave and pointerenter...
fireEvent(el, pointerEvent('pointerout', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
fireEvent(el, pointerEvent('pointermove', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
fireEvent(el, pointerEvent('pointerover', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
fireEvent(el, pointerEvent('pointerup', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));

expect(events).toEqual([
Expand Down Expand Up @@ -387,7 +395,9 @@ describe('usePress', function () {
let el = res.getByText('test');
fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
fireEvent(el, pointerEvent('pointermove', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
fireEvent(el, pointerEvent('pointerout', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
fireEvent(el, pointerEvent('pointermove', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
fireEvent(el, pointerEvent('pointerover', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));

expect(events).toEqual([
{
Expand Down

0 comments on commit 8503b04

Please sign in to comment.