feat: Added swipe gestures for dismissing panes on mobile (#2201)
This commit is contained in:
@@ -7,7 +7,6 @@ import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { usePrevious } from '../ContentListView/Calendar/usePrevious'
|
||||
import ContentListView from '../ContentListView/ContentListView'
|
||||
import NoteGroupView from '../NoteGroupView/NoteGroupView'
|
||||
import PanelResizer, { PanelResizeType, PanelSide, ResizeFinishCallback } from '../PanelResizer/PanelResizer'
|
||||
import { AppPaneId, AppPaneIdToDivId } from './AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from './ResponsivePaneProvider'
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
import { isPanesChangeLeafDismiss, isPanesChangePush } from '@/Controllers/PaneController/panesForLayout'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import EditorPane from '../NoteGroupView/EditorPane'
|
||||
|
||||
const NAVIGATION_PANEL_MIN_WIDTH = 48
|
||||
const ITEMS_PANEL_MIN_WIDTH = 200
|
||||
@@ -330,9 +330,9 @@ const PanesSystemComponent = () => {
|
||||
} else if (pane === AppPaneId.Editor) {
|
||||
return (
|
||||
<ErrorBoundary key="editor-pane">
|
||||
<NoteGroupView
|
||||
<EditorPane
|
||||
id={ElementIds.EditorColumn}
|
||||
innerRef={(ref) => setEditorRef(ref)}
|
||||
ref={setEditorRef}
|
||||
className={className}
|
||||
application={application}
|
||||
/>
|
||||
|
||||
138
packages/web/src/javascripts/Components/Panes/usePaneGesture.ts
Normal file
138
packages/web/src/javascripts/Components/Panes/usePaneGesture.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useStateRef } from '@/Hooks/useStateRef'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Direction, Pan, PointerListener, type GestureEventData } from 'contactjs'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
|
||||
export const usePaneSwipeGesture = (
|
||||
direction: 'left' | 'right',
|
||||
onSwipeEnd: (element: HTMLElement) => void,
|
||||
gesture: 'pan' | 'swipe' = 'pan',
|
||||
) => {
|
||||
const overlayElementRef = useRef<HTMLElement | null>(null)
|
||||
const [element, setElement] = useState<HTMLElement | null>(null)
|
||||
|
||||
const onSwipeEndRef = useStateRef(onSwipeEnd)
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isMobileScreen) {
|
||||
return
|
||||
}
|
||||
|
||||
const panRecognizer = new Pan(element, {
|
||||
supportedDirections: direction === 'left' ? [Direction.Left] : [Direction.Right],
|
||||
})
|
||||
|
||||
const pointerListener = new PointerListener(element, {
|
||||
supportedGestures: [panRecognizer],
|
||||
})
|
||||
|
||||
function onPan(e: unknown) {
|
||||
const event = e as CustomEvent<GestureEventData>
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
const x = event.detail.global.deltaX
|
||||
requestElementUpdate(x)
|
||||
}
|
||||
|
||||
let ticking = false
|
||||
|
||||
function onPanEnd(e: unknown) {
|
||||
const event = e as CustomEvent<GestureEventData>
|
||||
if (ticking) {
|
||||
setTimeout(function () {
|
||||
onPanEnd(event)
|
||||
}, 100)
|
||||
} else {
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
if (direction === 'right' && event.detail.global.deltaX > 40) {
|
||||
onSwipeEndRef.current(element)
|
||||
} else if (direction === 'left' && event.detail.global.deltaX < -40) {
|
||||
onSwipeEndRef.current(element)
|
||||
} else {
|
||||
requestElementUpdate(0)
|
||||
}
|
||||
|
||||
overlayElementRef.current?.remove()
|
||||
}
|
||||
}
|
||||
|
||||
function requestElementUpdate(x: number) {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(function () {
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!overlayElementRef.current) {
|
||||
const overlayElement = document.createElement('div')
|
||||
overlayElement.style.position = 'fixed'
|
||||
overlayElement.style.top = '0'
|
||||
overlayElement.style.left = '0'
|
||||
overlayElement.style.width = '100%'
|
||||
overlayElement.style.height = '100%'
|
||||
overlayElement.style.pointerEvents = 'none'
|
||||
overlayElement.style.backgroundColor = '#000'
|
||||
overlayElement.style.opacity = '0'
|
||||
overlayElement.style.willChange = 'opacity'
|
||||
|
||||
element.before(overlayElement)
|
||||
overlayElementRef.current = overlayElement
|
||||
}
|
||||
|
||||
const currentLeft = parseInt(element.style.left || '0')
|
||||
const newLeft = direction === 'right' ? Math.max(x, 0) : Math.min(x, 0)
|
||||
element.style.left = `${newLeft}px`
|
||||
|
||||
const percent = Math.min(window.innerWidth / currentLeft / 10, 0.45)
|
||||
overlayElementRef.current.animate([{ opacity: percent }], {
|
||||
duration: 0,
|
||||
fill: 'forwards',
|
||||
})
|
||||
|
||||
ticking = false
|
||||
})
|
||||
|
||||
ticking = true
|
||||
}
|
||||
}
|
||||
|
||||
if (gesture === 'pan') {
|
||||
element.addEventListener('panleft', onPan)
|
||||
element.addEventListener('panright', onPan)
|
||||
element.addEventListener('panend', onPanEnd)
|
||||
} else {
|
||||
if (direction === 'left') {
|
||||
element.addEventListener('swipeleft', onPanEnd)
|
||||
} else {
|
||||
element.addEventListener('swiperight', onPanEnd)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
pointerListener.destroy()
|
||||
if (gesture === 'pan') {
|
||||
element.removeEventListener('panleft', onPan)
|
||||
element.removeEventListener('panright', onPan)
|
||||
element.removeEventListener('panend', onPanEnd)
|
||||
} else {
|
||||
if (direction === 'left') {
|
||||
element.removeEventListener('swipeleft', onPanEnd)
|
||||
} else {
|
||||
element.removeEventListener('swiperight', onPanEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [direction, element, gesture, isMobileScreen, onSwipeEndRef])
|
||||
|
||||
return [setElement]
|
||||
}
|
||||
Reference in New Issue
Block a user