From 7f07adb25ac8eb53e3b6c9cc80fdea3e040a9218 Mon Sep 17 00:00:00 2001 From: Jeroen Kroese Date: Mon, 1 Apr 2024 16:29:37 +0200 Subject: [PATCH] Add 'reverse' option to wheel gesture --- documentation/pages/docs/code/examples.js | 33 +++++++++++++++++++ .../pages/docs/code/styles.module.css | 7 ++++ documentation/pages/docs/options.mdx | 9 +++++ .../core/src/config/wheelConfigResolver.ts | 5 ++- packages/core/src/engines/WheelEngine.ts | 4 ++- packages/core/src/types/config.ts | 11 +++++-- packages/core/src/types/internalConfig.ts | 4 +-- test/wheel.test.tsx | 9 +++++ 8 files changed, 76 insertions(+), 6 deletions(-) diff --git a/documentation/pages/docs/code/examples.js b/documentation/pages/docs/code/examples.js index 4f10fb2bf..b0fd3d796 100644 --- a/documentation/pages/docs/code/examples.js +++ b/documentation/pages/docs/code/examples.js @@ -246,6 +246,39 @@ const limitFn = (b, y) => const closestLimit = (x, y) => Math.max(limitFn(xBounds, x), limitFn(yBounds, y)) +export function Reverse({ setActive }) { + const [isReversed, setIsReversed] = useState(true) + const [{ x, y }, api] = useSpring(() => ({ x: 85, y: 51 })) + const [position] = useState({ x: 85, y: 51 }) + + const ref = useRef() + useWheel( + ({ down, offset: [x, y] }, memo = position) => { + setActive && setActive(down) + api.start({ x: memo.x + x, y: memo.y + y, immediate: true }) + }, + { + preventDefault: true, + target: ref, + eventOptions: { passive: false }, + reverse: isReversed + } + ) + return ( + <> +
+ +
+
+ +
+ + ) +} + export function Rubberband({ setActive }) { const [{ x, y }, api] = useSpring(() => ({ x: 0, y: 0 })) const bind = useDrag( diff --git a/documentation/pages/docs/code/styles.module.css b/documentation/pages/docs/code/styles.module.css index e4b772da9..cf8278c0f 100644 --- a/documentation/pages/docs/code/styles.module.css +++ b/documentation/pages/docs/code/styles.module.css @@ -29,6 +29,13 @@ user-select: none; } +.wheel { + background: #91c9f9; + border-radius: 16px; + height: 80px; + width: 80px; +} + .overlay { position: fixed; top: 0; diff --git a/documentation/pages/docs/options.mdx b/documentation/pages/docs/options.mdx index 48f4f73ce..4e1c4fda2 100755 --- a/documentation/pages/docs/options.mdx +++ b/documentation/pages/docs/options.mdx @@ -131,6 +131,7 @@ Here are all options that can be applied to gestures. | [`swipe.duration`](#swipeduration) | **drag** | The maximum duration in milliseconds that a swipe is detected. | | `keyboardDisplacement` | **drag** | The distance (in `pixels`) emulated by arrow keys. Default is `10`. | | `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. | +| [`reverse`](#reverse) | **wheel** | If `true`, inverts the direction of wheel scrolling to mimic natural touchpad gestures. | ## Options explained @@ -435,6 +436,14 @@ On desktop, you should be able to drag the torus as you would expect without del 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. +### reverse + + + +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. + + + ### rubberband diff --git a/packages/core/src/config/wheelConfigResolver.ts b/packages/core/src/config/wheelConfigResolver.ts index b71c3f068..c58740ad4 100644 --- a/packages/core/src/config/wheelConfigResolver.ts +++ b/packages/core/src/config/wheelConfigResolver.ts @@ -1,3 +1,6 @@ import { coordinatesConfigResolver } from './coordinatesConfigResolver' -export const wheelConfigResolver = coordinatesConfigResolver +export const wheelConfigResolver = { + ...coordinatesConfigResolver, + reverse: (value = false) => value +} diff --git a/packages/core/src/engines/WheelEngine.ts b/packages/core/src/engines/WheelEngine.ts index 3721d7193..72d189cd4 100644 --- a/packages/core/src/engines/WheelEngine.ts +++ b/packages/core/src/engines/WheelEngine.ts @@ -20,8 +20,10 @@ export class WheelEngine extends CoordinatesEngine<'wheel'> { wheelChange(event: WheelEvent) { const state = this.state + const { reverse } = this.config + state._delta = wheelValues(event) - V.addTo(state._movement, state._delta) + reverse ? V.subTo(state._movement, state._delta) : V.addTo(state._movement, state._delta) // _movement rolls back to when it passed the bounds. clampStateInternalMovementToBounds(state) diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index d8fff0b7a..33e431551 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -140,6 +140,13 @@ export type MoveConfig = CoordinatesConfig<'move'> & MoveAndHoverMouseOnly export type HoverConfig = MoveAndHoverMouseOnly +export type WheelConfig = { + /** + * If true, inverts the direction of wheel scrolling to mimic natural touchpad gestures. + */ + reverse?: boolean +} + export type DragConfig = Omit, 'axisThreshold' | 'bounds'> & { /** * 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, 'axisThreshold' | 'boun export type UserDragConfig = GenericOptions & DragConfig export type UserPinchConfig = GenericOptions & PinchConfig -export type UserWheelConfig = GenericOptions & CoordinatesConfig<'wheel'> +export type UserWheelConfig = GenericOptions & WheelConfig & CoordinatesConfig<'wheel'> export type UserScrollConfig = GenericOptions & CoordinatesConfig<'scroll'> export type UserMoveConfig = GenericOptions & MoveConfig export type UserHoverConfig = GenericOptions & HoverConfig export type UserGestureConfig = GenericOptions & { drag?: DragConfig - wheel?: CoordinatesConfig<'wheel'> + wheel?: WheelConfig & CoordinatesConfig<'wheel'> scroll?: CoordinatesConfig<'scroll'> move?: MoveConfig pinch?: PinchConfig diff --git a/packages/core/src/types/internalConfig.ts b/packages/core/src/types/internalConfig.ts index 460038a8c..06e8cefde 100644 --- a/packages/core/src/types/internalConfig.ts +++ b/packages/core/src/types/internalConfig.ts @@ -1,4 +1,4 @@ -import { GestureKey, CoordinatesKey, ModifierKey } from './config' +import { GestureKey, CoordinatesKey, ModifierKey, WheelConfig } from './config' import { State } from './state' import { PointerType, Vector2 } from './utils' @@ -66,7 +66,7 @@ type MoveAndHoverMouseOnly = { export type InternalConfig = { shared: InternalGenericOptions drag?: InternalDragOptions - wheel?: InternalCoordinatesOptions<'wheel'> + wheel?: InternalCoordinatesOptions<'wheel'> & WheelConfig scroll?: InternalCoordinatesOptions<'scroll'> move?: InternalCoordinatesOptions<'move'> & MoveAndHoverMouseOnly hover?: InternalCoordinatesOptions<'hover'> & MoveAndHoverMouseOnly diff --git a/test/wheel.test.tsx b/test/wheel.test.tsx index 7d9e0620b..a3afc3484 100644 --- a/test/wheel.test.tsx +++ b/test/wheel.test.tsx @@ -94,6 +94,15 @@ describe.each([ expect(getByTestId(`${prefix}wheel-movement`)).toHaveTextContent('13,0') await waitFor(() => expect(getByTestId(`${prefix}wheel-wheeling`)).toHaveTextContent('false')) }) + + test('applying reverse SHOULD inverse wheel directions', async () => { + rerender() + fireEvent.wheel(element, { deltaX: -3, deltaY: 10 }) + fireEvent.wheel(element, { deltaX: 4, deltaY: -6 }) + expect(getByTestId(`${prefix}wheel-movement`)).toHaveTextContent('-1,-4') + await waitFor(() => expect(getByTestId(`${prefix}wheel-wheeling`)).toHaveTextContent('false')) + }) + test('disabling all gestures should prevent state from updating', async () => { rerender() fireEvent.wheel(element)