fix: context menu on longpress on ios safari (#1405)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { FileItem } from '@standardnotes/snjs'
|
import { FileItem } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent, useCallback } from 'react'
|
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||||
import { getFileIconComponent } from '../AttachedFilesPopover/getFileIconComponent'
|
import { getFileIconComponent } from '../AttachedFilesPopover/getFileIconComponent'
|
||||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||||
import ListItemFlagIcons from './ListItemFlagIcons'
|
import ListItemFlagIcons from './ListItemFlagIcons'
|
||||||
@@ -9,6 +9,7 @@ import ListItemMetadata from './ListItemMetadata'
|
|||||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||||
|
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||||
|
|
||||||
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||||
application,
|
application,
|
||||||
@@ -24,8 +25,11 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { toggleAppPane } = useResponsiveAppPane()
|
const { toggleAppPane } = useResponsiveAppPane()
|
||||||
|
|
||||||
|
const listItemRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const openFileContextMenu = useCallback(
|
const openFileContextMenu = useCallback(
|
||||||
(posX: number, posY: number) => {
|
(posX: number, posY: number) => {
|
||||||
|
filesController.setShowFileContextMenu(false)
|
||||||
filesController.setFileContextMenuLocation({
|
filesController.setFileContextMenuLocation({
|
||||||
x: posX,
|
x: posX,
|
||||||
y: posY,
|
y: posY,
|
||||||
@@ -66,17 +70,16 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
'w-5 h-5 flex-shrink-0',
|
'w-5 h-5 flex-shrink-0',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useContextMenuEvent(listItemRef, openContextMenu)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={listItemRef}
|
||||||
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
||||||
selected && 'selected border-l-2px border-solid border-info'
|
selected && 'selected border-l-2px border-solid border-info'
|
||||||
}`}
|
}`}
|
||||||
id={item.uuid}
|
id={item.uuid}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onContextMenu={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
void openContextMenu(event.clientX, event.clientY)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{!hideIcon ? (
|
{!hideIcon ? (
|
||||||
<div className="mr-0 flex flex-col items-center justify-between p-4.5 pr-3">
|
<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 { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||||
import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
|
import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent, useCallback } from 'react'
|
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||||
import ListItemFlagIcons from './ListItemFlagIcons'
|
import ListItemFlagIcons from './ListItemFlagIcons'
|
||||||
@@ -10,6 +10,7 @@ import ListItemMetadata from './ListItemMetadata'
|
|||||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||||
|
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||||
|
|
||||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||||
application,
|
application,
|
||||||
@@ -26,12 +27,15 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { toggleAppPane } = useResponsiveAppPane()
|
const { toggleAppPane } = useResponsiveAppPane()
|
||||||
|
|
||||||
|
const listItemRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
||||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
||||||
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
||||||
const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0
|
const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0
|
||||||
|
|
||||||
const openNoteContextMenu = (posX: number, posY: number) => {
|
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||||
|
notesController.setContextMenuOpen(false)
|
||||||
notesController.setContextMenuClickLocation({
|
notesController.setContextMenuClickLocation({
|
||||||
x: posX,
|
x: posX,
|
||||||
y: posY,
|
y: posY,
|
||||||
@@ -62,17 +66,16 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
}
|
}
|
||||||
}, [item.uuid, selectionController, toggleAppPane])
|
}, [item.uuid, selectionController, toggleAppPane])
|
||||||
|
|
||||||
|
useContextMenuEvent(listItemRef, openContextMenu)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={listItemRef}
|
||||||
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
||||||
selected && 'selected border-l-2 border-solid border-info'
|
selected && 'selected border-l-2 border-solid border-info'
|
||||||
}`}
|
}`}
|
||||||
id={item.uuid}
|
id={item.uuid}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onContextMenu={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
void openContextMenu(event.clientX, event.clientY)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{!hideIcon ? (
|
{!hideIcon ? (
|
||||||
<div className="mr-0 flex flex-col items-center justify-between p-4 pr-4">
|
<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'
|
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
|
// https://stackoverflow.com/a/57527009/2504429
|
||||||
export const disableIosTextFieldZoom = () => {
|
export const disableIosTextFieldZoom = () => {
|
||||||
const addMaximumScaleToMetaViewport = () => {
|
const addMaximumScaleToMetaViewport = () => {
|
||||||
@@ -194,10 +199,7 @@ export const disableIosTextFieldZoom = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885#9039885
|
if (isIOS()) {
|
||||||
const checkIsIOS = () => /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
|
|
||||||
|
|
||||||
if (checkIsIOS()) {
|
|
||||||
addMaximumScaleToMetaViewport()
|
addMaximumScaleToMetaViewport()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user