fix: prevent native text selection on mobile long-press

iOS triggered both the app context menu and native text selection overlay
on long-press. Three-layer fix: CSS user-select:none on buttons/[role=button]/
[data-context-menu] elements; non-passive touchstart listener with conditional
preventDefault on context-menu targets; removeAllRanges() at timer fire to
clear any residual selection. Added data-context-menu to all mobile surfaces
that use onContextMenu handlers.
This commit is contained in:
Jannis Braun
2026-03-23 21:12:06 +01:00
parent 161e876df8
commit 9f8a4ffa90
7 changed files with 19 additions and 5 deletions
+2 -1
View File
@@ -224,6 +224,7 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId
const ownerId = spaces.find(s => s.id === currentSpaceId)?.ownerId;
const getMemberDisplayColor = (userId: string) => {
if (isDmMessage) return { color: '#d8d8de' };
const member = members.find(m => m.userId === userId);
if (member?.roles && member.roles.length > 0) {
const sorted = [...member.roles].sort((a, b) => b.position - a.position);
@@ -524,7 +525,7 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId
);
return (
<div onContextMenu={handleContextMenu}>
<div data-context-menu onContextMenu={handleContextMenu}>
{content}
</div>
);
@@ -165,6 +165,7 @@ export function MobileDmsScreen() {
return (
<button
key={dm.id}
data-context-menu
onClick={() => handleDmTap(dm.id)}
onContextMenu={(e) => handleDmContextMenu(e, dm.id, isGroup)}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-interactive-hover text-left transition-colors"
@@ -94,7 +94,7 @@ export function MobileFolderSheet({ folder, onClose, onSelectSpace, onUpdateFold
<div className="w-10 h-1 bg-txt-tertiary/30 rounded-full mx-auto mt-2 mb-1 shrink-0" />
{/* Folder header */}
<div className="px-4 py-2 flex items-center gap-2 shrink-0" onContextMenu={handleFolderContextMenu}>
<div className="px-4 py-2 flex items-center gap-2 shrink-0" data-context-menu onContextMenu={handleFolderContextMenu}>
<div
className="w-5 h-5 rounded"
style={{ background: folder.color || 'rgb(var(--text-tertiary))' }}
@@ -669,6 +669,7 @@ export function MobileSpacesScreen() {
/>
</div>
<button
data-context-menu
onClick={() => handleSpaceSelect(space.id)}
onContextMenu={(e) => handleSpaceContextMenu(e, space.id)}
className={`w-10 h-10 rounded-2xl overflow-hidden flex items-center justify-center transition-all ${
@@ -765,6 +766,7 @@ export function MobileSpacesScreen() {
return (
<div key={category.id} className="mt-3">
<button
data-context-menu
onClick={() => toggleCategory(category.id)}
onContextMenu={(e) => handleCategoryContextMenu(e, category.id)}
className="flex items-center gap-1 px-1 py-1 w-full text-left"
@@ -198,6 +198,7 @@ export function MobileVoiceFullScreen() {
return (
<div
key={userId}
data-context-menu
className={`rounded-xl bg-surface-channel p-4 flex flex-col items-center gap-3 ${
participantIds.length <= 2 ? 'py-8' : 'py-4'
}`}
@@ -550,6 +550,10 @@ function useGlobalLongPress(isMobile: boolean) {
};
const onTouchStart = (e: TouchEvent) => {
const target = e.target as HTMLElement;
if (target.closest('[data-context-menu]')) {
e.preventDefault();
}
if (e.touches.length !== 1) { cancel(); return; }
const touch = e.touches[0]!;
originX = touch.clientX;
@@ -600,7 +604,7 @@ function useGlobalLongPress(isMobile: boolean) {
}
};
document.addEventListener('touchstart', onTouchStart, { passive: true });
document.addEventListener('touchstart', onTouchStart, { passive: false });
document.addEventListener('touchmove', onTouchMove, { passive: true });
document.addEventListener('touchend', onTouchEnd, { passive: true });
document.addEventListener('touchcancel', onTouchEnd, { passive: true });
+7 -2
View File
@@ -99,13 +99,18 @@
text-rendering: optimizeLegibility;
}
/* Suppress native long-press callout on mobile (iOS "Copy/Lookup" popup).
Our global useLongPress handler provides context menus instead.
/* Suppress native text selection and callout on mobile interactive elements.
Our global useGlobalLongPress handler provides context menus instead.
Text can still be copied via context menu "Copy Text" option. */
@media (max-width: 767px) {
* {
-webkit-touch-callout: none;
}
button, [role="button"], [data-context-menu] {
-webkit-user-select: none;
user-select: none;
}
}
/* Thin scrollbar */