Skip to content

Commit 670e6e2

Browse files
authoredMar 31, 2022
Fix/prevent scroll pencil (#475)
1 parent 5979b1a commit 670e6e2

File tree

14 files changed

+128
-40
lines changed

14 files changed

+128
-40
lines changed
 

‎.changeset/clean-maps-happen.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@use-gesture/core': patch
3+
---
4+
5+
- fix: trigger `pointerDown` event when `triggerAllEvents` and `delay` options are set
6+
- fix: disable scroll prevention when the event type is `'mouse'`
7+
- feat: add `axisThreshold` property to set a threshold for axis calculation (can be set per device for the drag gesture)
8+
- fix: axis are now calculated on pixel movement rather than on transformed movement

‎demo/src/sandboxes/gesture-drag/src/App.jsx

-2
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ function Draggable() {
5353
}
5454
)
5555

56-
console.log(bind())
57-
5856
return (
5957
<>
6058
<a.div tabIndex={-1} {...bind()} className={styles.drag} style={style}>

‎documentation/pages/docs/options.mdx

+20-1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Here are all options that can be applied to gestures.
104104
| [`preventDefault`](#preventdefault) | **all** | Will `preventDefault` all events triggered by the handler. |
105105
| [`triggerAllEvents`](#triggerallevents) | **all** | Forces the handler to fire even for non intentional displacement (ignores the `threshold`). In that case, the `intentional` attribute from state will remain `false` until the threshold is reached. |
106106
| [`axis`](#axis) | **all** | Your handler will only trigger if a movement is detected on the specified axis. |
107+
| [`axisThreshold`](#axisthreshold) | **xy** | Axes are calculated based on a threshold. For drag, thresholds are specified per device type. |
107108
| [`bounds`](#bounds) | **xy** | Limits the gesture `offset` to the specified bounds. |
108109
| [`scaleBounds`](#scalebounds) | **pinch** | Limits the scale `offset` to the specified bounds. |
109110
| [`angleBounds`](#anglebounds) | **pinch** | Limits the angle `offset` to the specified bounds. |
@@ -181,6 +182,22 @@ function LockAxisExample() {
181182
}
182183
```
183184

185+
### axisThreshold
186+
187+
A gesture axis is determined whenever x-axis and y-axis displacements reach a threshold.
188+
189+
#### xy gestures (except for drag)
190+
191+
<Specs types="number" defaultValue={0} />
192+
193+
For non-drag xy gestures, that threshold is always a number, which defaults to `0`.
194+
195+
#### drag
196+
197+
<Specs types="{ mouse: number, pen: number, touch: number }" defaultValue="{ mouse: 0, pen: 8, touch: 0 }" />
198+
199+
Drag logic is the same, but you must specify the threshold for a specific device. Since pen is much more sensitive than other devices, the default threshold has been empirically set to `8`.
200+
184201
### bounds
185202

186203
<Specs
@@ -560,7 +577,7 @@ function Transform() {
560577
}
561578
```
562579

563-
> When you use the `useGesture` hook, you can set the `transform` option at the shared level and at the gesture level, with the `transform` set at the gesture level oveerriding the shared one.
580+
> When you use the `useGesture` hook, you can set the `transform` option at the shared level and at the gesture level, with the `transform` set at the gesture level overriding the shared one.
564581
565582
```js
566583
useGesture({/* handlers */ }, {
@@ -577,6 +594,8 @@ useGesture({/* handlers */ }, {
577594

578595
Forces the handler to fire even for non intentional displacement (ignores the `threshold`). In that case, the `intentional` attribute from state will remain `false` until the threshold is reached.
579596

597+
> At this point, this option will not work as expected when using `preventScroll` or `preventScrollAxis`. Please flag an issue if this breaks your usecase.
598+
580599
### window
581600

582601
<Specs types="node" defaultValue="window" />

‎packages/core/src/config/coordinatesConfigResolver.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { commonConfigResolver } from './commonConfigResolver'
22
import { InternalCoordinatesOptions, CoordinatesConfig, Bounds, DragBounds, State, Vector2 } from '../types'
33

4+
const DEFAULT_AXIS_THRESHOLD = 0
5+
46
export const coordinatesConfigResolver = {
57
...commonConfigResolver,
68
axis(
@@ -12,6 +14,9 @@ export const coordinatesConfigResolver = {
1214
this.lockDirection = axis === 'lock'
1315
if (!this.lockDirection) return axis as any
1416
},
17+
axisThreshold(value = DEFAULT_AXIS_THRESHOLD) {
18+
return value
19+
},
1520
bounds(
1621
value: DragBounds | ((state: State) => DragBounds) = {}
1722
): (() => EventTarget | null) | HTMLElement | [Vector2, Vector2] {

‎packages/core/src/config/dragConfigResolver.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PointerType } from '../types'
12
import { DragConfig, InternalDragOptions, Vector2 } from '../types'
23
import { V } from '../utils/maths'
34
import { coordinatesConfigResolver } from './coordinatesConfigResolver'
@@ -9,6 +10,8 @@ export const DEFAULT_SWIPE_VELOCITY = 0.5
910
export const DEFAULT_SWIPE_DISTANCE = 50
1011
export const DEFAULT_SWIPE_DURATION = 250
1112

13+
const DEFAULT_DRAG_AXIS_THRESHOLD: Record<PointerType, number> = { mouse: 0, touch: 0, pen: 8 }
14+
1215
export const dragConfigResolver = {
1316
...coordinatesConfigResolver,
1417
device(
@@ -69,6 +72,10 @@ export const dragConfigResolver = {
6972
default:
7073
return value
7174
}
75+
},
76+
axisThreshold(value: Record<PointerType, number>) {
77+
if (!value) return DEFAULT_DRAG_AXIS_THRESHOLD
78+
return { ...DEFAULT_DRAG_AXIS_THRESHOLD, ...value }
7279
}
7380
}
7481

+32-22
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import { Engine } from './Engine'
22
import { V } from '../utils/maths'
33
import { CoordinatesKey, Vector2 } from '../types'
4+
import { getPointerType } from '../utils/events'
45

5-
function selectAxis([dx, dy]: Vector2) {
6-
const d = Math.abs(dx) - Math.abs(dy)
7-
if (d > 0) return 'x'
8-
if (d < 0) return 'y'
9-
return undefined
10-
}
6+
function selectAxis([dx, dy]: Vector2, threshold: number) {
7+
const absDx = Math.abs(dx)
8+
const absDy = Math.abs(dy)
119

12-
function restrictVectorToAxis(v: Vector2, axis?: 'x' | 'y') {
13-
switch (axis) {
14-
case 'x':
15-
v[1] = 0
16-
break // [ x, 0 ]
17-
case 'y':
18-
v[0] = 0
19-
break // [ 0, y ]
10+
if (absDx > absDy && absDx > threshold) {
11+
return 'x'
12+
}
13+
if (absDy > absDx && absDy > threshold) {
14+
return 'y'
2015
}
16+
return undefined
2117
}
2218

2319
export abstract class CoordinatesEngine<Key extends CoordinatesKey> extends Engine<Key> {
@@ -41,20 +37,34 @@ export abstract class CoordinatesEngine<Key extends CoordinatesKey> extends Engi
4137
this.state.movement = V.sub(this.state.offset, this.state.lastOffset)
4238
}
4339

44-
intent(v: Vector2) {
45-
this.state.axis = this.state.axis || selectAxis(v)
40+
axisIntent(event?: UIEvent) {
41+
const state = this.state
42+
const config = this.config
43+
44+
if (!state.axis && event) {
45+
const threshold =
46+
typeof config.axisThreshold === 'object' ? config.axisThreshold[getPointerType(event)] : config.axisThreshold
47+
48+
state.axis = selectAxis(state._movement, threshold)
49+
}
4650

4751
// We block the movement if either:
4852
// - config.lockDirection or config.axis was set but axis isn't detected yet
4953
// - config.axis was set but is different than detected axis
50-
this.state._blocked =
51-
((this.config.lockDirection || !!this.config.axis) && !this.state.axis) ||
52-
(!!this.config.axis && this.config.axis !== this.state.axis)
53-
54-
if (this.state._blocked) return
54+
state._blocked =
55+
((config.lockDirection || !!config.axis) && !state.axis) || (!!config.axis && config.axis !== state.axis)
56+
}
5557

58+
restrictToAxis(v: Vector2) {
5659
if (this.config.axis || this.config.lockDirection) {
57-
restrictVectorToAxis(v, this.state.axis)
60+
switch (this.state.axis) {
61+
case 'x':
62+
v[1] = 0
63+
break // [ x, 0 ]
64+
case 'y':
65+
v[0] = 0
66+
break // [ 0, y ]
67+
}
5868
}
5969
}
6070
}

‎packages/core/src/engines/DragEngine.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CoordinatesEngine } from './CoordinatesEngine'
22
import { coordinatesConfigResolver } from '../config/coordinatesConfigResolver'
3-
import { pointerId, pointerValues } from '../utils/events'
3+
import { pointerId, getPointerType, pointerValues } from '../utils/events'
44
import { V } from '../utils/maths'
55
import { Vector2 } from '../types'
66

@@ -106,13 +106,18 @@ export class DragEngine extends CoordinatesEngine<'drag'> {
106106
this.computeValues(pointerValues(event))
107107
this.computeInitial()
108108

109-
if (config.preventScrollAxis) {
109+
if (config.preventScrollAxis && getPointerType(event) !== 'mouse') {
110110
// when preventScrollAxis is set we don't consider the gesture active
111111
// until it's deliberate
112112
state._active = false
113113
this.setupScrollPrevention(event)
114114
} else if (config.delay > 0) {
115115
this.setupDelayTrigger(event)
116+
// makes sure we emit all events when `triggerAllEvents` flag is `true`
117+
if (config.triggerAllEvents) {
118+
this.compute(event)
119+
this.emit()
120+
}
116121
} else {
117122
this.startPointerDrag(event)
118123
}

‎packages/core/src/engines/Engine.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ export interface Engine<Key extends GestureKey> {
3535
* Function used by some gestures to determine the intentionality of a
3636
* a movement depending on thresholds. The intent function can change the
3737
* `state._active` or `state._blocked` flags if the gesture isn't intentional.
38-
* @param movement
38+
* @param event
3939
*/
40-
intent?(movement: Vector2): void
40+
axisIntent?(event?: UIEvent): void
41+
42+
restrictToAxis?(movement: Vector2): void
4143
}
4244

4345
export abstract class Engine<Key extends GestureKey> {
@@ -251,6 +253,9 @@ export abstract class Engine<Key extends GestureKey> {
251253
V.addTo(state._distance, _absoluteDelta)
252254
}
253255

256+
// let's run intentionality check.
257+
if (this.axisIntent) this.axisIntent(event)
258+
254259
// _movement is calculated by each gesture engine
255260
const [_m0, _m1] = state._movement
256261
const [t0, t1] = config.threshold
@@ -284,8 +289,8 @@ export abstract class Engine<Key extends GestureKey> {
284289
movement[1] = _step[1] !== false ? _m1 - _step[1] : 0
285290
}
286291

287-
// let's run intentionality check.
288-
if (this.intent) this.intent(movement)
292+
if (this.restrictToAxis && !state._blocked) this.restrictToAxis(movement)
293+
289294
const previousOffset = state.offset
290295

291296
const gestureIsActive = (state._active && !state._blocked) || state.active

‎packages/core/src/engines/PinchEngine.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,20 @@ export class PinchEngine extends Engine<'pinch'> {
4040
this.state.movement = [offset[0] / lastOffset[0], offset[1] - lastOffset[1]]
4141
}
4242

43-
intent(v: Vector2) {
43+
axisIntent() {
4444
const state = this.state
45+
const [_m0, _m1] = state._movement
4546
if (!state.axis) {
46-
const axisMovementDifference = Math.abs(v[0]) * SCALE_ANGLE_RATIO_INTENT_DEG - Math.abs(v[1])
47+
const axisMovementDifference = Math.abs(_m0) * SCALE_ANGLE_RATIO_INTENT_DEG - Math.abs(_m1)
4748
if (axisMovementDifference < 0) state.axis = 'angle'
4849
else if (axisMovementDifference > 0) state.axis = 'scale'
4950
}
51+
}
5052

53+
restrictToAxis(v: Vector2) {
5154
if (this.config.lockDirection) {
52-
if (state.axis === 'scale') v[1] = 0
53-
else if (state.axis === 'angle') v[0] = 0
55+
if (this.state.axis === 'scale') v[1] = 0
56+
else if (this.state.axis === 'angle') v[0] = 0
5457
}
5558
}
5659

‎packages/core/src/types/config.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { State } from './state'
2-
import { Vector2, Target } from './utils'
2+
import { Vector2, Target, PointerType } from './utils'
33

44
export type GestureKey = Exclude<keyof State, 'shared'>
55
export type CoordinatesKey = Exclude<GestureKey, 'pinch'>
@@ -84,6 +84,11 @@ export type CoordinatesConfig<Key extends CoordinatesKey = CoordinatesKey> = Ges
8484
* Limits the gesture `offset` to the specified bounds.
8585
*/
8686
bounds?: Bounds | ((state: State[Key]) => Bounds)
87+
/**
88+
* Determines the number of pixels in one direction needed for axises to be
89+
* calculated.
90+
*/
91+
axisThreshold?: number
8792
}
8893

8994
export type PinchBounds = { min?: number; max?: number }
@@ -127,7 +132,7 @@ export type MoveConfig = CoordinatesConfig<'move'> & MoveAndHoverMouseOnly
127132

128133
export type HoverConfig = MoveAndHoverMouseOnly
129134

130-
export type DragConfig = CoordinatesConfig<'drag'> & {
135+
export type DragConfig = Omit<CoordinatesConfig<'drag'>, 'axisThreshold'> & {
131136
/**
132137
* If true, the component won't trigger your drag logic if the user just clicked on the component.
133138
*/
@@ -203,6 +208,12 @@ export type DragConfig = CoordinatesConfig<'drag'> & {
203208
* to 180ms.
204209
*/
205210
delay?: boolean | number
211+
/**
212+
* Key-number record that determines for each device (`'mouse'`, `'touch'`,
213+
* `'pen'`) the number of pixels of drag in one direction needed for axises to
214+
* be calculated.
215+
*/
216+
axisThreshold?: Partial<Record<PointerType, number>>
206217
}
207218

208219
export type UserDragConfig = GenericOptions & DragConfig

‎packages/core/src/types/internalConfig.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GestureKey, CoordinatesKey, ModifierKey } from './config'
22
import { State } from './state'
3-
import { Vector2 } from './utils'
3+
import { PointerType, Vector2 } from './utils'
44

55
export type InternalGenericOptions = {
66
target?: () => EventTarget
@@ -25,9 +25,10 @@ export type InternalGestureOptions<Key extends GestureKey = GestureKey> = {
2525
export type InternalCoordinatesOptions<Key extends CoordinatesKey = CoordinatesKey> = InternalGestureOptions<Key> & {
2626
axis?: 'x' | 'y'
2727
lockDirection: boolean
28+
axisThreshold: number
2829
}
2930

30-
export type InternalDragOptions = InternalCoordinatesOptions<'drag'> & {
31+
export type InternalDragOptions = Omit<InternalCoordinatesOptions<'drag'>, 'axisThreshold'> & {
3132
filterTaps: boolean
3233
tapsThreshold: number
3334
pointerButtons: number | number[]
@@ -42,6 +43,7 @@ export type InternalDragOptions = InternalCoordinatesOptions<'drag'> & {
4243
duration: number
4344
}
4445
delay: number
46+
axisThreshold: Record<PointerType, number>
4547
}
4648

4749
export type InternalPinchOptions = InternalGestureOptions<'pinch'> & {

‎packages/core/src/types/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export type Vector2 = [number, number]
22
export type WebKitGestureEvent = PointerEvent & { scale: number; rotation: number }
33
export type Target = EventTarget | React.RefObject<EventTarget>
4+
export type PointerType = 'mouse' | 'touch' | 'pen'

‎packages/core/src/utils/events.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PointerType } from '../types'
12
import { Vector2 } from '../types'
23

34
const EVENT_TYPE_MAP: any = {
@@ -43,6 +44,12 @@ export function isTouch(event: UIEvent) {
4344
return 'touches' in event
4445
}
4546

47+
export function getPointerType(event: UIEvent): PointerType {
48+
if (isTouch(event)) return 'touch'
49+
if ('pointerType' in event) return (event as PointerEvent).pointerType as PointerType
50+
return 'mouse'
51+
}
52+
4653
function getCurrentTargetTouchList(event: TouchEvent) {
4754
return Array.from(event.touches).filter(
4855
(e) => e.target === event.currentTarget || (event.currentTarget as Node)?.contains?.(e.target as Node)

‎test/config.test.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ describe('testing derived config', () => {
7676
pointerLock: false,
7777
pointerCapture: true,
7878
filterTaps: false,
79-
tapsThreshold: 3
79+
tapsThreshold: 3,
80+
axisThreshold: { mouse: 0, pen: 8, touch: 0 }
8081
})
8182
})
8283

@@ -118,6 +119,12 @@ describe('testing derived config', () => {
118119
expect(parse(dragConfig, 'drag').drag).toHaveProperty('device', 'pointer')
119120
})
120121

122+
test(`derived axisThreshold is preoperly computer`, () => {
123+
const axisThreshold = { pen: 3, mouse: 2 }
124+
dragConfig = { axisThreshold }
125+
expect(parse(dragConfig, 'drag').drag).toHaveProperty('axisThreshold', { mouse: 2, pen: 3, touch: 0 })
126+
})
127+
121128
test(`derived transform is properly computed`, () => {
122129
const transform = ([x, y]: Vector2) => [x / 2, y / 4] as Vector2
123130

0 commit comments

Comments
 (0)
Please sign in to comment.