Skip to content

Commit 0b04b34

Browse files
committed
file-explorer: attempt to fix ios long-tap
1 parent 6bea63d commit 0b04b34

File tree

4 files changed

+104
-14
lines changed

4 files changed

+104
-14
lines changed

hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
padding: 4px 0;
88
min-width: 160px;
99
z-index: 1000;
10+
/* Ensure context menu appears above iOS touch callouts */
11+
-webkit-touch-callout: none;
12+
-webkit-user-select: none;
13+
user-select: none;
1014
}
1115

1216
.context-menu button {

hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,18 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ position, file, onClose, onSh
2323
}
2424
};
2525

26+
const handleTouchOutside = (e: TouchEvent) => {
27+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
28+
onClose();
29+
}
30+
};
31+
2632
document.addEventListener('mousedown', handleClickOutside);
27-
return () => document.removeEventListener('mousedown', handleClickOutside);
33+
document.addEventListener('touchstart', handleTouchOutside);
34+
return () => {
35+
document.removeEventListener('mousedown', handleClickOutside);
36+
document.removeEventListener('touchstart', handleTouchOutside);
37+
};
2838
}, [onClose]);
2939

3040
const handleUnshare = async () => {

hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileItem.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
transition: background-color 0.2s;
99
color: var(--text-primary);
1010
position: relative;
11+
/* Prevent iOS default touch behaviors */
12+
-webkit-touch-callout: none;
13+
-webkit-tap-highlight-color: transparent;
1114
}
1215

1316
.file-item:hover {

hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileItem.tsx

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useRef, useEffect } from 'react';
22
import { FileInfo, deleteFile, deleteDirectory } from '../../lib/api';
33
import useFileExplorerStore from '../../store/fileExplorer';
44
import ContextMenu from '../ContextMenu/ContextMenu';
@@ -27,6 +27,11 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
2727
const [childrenLoaded, setChildrenLoaded] = useState(false);
2828
const [loadedChildren, setLoadedChildren] = useState<(FileInfo & { children?: FileInfo[] })[]>([]);
2929

30+
// Touch handling for iOS long-press
31+
const touchTimerRef = useRef<number | null>(null);
32+
const touchStartPos = useRef<{ x: number; y: number } | null>(null);
33+
const longPressTriggered = useRef(false);
34+
3035
const handleClick = (e: React.MouseEvent) => {
3136
if (e.ctrlKey || e.metaKey) {
3237
toggleFileSelection(file.path);
@@ -40,17 +45,17 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
4045
const buildTreeFromFlatList = (flatList: FileInfo[], parentPath: string): (FileInfo & { children?: FileInfo[] })[] => {
4146
const fileMap = new Map<string, FileInfo & { children?: FileInfo[] }>();
4247
const topLevelFiles: (FileInfo & { children?: FileInfo[] })[] = [];
43-
48+
4449
// First pass: create map of all files
4550
flatList.forEach(file => {
4651
fileMap.set(file.path, { ...file, children: [] });
4752
});
48-
53+
4954
// Second pass: build parent-child relationships
5055
flatList.forEach(file => {
5156
const fileWithChildren = fileMap.get(file.path)!;
5257
const fileParentPath = file.path.substring(0, file.path.lastIndexOf('/'));
53-
58+
5459
if (fileMap.has(fileParentPath)) {
5560
// This file has a parent in our list
5661
const parent = fileMap.get(fileParentPath)!;
@@ -61,7 +66,7 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
6166
topLevelFiles.push(fileWithChildren);
6267
}
6368
});
64-
69+
6570
// Sort files: directories first, then by name
6671
const sortFiles = (files: (FileInfo & { children?: FileInfo[] })[]) => {
6772
return [...files].sort((a, b) => {
@@ -70,7 +75,7 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
7075
return a.name.localeCompare(b.name);
7176
});
7277
};
73-
78+
7479
// Recursively sort all children
7580
const sortRecursive = (files: (FileInfo & { children?: FileInfo[] })[]) => {
7681
const sorted = sortFiles(files);
@@ -81,21 +86,21 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
8186
});
8287
return sorted;
8388
};
84-
89+
8590
return sortRecursive(topLevelFiles);
8691
};
8792

