Skip to content

Commit 7f07adb

Browse files
author
Jeroen Kroese
committed
Add 'reverse' option to wheel gesture
1 parent c779631 commit 7f07adb

File tree

8 files changed

+76
-6
lines changed

8 files changed

+76
-6
lines changed

documentation/pages/docs/code/examples.js

+33
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,39 @@ const limitFn = (b, y) =>
246246

247247
const closestLimit = (x, y) => Math.max(limitFn(xBounds, x), limitFn(yBounds, y))
248248

249+
export function Reverse({ setActive }) {
250+
const [isReversed, setIsReversed] = useState(true)
251+
const [{ x, y }, api] = useSpring(() => ({ x: 85, y: 51 }))
252+
const [position] = useState({ x: 85, y: 51 })
253+
254+
const ref = useRef()
255+
useWheel(
256+
({ down, offset: [x, y] }, memo = position) => {
257+
setActive && setActive(down)
258+
api.start({ x: memo.x + x, y: memo.y + y, immediate: true })
259+
},
260+
{
261+
preventDefault: true,
262+
target: ref,
263+
eventOptions: { passive: false },
264+
reverse: isReversed
265+
}
266+
)
267+
return (
268+
<>
269+
<div className={styles.ui}>
270+
<label>
271+
<input type="checkbox" checked={isReversed} onChange={(e) => setIsReversed(e.target.checked)} />
272+
Use `reverse`
273+
</label>
274+
</div>
275+
<div ref={ref} className={styles.limits}>
276+
<animated.div className={styles.wheel} style={{ x, y }} />
277+
</div>
278+
</>
279+
)
280+
}
281+
249282
export function Rubberband({ setActive }) {
250283
const [{ x, y }, api] = useSpring(() => ({ x: 0, y: 0 }))
251284
const bind = useDrag(

documentation/pages/docs/code/styles.module.css

+7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
user-select: none;
3030
}
3131

32+
.wheel {
33+
background: #91c9f9;
34+
border-radius: 16px;
35+
height: 80px;
36+
width: 80px;
37+
}
38+
3239
.overlay {
3340
position: fixed;
3441
top: 0;

documentation/pages/docs/options.mdx

+9
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Here are all options that can be applied to gestures.
131131
| [`swipe.duration`](#swipeduration) | **drag** | The maximum duration in milliseconds that a swipe is detected. |
132132
| `keyboardDisplacement` | **drag** | The distance (in `pixels`) emulated by arrow keys. Default is `10`. |
133133
| `mouseOnly` | **hover, move** | Set to `false` if you want your `hover` or `move` handlers to be triggered on non-mouse events. This is a useful option in case you want to perform logic on touch-enabled devices. |
134+
| [`reverse`](#reverse) | **wheel** | If `true`, inverts the direction of wheel scrolling to mimic natural touchpad gestures. |
134135

135136
## Options explained
136137

@@ -435,6 +436,14 @@ On desktop, you should be able to drag the torus as you would expect without del
435436

436437
This can optionally be used together with `preventScroll`. This defines the axis/axes in which scrolling is permitted, unless the user taps and holds on the element for the specified duration. Afterwhich, all scrolling is blocked. Depending on the complexity of the nesting of the element, you may need to assign the property `touch-action: pan-x`, `touch-action: pan-y`, or both, to the element to allow for the correct behavior.
437438

439+
### reverse
440+
441+
<Specs types="boolean" defaultValue="false" />
442+
443+
When set to `true`, the `reverse` option inverses the direction of the wheel gesture. This can be useful in scenarios where the default gesture direction does not align with the intended interaction design. For example, in a carousel, setting `reverse` to `true` would mean swiping left would move to the next item, and swiping right would move to the previous item, which is the opposite of the default behavior.
444+
445+
<Code id="Reverse" />
446+
438447
### rubberband
439448

440449
<Specs types={['boolean', 'number', 'vector']} defaultValue="[0,0]" />
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import { coordinatesConfigResolver } from './coordinatesConfigResolver'
22

3-
export const wheelConfigResolver = coordinatesConfigResolver
3+
export const wheelConfigResolver = {
4+
...coordinatesConfigResolver,
5+
reverse: (value = false) => value
6+
}

packages/core/src/engines/WheelEngine.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ export class WheelEngine extends CoordinatesEngine<'wheel'> {
2020

2121
wheelChange(event: WheelEvent) {
2222
const state = this.state
23+
const { reverse } = this.config
24+
2325
state._delta = wheelValues(event)
24-
V.addTo(state._movement, state._delta)
26+
reverse ? V.subTo(state._movement, state._delta) : V.addTo(state._movement, state._delta)
2527

2628
// _movement rolls back to when it passed the bounds.
2729
clampStateInternalMovementToBounds(state)

packages/core/src/types/config.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,13 @@ export type MoveConfig = CoordinatesConfig<'move'> & MoveAndHoverMouseOnly
140140

141141
export type HoverConfig = MoveAndHoverMouseOnly
142142

143+
export type WheelConfig = {
144+
/**
145+
* If true, inverts the direction of wheel scrolling to mimic natural touchpad gestures.
146+
*/
147+
reverse?: boolean
148+
}
149+
143150
export type DragConfig = Omit<CoordinatesConfig<'drag'>, 'axisThreshold' | 'bounds'> & {
144151
/**
145152
* If true, the component won't trigger your drag logic if the user just clicked on the component.
@@ -235,14 +242,14 @@ export type DragConfig = Omit<CoordinatesConfig<'drag'>, 'axisThreshold' | 'boun
235242

236243
export type UserDragConfig = GenericOptions & DragConfig
237244
export type UserPinchConfig = GenericOptions & PinchConfig
238-
export type UserWheelConfig = GenericOptions & CoordinatesConfig<'wheel'>
245+
export type UserWheelConfig = GenericOptions & WheelConfig & CoordinatesConfig<'wheel'>
239246
export type UserScrollConfig = GenericOptions & CoordinatesConfig<'scroll'>
240247
export type UserMoveConfig = GenericOptions & MoveConfig
241248
export type UserHoverConfig = GenericOptions & HoverConfig
242249

243250
export type UserGestureConfig = GenericOptions & {
244251
drag?: DragConfig
245-
wheel?: CoordinatesConfig<'wheel'>
252+
wheel?: WheelConfig & CoordinatesConfig<'wheel'>
246253
scroll?: CoordinatesConfig<'scroll'>
247254
move?: MoveConfig
248255
pinch?: PinchConfig

packages/core/src/types/internalConfig.ts

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

@@ -66,7 +66,7 @@ type MoveAndHoverMouseOnly = {
6666
export type InternalConfig = {
6767
shared: InternalGenericOptions
6868
drag?: InternalDragOptions
69-
wheel?: InternalCoordinatesOptions<'wheel'>
69+
wheel?: InternalCoordinatesOptions<'wheel'> & WheelConfig
7070
scroll?: InternalCoordinatesOptions<'scroll'>
7171
move?: InternalCoordinatesOptions<'move'> & MoveAndHoverMouseOnly
7272
hover?: InternalCoordinatesOptions<'hover'> & MoveAndHoverMouseOnly

test/wheel.test.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ describe.each([
9494
expect(getByTestId(`${prefix}wheel-movement`)).toHaveTextContent('13,0')
9595
await waitFor(() => expect(getByTestId(`${prefix}wheel-wheeling`)).toHaveTextContent('false'))
9696
})
97+
98+
test('applying reverse SHOULD inverse wheel directions', async () => {
99+
rerender(<Component gestures={['Wheel']} config={{ wheel: { reverse: true } }} />)
100+
fireEvent.wheel(element, { deltaX: -3, deltaY: 10 })
101+
fireEvent.wheel(element, { deltaX: 4, deltaY: -6 })
102+
expect(getByTestId(`${prefix}wheel-movement`)).toHaveTextContent('-1,-4')
103+
await waitFor(() => expect(getByTestId(`${prefix}wheel-wheeling`)).toHaveTextContent('false'))
104+
})
105+
97106
test('disabling all gestures should prevent state from updating', async () => {
98107
rerender(<Component gestures={['Wheel']} config={{ enabled: false }} />)
99108
fireEvent.wheel(element)

0 commit comments

Comments
 (0)