Skip to content

Commit

Permalink
Merge pull request #1407 from atlassian/fixing-force-press
Browse files Browse the repository at this point in the history
back porting force press fix from virtual branch
  • Loading branch information
alexreardon authored Jul 22, 2019
2 parents 0bf8072 + 0218553 commit 3fd58eb
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 487 deletions.
22 changes: 11 additions & 11 deletions .size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
{
"dist/react-beautiful-dnd.js": {
"bundled": 393181,
"minified": 147695,
"gzipped": 41532
"bundled": 392081,
"minified": 147079,
"gzipped": 41340
},
"dist/react-beautiful-dnd.min.js": {
"bundled": 324803,
"minified": 116622,
"gzipped": 33468
"bundled": 323864,
"minified": 116189,
"gzipped": 33372
},
"dist/react-beautiful-dnd.esm.js": {
"bundled": 239412,
"minified": 124303,
"gzipped": 31620,
"bundled": 238384,
"minified": 123773,
"gzipped": 31477,
"treeshaked": {
"rollup": {
"code": 30396,
"code": 29975,
"import_statements": 793
},
"webpack": {
"code": 34327
"code": 33907
}
}
}
Expand Down
44 changes: 9 additions & 35 deletions src/view/use-drag-handle/sensor/use-mouse-sensor.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import createEventMarshal, {
import type { Callbacks } from '../drag-handle-types';
import { bindEvents, unbindEvents } from '../util/bind-events';
import createScheduler from '../util/create-scheduler';
import { warning } from '../../../dev-warning';
import * as keyCodes from '../../key-codes';
import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name';
import createPostDragEventPreventer, {
Expand All @@ -32,10 +31,6 @@ export type Args = {|
export type OnMouseDown = (event: MouseEvent) => void;

// Custom event format for force press inputs
type MouseForceChangedEvent = MouseEvent & {
webkitForce?: number,
};

// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
const primaryButton: number = 0;
const noop = () => {};
Expand All @@ -48,7 +43,8 @@ export default function useMouseSensor(args: Args): OnMouseDown {
canStartCapturing,
getWindow,
callbacks,
getShouldRespectForcePress,
// Currently always respecting force press due to safari bug (see below)
// getShouldRespectForcePress,
onCaptureStart,
onCaptureEnd,
} = args;
Expand Down Expand Up @@ -248,34 +244,13 @@ export default function useMouseSensor(args: Args): OnMouseDown {
// Only for safari which has decided to introduce its own custom way of doing things
// https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html
{
eventName: 'webkitmouseforcechanged',
fn: (event: MouseForceChangedEvent) => {
if (
event.webkitForce == null ||
(MouseEvent: any).WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN == null
) {
warning(
'handling a mouse force changed event when it is not supported',
);
return;
}

const forcePressThreshold: number = (MouseEvent: any)
.WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN;
const isForcePressing: boolean =
event.webkitForce >= forcePressThreshold;

// New behaviour
if (!getShouldRespectForcePress()) {
event.preventDefault();
return;
}

if (isForcePressing) {
// it is considered a indirect cancel so we do not
// prevent default in any situation.
cancel();
}
eventName: 'webkitmouseforcedown',
fn: () => {
// In order to opt out of force press correctly we need to call
// event.preventDefault() on webkitmouseforcewillbegin
// We have no way of doing this in this branch so we are always respecting force touches
// There is a correct fix in the `virtual` branch
cancel();
},
},
// Cancel on page visibility change
Expand All @@ -293,7 +268,6 @@ export default function useMouseSensor(args: Args): OnMouseDown {
stop,
callbacks,
getWindow,
getShouldRespectForcePress,
]);

const bindWindowEvents = useCallback(() => {
Expand Down
140 changes: 54 additions & 86 deletions src/view/use-drag-handle/sensor/use-touch-sensor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import supportedPageVisibilityEventName from '../util/supported-page-visibility-
import createPostDragEventPreventer, {
type EventPreventer,
} from '../util/create-post-drag-event-preventer';
import useLayoutEffect from '../../use-isomorphic-layout-effect';

export type Args = {|
callbacks: Callbacks,
Expand All @@ -36,75 +37,13 @@ type TouchWithForce = Touch & {
force: number,
};

type WebkitHack = {|
preventTouchMove: () => void,
releaseTouchMove: () => void,
|};

export const timeForLongPress: number = 150;
// Decreased from 150 as a work around for an issue for forcepress on iOS
// https://github.com/atlassian/react-beautiful-dnd/issues/1401
export const timeForLongPress: number = 120;
export const forcePressThreshold: number = 0.15;
const touchStartMarshal: EventMarshal = createEventMarshal();
const noop = (): void => {};

// Webkit does not allow event.preventDefault() in dynamically added handlers
// So we add an always listening event handler to get around this :(
// webkit bug: https://bugs.webkit.org/show_bug.cgi?id=184250
const webkitHack: WebkitHack = (() => {
const stub: WebkitHack = {
preventTouchMove: noop,
releaseTouchMove: noop,
};

// Do nothing when server side rendering
if (typeof window === 'undefined') {
return stub;
}

// Device has no touch support - no point adding the touch listener
if (!('ontouchstart' in window)) {
return stub;
}

// Not adding any user agent testing as everything pretends to be webkit

let isBlocking: boolean = false;

// Adding a persistent event handler
window.addEventListener(
'touchmove',
(event: TouchEvent) => {
// We let the event go through as normal as nothing
// is blocking the touchmove
if (!isBlocking) {
return;
}

// Our event handler would have worked correctly if the browser
// was not webkit based, or an older version of webkit.
if (event.defaultPrevented) {
return;
}

// Okay, now we need to step in and fix things
event.preventDefault();

// Forcing this to be non-passive so we can get every touchmove
// Not activating in the capture phase like the dynamic touchmove we add.
// Technically it would not matter if we did this in the capture phase
},
{ passive: false, capture: false },
);

const preventTouchMove = () => {
isBlocking = true;
};
const releaseTouchMove = () => {
isBlocking = false;
};

return { preventTouchMove, releaseTouchMove };
})();

export default function useTouchSensor(args: Args): OnTouchStart {
const {
callbacks,
Expand Down Expand Up @@ -143,7 +82,6 @@ export default function useTouchSensor(args: Args): OnTouchStart {
schedule.cancel();
unbindWindowEventsRef.current();
touchStartMarshal.reset();
webkitHack.releaseTouchMove();
hasMovedRef.current = false;
onCaptureEnd();

Expand Down Expand Up @@ -196,11 +134,15 @@ export default function useTouchSensor(args: Args): OnTouchStart {
hasMovedRef.current = true;
}

const { clientX, clientY } = event.touches[0];
const touch: ?Touch = event.touches[0];

if (!touch) {
return;
}

const point: Position = {
x: clientX,
y: clientY,
x: touch.clientX,
y: touch.clientY,
};

// We need to prevent the default event in order to block native scrolling
Expand Down Expand Up @@ -308,32 +250,44 @@ export default function useTouchSensor(args: Args): OnTouchStart {
// Need to opt out of dragging if the user is a force press
// Only for webkit which has decided to introduce its own custom way of doing things
// https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html
// NOTE: this function is back-ported from the `virtual` branch
{
eventName: 'touchforcechange',
fn: (event: TouchEvent) => {
// Not respecting force touches - prevent the event
if (!getShouldRespectForcePress()) {
event.preventDefault();
const touch: TouchWithForce = (event.touches[0]: any);
const isForcePress: boolean = touch.force >= forcePressThreshold;

if (!isForcePress) {
return;
}

// A force push action will no longer fire after a touchmove
if (hasMovedRef.current) {
// This is being super safe. While this situation should not occur we
// are still expressing that we want to opt out of force pressing
event.preventDefault();
const shouldRespect: boolean = getShouldRespectForcePress();

if (pendingRef.current) {
if (shouldRespect) {
cancel();
}
// If not respecting we just let the event go through
// It will not have an impact on the browser until
// there has been a sufficient time ellapsed
return;
}

// A drag could be pending or has already started but no movement has occurred

const touch: TouchWithForce = (event.touches[0]: any);
// DRAGGING

if (touch.force >= forcePressThreshold) {
// this is an indirect cancel so we do not preventDefault
// we also want to allow the force press to occur
if (shouldRespect) {
if (hasMovedRef.current) {
// After the user has moved we do not allow the dragging item to be force pressed
// This prevents strange behaviour such as a link preview opening mid drag
event.preventDefault();
return;
}
// indirect cancel
cancel();
return;
}
// not respecting during a drag
event.preventDefault();
},
},
// Cancel on page visibility change
Expand Down Expand Up @@ -425,11 +379,25 @@ export default function useTouchSensor(args: Args): OnTouchStart {
// browser interactions as possible.
// This includes navigation on anchors which we want to preserve
touchStartMarshal.handle();

// A webkit only hack to prevent touch move events
webkitHack.preventTouchMove();
startPendingDrag(event);
};

// This is needed for safari
// Simply adding a non capture, non passive 'touchmove' listener.
// This forces event.preventDefault() in dynamically added
// touchmove event handlers to actually work
// https://github.com/atlassian/react-beautiful-dnd/issues/1374
useLayoutEffect(function webkitHack() {
const unbind = bindEvents(window, [
{
eventName: 'touchmove',
fn: noop,
options: { capture: false, passive: false },
},
]);

return unbind;
}, []);

return onTouchStart;
}
Loading

0 comments on commit 3fd58eb

Please sign in to comment.