fix: context menu on longpress on ios safari (#1405)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback } from 'react'
|
||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||
import { getFileIconComponent } from '../AttachedFilesPopover/getFileIconComponent'
|
||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||
import ListItemFlagIcons from './ListItemFlagIcons'
|
||||
@@ -9,6 +9,7 @@ import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||
|
||||
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
@@ -24,8 +25,11 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
}) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const listItemRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const openFileContextMenu = useCallback(
|
||||
(posX: number, posY: number) => {
|
||||
filesController.setShowFileContextMenu(false)
|
||||
filesController.setFileContextMenuLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
@@ -66,17 +70,16 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
'w-5 h-5 flex-shrink-0',
|
||||
)
|
||||
|
||||
useContextMenuEvent(listItemRef, openContextMenu)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={listItemRef}
|
||||
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
||||
selected && 'selected border-l-2px border-solid border-info'
|
||||
}`}
|
||||
id={item.uuid}
|
||||
onClick={onClick}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
void openContextMenu(event.clientX, event.clientY)
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="mr-0 flex flex-col items-center justify-between p-4.5 pr-3">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||
import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback } from 'react'
|
||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||
import ListItemFlagIcons from './ListItemFlagIcons'
|
||||
@@ -10,6 +10,7 @@ import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||
|
||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
@@ -26,12 +27,15 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
}) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const listItemRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
||||
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
||||
const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0
|
||||
|
||||
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||
notesController.setContextMenuOpen(false)
|
||||
notesController.setContextMenuClickLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
@@ -62,17 +66,16 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
}
|
||||
}, [item.uuid, selectionController, toggleAppPane])
|
||||
|
||||
useContextMenuEvent(listItemRef, openContextMenu)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={listItemRef}
|
||||
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
||||
selected && 'selected border-l-2 border-solid border-info'
|
||||
}`}
|
||||
id={item.uuid}
|
||||
onClick={onClick}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
void openContextMenu(event.clientX, event.clientY)
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="mr-0 flex flex-col items-center justify-between p-4 pr-4">
|
||||
|
||||
38
packages/web/src/javascripts/Hooks/useContextMenuEvent.tsx
Normal file
38
packages/web/src/javascripts/Hooks/useContextMenuEvent.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { isIOS } from '@/Utils'
|
||||
import { RefObject, useCallback, useEffect } from 'react'
|
||||
import { useLongPressEvent } from './useLongPress'
|
||||
|
||||
export const useContextMenuEvent = (elementRef: RefObject<HTMLElement>, listener: (x: number, y: number) => void) => {
|
||||
const { attachEvents, cleanupEvents } = useLongPressEvent(elementRef, listener)
|
||||
|
||||
const handleContextMenuEvent = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
listener(event.clientX, event.clientY)
|
||||
},
|
||||
[listener],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current
|
||||
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
const shouldUseLongPress = isIOS()
|
||||
|
||||
element.addEventListener('contextmenu', handleContextMenuEvent)
|
||||
|
||||
if (shouldUseLongPress) {
|
||||
attachEvents()
|
||||
}
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('contextmenu', handleContextMenuEvent)
|
||||
if (shouldUseLongPress) {
|
||||
cleanupEvents()
|
||||
}
|
||||
}
|
||||
}, [attachEvents, cleanupEvents, elementRef, handleContextMenuEvent, listener])
|
||||
}
|
||||
59
packages/web/src/javascripts/Hooks/useLongPress.tsx
Normal file
59
packages/web/src/javascripts/Hooks/useLongPress.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { RefObject, useCallback, useMemo, useRef } from 'react'
|
||||
|
||||
// According to https://reactnative.dev/docs/touchablewithoutfeedback#onlongpress
|
||||
const ReactNativeLongpressDelay = 370
|
||||
|
||||
export const useLongPressEvent = (
|
||||
elementRef: RefObject<HTMLElement>,
|
||||
listener: (x: number, y: number) => void,
|
||||
delay = ReactNativeLongpressDelay,
|
||||
) => {
|
||||
const longPressTimeout = useRef<number>()
|
||||
|
||||
const clearLongPressTimeout = useCallback(() => {
|
||||
if (longPressTimeout.current) {
|
||||
clearTimeout(longPressTimeout.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const createLongPressTimeout = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
clearLongPressTimeout()
|
||||
longPressTimeout.current = window.setTimeout(() => {
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
|
||||
listener(x, y)
|
||||
}, delay)
|
||||
},
|
||||
[clearLongPressTimeout, delay, listener],
|
||||
)
|
||||
|
||||
const attachEvents = useCallback(() => {
|
||||
if (!elementRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
elementRef.current.addEventListener('pointerdown', createLongPressTimeout)
|
||||
elementRef.current.addEventListener('pointercancel', clearLongPressTimeout)
|
||||
}, [clearLongPressTimeout, createLongPressTimeout, elementRef])
|
||||
|
||||
const cleanupEvents = useCallback(() => {
|
||||
if (!elementRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
elementRef.current.removeEventListener('pointerdown', createLongPressTimeout)
|
||||
elementRef.current.removeEventListener('pointercancel', clearLongPressTimeout)
|
||||
}, [clearLongPressTimeout, createLongPressTimeout, elementRef])
|
||||
|
||||
const memoizedReturn = useMemo(
|
||||
() => ({
|
||||
attachEvents,
|
||||
cleanupEvents,
|
||||
}),
|
||||
[attachEvents, cleanupEvents],
|
||||
)
|
||||
|
||||
return memoizedReturn
|
||||
}
|
||||
@@ -172,6 +172,11 @@ export const convertStringifiedBooleanToBoolean = (value: string) => {
|
||||
return value !== 'false'
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885#9039885
|
||||
export const isIOS = () =>
|
||||
(/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) ||
|
||||
(navigator.userAgent.includes('Mac') && 'ontouchend' in document && navigator.maxTouchPoints > 1)
|
||||
|
||||
// https://stackoverflow.com/a/57527009/2504429
|
||||
export const disableIosTextFieldZoom = () => {
|
||||
const addMaximumScaleToMetaViewport = () => {
|
||||
@@ -194,10 +199,7 @@ export const disableIosTextFieldZoom = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885#9039885
|
||||
const checkIsIOS = () => /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
|
||||
|
||||
if (checkIsIOS()) {
|
||||
if (isIOS()) {
|
||||
addMaximumScaleToMetaViewport()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user