feat: Added swipe gestures for dismissing panes on mobile (#2201)
This commit is contained in:
BIN
.yarn/cache/contactjs-npm-2.1.5-c7bb4524ec-5be3d66835.zip
vendored
Normal file
BIN
.yarn/cache/contactjs-npm-2.1.5-c7bb4524ec-5be3d66835.zip
vendored
Normal file
Binary file not shown.
@@ -117,6 +117,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lexical/headless": "^0.7.7",
|
"@lexical/headless": "^0.7.7",
|
||||||
"@simplewebauthn/browser": "^7.0.0"
|
"@simplewebauthn/browser": "^7.0.0",
|
||||||
|
"contactjs": "2.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalCo
|
|||||||
import { PaneController } from '@/Controllers/PaneController/PaneController'
|
import { PaneController } from '@/Controllers/PaneController/PaneController'
|
||||||
import EmptyFilesView from './EmptyFilesView'
|
import EmptyFilesView from './EmptyFilesView'
|
||||||
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
|
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
|
||||||
|
import { usePaneSwipeGesture } from '../Panes/usePaneGesture'
|
||||||
|
import { mergeRefs } from '@/Hooks/mergeRefs'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
accountMenuController: AccountMenuController
|
accountMenuController: AccountMenuController
|
||||||
@@ -303,12 +305,14 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
|||||||
}
|
}
|
||||||
}, [selectedUuids, innerRef, isCurrentNoteTemplate, renderedItems, panes])
|
}, [selectedUuids, innerRef, isCurrentNoteTemplate, renderedItems, panes])
|
||||||
|
|
||||||
|
const [setElement] = usePaneSwipeGesture('right', () => setPaneLayout(PaneLayout.TagSelection))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={id}
|
id={id}
|
||||||
className={classNames(className, 'sn-component section h-full overflow-hidden pt-safe-top')}
|
className={classNames(className, 'sn-component section h-full overflow-hidden pt-safe-top')}
|
||||||
aria-label={'Notes & Files'}
|
aria-label={'Notes & Files'}
|
||||||
ref={innerRef}
|
ref={mergeRefs([innerRef, setElement])}
|
||||||
>
|
>
|
||||||
{isMobileScreen && (
|
{isMobileScreen && (
|
||||||
<FloatingAddButton onClick={addNewItem} label={addButtonLabel} style={dailyMode ? 'danger' : 'info'} />
|
<FloatingAddButton onClick={addNewItem} label={addButtonLabel} style={dailyMode ? 'danger' : 'info'} />
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
|
||||||
|
import { mergeRefs } from '@/Hooks/mergeRefs'
|
||||||
|
import { ForwardedRef, forwardRef } from 'react'
|
||||||
|
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||||
|
import { usePaneSwipeGesture } from '../Panes/usePaneGesture'
|
||||||
|
import NoteGroupView from './NoteGroupView'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication
|
||||||
|
className?: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditorPane = forwardRef(({ application, className, id }: Props, ref: ForwardedRef<HTMLDivElement>) => {
|
||||||
|
const { setPaneLayout } = useResponsiveAppPane()
|
||||||
|
|
||||||
|
const [setElement] = usePaneSwipeGesture('right', () => {
|
||||||
|
setPaneLayout(PaneLayout.ItemSelection)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
ref={mergeRefs([ref, setElement])}
|
||||||
|
className={`flex h-full flex-grow flex-col bg-default pt-safe-top ${className}`}
|
||||||
|
>
|
||||||
|
<NoteGroupView className={className} application={application} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default EditorPane
|
||||||
@@ -21,8 +21,6 @@ type State = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
className?: string
|
className?: string
|
||||||
innerRef: (ref: HTMLDivElement) => void
|
|
||||||
id: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoteGroupView extends AbstractComponent<Props, State> {
|
class NoteGroupView extends AbstractComponent<Props, State> {
|
||||||
@@ -99,11 +97,7 @@ class NoteGroupView extends AbstractComponent<Props, State> {
|
|||||||
const hasControllers = this.state.controllers.length > 0
|
const hasControllers = this.state.controllers.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
id={this.props.id}
|
|
||||||
className={`flex h-full flex-grow flex-col pt-safe-top ${this.props.className}`}
|
|
||||||
ref={this.props.innerRef}
|
|
||||||
>
|
|
||||||
{this.state.showMultipleSelectedNotes && (
|
{this.state.showMultipleSelectedNotes && (
|
||||||
<MultipleSelectedNotes
|
<MultipleSelectedNotes
|
||||||
application={this.application}
|
application={this.application}
|
||||||
@@ -138,7 +132,7 @@ class NoteGroupView extends AbstractComponent<Props, State> {
|
|||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { observer } from 'mobx-react-lite'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { usePrevious } from '../ContentListView/Calendar/usePrevious'
|
import { usePrevious } from '../ContentListView/Calendar/usePrevious'
|
||||||
import ContentListView from '../ContentListView/ContentListView'
|
import ContentListView from '../ContentListView/ContentListView'
|
||||||
import NoteGroupView from '../NoteGroupView/NoteGroupView'
|
|
||||||
import PanelResizer, { PanelResizeType, PanelSide, ResizeFinishCallback } from '../PanelResizer/PanelResizer'
|
import PanelResizer, { PanelResizeType, PanelSide, ResizeFinishCallback } from '../PanelResizer/PanelResizer'
|
||||||
import { AppPaneId, AppPaneIdToDivId } from './AppPaneMetadata'
|
import { AppPaneId, AppPaneIdToDivId } from './AppPaneMetadata'
|
||||||
import { useResponsiveAppPane } from './ResponsivePaneProvider'
|
import { useResponsiveAppPane } from './ResponsivePaneProvider'
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
import { isPanesChangeLeafDismiss, isPanesChangePush } from '@/Controllers/PaneController/panesForLayout'
|
import { isPanesChangeLeafDismiss, isPanesChangePush } from '@/Controllers/PaneController/panesForLayout'
|
||||||
import { log, LoggingDomain } from '@/Logging'
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
import { useMediaQuery } from '@/Hooks/useMediaQuery'
|
import { useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||||
|
import EditorPane from '../NoteGroupView/EditorPane'
|
||||||
|
|
||||||
const NAVIGATION_PANEL_MIN_WIDTH = 48
|
const NAVIGATION_PANEL_MIN_WIDTH = 48
|
||||||
const ITEMS_PANEL_MIN_WIDTH = 200
|
const ITEMS_PANEL_MIN_WIDTH = 200
|
||||||
@@ -330,9 +330,9 @@ const PanesSystemComponent = () => {
|
|||||||
} else if (pane === AppPaneId.Editor) {
|
} else if (pane === AppPaneId.Editor) {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary key="editor-pane">
|
<ErrorBoundary key="editor-pane">
|
||||||
<NoteGroupView
|
<EditorPane
|
||||||
id={ElementIds.EditorColumn}
|
id={ElementIds.EditorColumn}
|
||||||
innerRef={(ref) => setEditorRef(ref)}
|
ref={setEditorRef}
|
||||||
className={className}
|
className={className}
|
||||||
application={application}
|
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]
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ import { isIOS } from '@/Utils'
|
|||||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||||
import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
||||||
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
|
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
|
||||||
|
import { usePaneSwipeGesture } from '../Panes/usePaneGesture'
|
||||||
|
import { mergeRefs } from '@/Hooks/mergeRefs'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -44,6 +46,15 @@ const Navigation = forwardRef<HTMLDivElement, Props>(({ application, className,
|
|||||||
})
|
})
|
||||||
}, [application])
|
}, [application])
|
||||||
|
|
||||||
|
const [setElement] = usePaneSwipeGesture(
|
||||||
|
'left',
|
||||||
|
(element) => {
|
||||||
|
setPaneLayout(PaneLayout.ItemSelection)
|
||||||
|
element.style.left = '0'
|
||||||
|
},
|
||||||
|
'swipe',
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={id}
|
id={id}
|
||||||
@@ -52,7 +63,7 @@ const Navigation = forwardRef<HTMLDivElement, Props>(({ application, className,
|
|||||||
'sn-component section pb-[50px] md:pb-0',
|
'sn-component section pb-[50px] md:pb-0',
|
||||||
'h-full max-h-full overflow-hidden pt-safe-top md:h-full md:max-h-full md:min-h-0',
|
'h-full max-h-full overflow-hidden pt-safe-top md:h-full md:max-h-full md:min-h-0',
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={mergeRefs([ref, setElement])}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="navigation-content"
|
id="navigation-content"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const useLongPressEvent = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
elementRef.current.addEventListener('pointerdown', createLongPressTimeout)
|
elementRef.current.addEventListener('pointerdown', createLongPressTimeout)
|
||||||
|
elementRef.current.addEventListener('pointermove', clearLongPressTimeout)
|
||||||
elementRef.current.addEventListener('pointercancel', clearLongPressTimeout)
|
elementRef.current.addEventListener('pointercancel', clearLongPressTimeout)
|
||||||
elementRef.current.addEventListener('pointerup', clearLongPressTimeout)
|
elementRef.current.addEventListener('pointerup', clearLongPressTimeout)
|
||||||
}, [clearLongPressTimeout, createLongPressTimeout, elementRef])
|
}, [clearLongPressTimeout, createLongPressTimeout, elementRef])
|
||||||
@@ -45,6 +46,7 @@ export const useLongPressEvent = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
elementRef.current.removeEventListener('pointerdown', createLongPressTimeout)
|
elementRef.current.removeEventListener('pointerdown', createLongPressTimeout)
|
||||||
|
elementRef.current.addEventListener('pointermove', clearLongPressTimeout)
|
||||||
elementRef.current.removeEventListener('pointercancel', clearLongPressTimeout)
|
elementRef.current.removeEventListener('pointercancel', clearLongPressTimeout)
|
||||||
elementRef.current.removeEventListener('pointerup', clearLongPressTimeout)
|
elementRef.current.removeEventListener('pointerup', clearLongPressTimeout)
|
||||||
}, [clearLongPressTimeout, createLongPressTimeout, elementRef])
|
}, [clearLongPressTimeout, createLongPressTimeout, elementRef])
|
||||||
|
|||||||
@@ -5307,6 +5307,7 @@ __metadata:
|
|||||||
autoprefixer: ^10.4.13
|
autoprefixer: ^10.4.13
|
||||||
babel-loader: ^9.1.0
|
babel-loader: ^9.1.0
|
||||||
circular-dependency-plugin: ^5.2.2
|
circular-dependency-plugin: ^5.2.2
|
||||||
|
contactjs: 2.1.5
|
||||||
copy-webpack-plugin: ^11.0.0
|
copy-webpack-plugin: ^11.0.0
|
||||||
css-loader: "*"
|
css-loader: "*"
|
||||||
dayjs: ^1.11.7
|
dayjs: ^1.11.7
|
||||||
@@ -9169,6 +9170,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"contactjs@npm:2.1.5":
|
||||||
|
version: 2.1.5
|
||||||
|
resolution: "contactjs@npm:2.1.5"
|
||||||
|
checksum: 5be3d66835e5a78a16abe6cdbf0c2f4a44471086e313ef8550c6747026cca9e7ff5144fdaaf6b4307cb296baf0fd394ca63758354e58099f1fde3c6756a390c3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"content-disposition@npm:0.5.4":
|
"content-disposition@npm:0.5.4":
|
||||||
version: 0.5.4
|
version: 0.5.4
|
||||||
resolution: "content-disposition@npm:0.5.4"
|
resolution: "content-disposition@npm:0.5.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user