Skip to content

Commit

Permalink
fix: context menu on longpress on ios safari (#1405)
Browse files Browse the repository at this point in the history
  • Loading branch information
amanharwara committed Aug 17, 2022
1 parent 13343c9 commit 818c306
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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">
Expand Down
38 changes: 38 additions & 0 deletions packages/web/src/javascripts/Hooks/useContextMenuEvent.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 59 additions & 0 deletions packages/web/src/javascripts/Hooks/useLongPress.tsx
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 6 additions & 4 deletions packages/web/src/javascripts/Utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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()
}
}

0 comments on commit 818c306

Please sign in to comment.