8893
const handleExpandToggle = async (e: React.MouseEvent) => {
8994
e.stopPropagation();
90-
95+
9196
// If expanding and we haven't loaded children yet, load them
9297
if (!isExpanded && file.isDirectory && !childrenLoaded && onLoadSubdirectory) {
9398
const flatChildren = await onLoadSubdirectory(file.path);
9499
const treeChildren = buildTreeFromFlatList(flatChildren, file.path);
95100
setLoadedChildren(treeChildren);
96101
setChildrenLoaded(true);
97102
}
98-
103+
99104
setIsExpanded(!isExpanded);
100105
};
101106

@@ -105,9 +110,74 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
105110
setContextMenuOpen(true);
106111
};
107112

113+
// Touch event handlers for iOS compatibility
114+
const handleTouchStart = (e: React.TouchEvent) => {
115+
const touch = e.touches[0];
116+
touchStartPos.current = { x: touch.clientX, y: touch.clientY };
117+
longPressTriggered.current = false;
118+
119+
// Start long press timer (500ms)
120+
touchTimerRef.current = window.setTimeout(() => {
121+
if (touchStartPos.current) {
122+
longPressTriggered.current = true;
123+
// Trigger context menu
124+
setContextMenuPosition({ x: touchStartPos.current.x, y: touchStartPos.current.y });
125+
setContextMenuOpen(true);
126+
// Prevent default touch behavior
127+
e.preventDefault();
128+
}
129+
}, 500);
130+
};
131+
132+
const handleTouchMove = (e: React.TouchEvent) => {
133+
// If the touch moves more than 10px, cancel the long press
134+
if (touchStartPos.current && touchTimerRef.current) {
135+
const touch = e.touches[0];
136+
const deltaX = Math.abs(touch.clientX - touchStartPos.current.x);
137+
const deltaY = Math.abs(touch.clientY - touchStartPos.current.y);
138+
139+
if (deltaX > 10 || deltaY > 10) {
140+
if (touchTimerRef.current) {
141+
clearTimeout(touchTimerRef.current);
142+
touchTimerRef.current = null;
143+
}
144+
}
145+
}
146+
};
147+
148+
const handleTouchEnd = (e: React.TouchEvent) => {
149+
// Clear the timer
150+
if (touchTimerRef.current) {
151+
clearTimeout(touchTimerRef.current);
152+
touchTimerRef.current = null;
153+
}
154+
155+
// If long press was triggered, prevent default click behavior
156+
if (longPressTriggered.current) {
157+
e.preventDefault();
158+
longPressTriggered.current = false;
159+
} else if (!e.defaultPrevented) {
160+
// Normal tap - handle as click
161+
if (file.isDirectory) {
162+
onNavigate(file.path);
163+
}
164+
}
165+
166+
touchStartPos.current = null;
167+
};
168+
169+
// Clean up timer on unmount
170+
useEffect(() => {
171+
return () => {
172+
if (touchTimerRef.current) {
173+
clearTimeout(touchTimerRef.current);
174+
}
175+
};
176+
}, []);
177+
108178
const handleDelete = async () => {
109179
if (!confirm(`Delete ${file.name}?`)) return;
110-
180+
111181
try {
112182
if (file.isDirectory) {
113183
await deleteDirectory(file.path);
@@ -164,9 +234,12 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
164234
className={`file-item file-item-${viewMode} ${isSelected ? 'selected' : ''}`}
165235
onClick={handleClick}
166236
onContextMenu={handleContextMenu}
237+
onTouchStart={handleTouchStart}
238+
onTouchMove={handleTouchMove}
239+
onTouchEnd={handleTouchEnd}
167240
style={{ paddingLeft: `${depth * 20 + 10}px` }}
168241
>
169-
<span
242+
<span
170243
className={`file-icon ${file.isDirectory && viewMode === 'list' ? 'clickable-folder' : ''}`}
171244
onClick={file.isDirectory && viewMode === 'list' ? handleExpandToggle : undefined}
172245
>
@@ -189,7 +262,7 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
189262
</>
190263
)}
191264
</div>
192-
265+
193266
{/* Render children when expanded */}
194267
{isExpanded && viewMode === 'list' && childrenToRender.length > 0 && (
195268
<div className="file-children">
@@ -230,4 +303,4 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
230303
);
231304
};
232305

233-
export default FileItem;
306+
export default FileItem;

0 commit comments

Comments
 (0)