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:
@@ -224,6 +224,7 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId
|
|||||||
const ownerId = spaces.find(s => s.id === currentSpaceId)?.ownerId;
|
const ownerId = spaces.find(s => s.id === currentSpaceId)?.ownerId;
|
||||||
|
|
||||||
const getMemberDisplayColor = (userId: string) => {
|
const getMemberDisplayColor = (userId: string) => {
|
||||||
|
if (isDmMessage) return { color: '#d8d8de' };
|
||||||
const member = members.find(m => m.userId === userId);
|
const member = members.find(m => m.userId === userId);
|
||||||
if (member?.roles && member.roles.length > 0) {
|
if (member?.roles && member.roles.length > 0) {
|
||||||
const sorted = [...member.roles].sort((a, b) => b.position - a.position);
|
const sorted = [...member.roles].sort((a, b) => b.position - a.position);
|
||||||
@@ -524,7 +525,7 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onContextMenu={handleContextMenu}>
|
<div data-context-menu onContextMenu={handleContextMenu}>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export function MobileDmsScreen() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={dm.id}
|
key={dm.id}
|
||||||
|
data-context-menu
|
||||||
onClick={() => handleDmTap(dm.id)}
|
onClick={() => handleDmTap(dm.id)}
|
||||||
onContextMenu={(e) => handleDmContextMenu(e, dm.id, isGroup)}
|
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"
|
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" />
|
<div className="w-10 h-1 bg-txt-tertiary/30 rounded-full mx-auto mt-2 mb-1 shrink-0" />
|
||||||
|
|
||||||
{/* Folder header */}
|
{/* 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
|
<div
|
||||||
className="w-5 h-5 rounded"
|
className="w-5 h-5 rounded"
|
||||||
style={{ background: folder.color || 'rgb(var(--text-tertiary))' }}
|
style={{ background: folder.color || 'rgb(var(--text-tertiary))' }}
|
||||||
|
|||||||
@@ -669,6 +669,7 @@ export function MobileSpacesScreen() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
data-context-menu
|
||||||
onClick={() => handleSpaceSelect(space.id)}
|
onClick={() => handleSpaceSelect(space.id)}
|
||||||
onContextMenu={(e) => handleSpaceContextMenu(e, space.id)}
|
onContextMenu={(e) => handleSpaceContextMenu(e, space.id)}
|
||||||
className={`w-10 h-10 rounded-2xl overflow-hidden flex items-center justify-center transition-all ${
|
className={`w-10 h-10 rounded-2xl overflow-hidden flex items-center justify-center transition-all ${
|
||||||
@@ -765,6 +766,7 @@ export function MobileSpacesScreen() {
|
|||||||
return (
|
return (
|
||||||
<div key={category.id} className="mt-3">
|
<div key={category.id} className="mt-3">
|
||||||
<button
|
<button
|
||||||
|
data-context-menu
|
||||||
onClick={() => toggleCategory(category.id)}
|
onClick={() => toggleCategory(category.id)}
|
||||||
onContextMenu={(e) => handleCategoryContextMenu(e, category.id)}
|
onContextMenu={(e) => handleCategoryContextMenu(e, category.id)}
|
||||||
className="flex items-center gap-1 px-1 py-1 w-full text-left"
|
className="flex items-center gap-1 px-1 py-1 w-full text-left"
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ export function MobileVoiceFullScreen() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={userId}
|
key={userId}
|
||||||
|
data-context-menu
|
||||||
className={`rounded-xl bg-surface-channel p-4 flex flex-col items-center gap-3 ${
|
className={`rounded-xl bg-surface-channel p-4 flex flex-col items-center gap-3 ${
|
||||||
participantIds.length <= 2 ? 'py-8' : 'py-4'
|
participantIds.length <= 2 ? 'py-8' : 'py-4'
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -550,6 +550,10 @@ function useGlobalLongPress(isMobile: boolean) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onTouchStart = (e: TouchEvent) => {
|
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; }
|
if (e.touches.length !== 1) { cancel(); return; }
|
||||||
const touch = e.touches[0]!;
|
const touch = e.touches[0]!;
|
||||||
originX = touch.clientX;
|
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('touchmove', onTouchMove, { passive: true });
|
||||||
document.addEventListener('touchend', onTouchEnd, { passive: true });
|
document.addEventListener('touchend', onTouchEnd, { passive: true });
|
||||||
document.addEventListener('touchcancel', onTouchEnd, { passive: true });
|
document.addEventListener('touchcancel', onTouchEnd, { passive: true });
|
||||||
|
|||||||
@@ -99,13 +99,18 @@
|
|||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Suppress native long-press callout on mobile (iOS "Copy/Lookup" popup).
|
/* Suppress native text selection and callout on mobile interactive elements.
|
||||||
Our global useLongPress handler provides context menus instead.
|
Our global useGlobalLongPress handler provides context menus instead.
|
||||||
Text can still be copied via context menu "Copy Text" option. */
|
Text can still be copied via context menu "Copy Text" option. */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
* {
|
* {
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button, [role="button"], [data-context-menu] {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Thin scrollbar */
|
/* Thin scrollbar */
|
||||||
|
|||||||
Reference in New Issue
Block a user