fix(touch): harden long-press for iOS (#199)
CI / Build & Quality Checks (pull_request) Successful in 2m57s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 18m1s
CI / Build & Quality Checks (pull_request) Successful in 2m57s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 18m1s
Long-press opens the message action sheet (#166), but on iOS the system takes a long-press for itself: the link/image callout or the text-selection loupe races the 450 ms press. Message rows on touch screens now carry data-touch-actions (no -webkit-touch-callout), and the pressed row carries data-pressing from touch-down until the finger lifts, which turns text selection off for that moment only. Copy Text stays in the sheet. Refs #199 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ccbafdb147
commit
f920fd82fb
@@ -1,4 +1,4 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
import { DefaultReset, FocusOutline, color, config, toRem } from 'folds';
|
import { DefaultReset, FocusOutline, color, config, toRem } from 'folds';
|
||||||
|
|
||||||
export const MessageBase = style({
|
export const MessageBase = style({
|
||||||
@@ -77,3 +77,14 @@ export const ReactionsMore = style([
|
|||||||
export const ReactionsTooltipText = style({
|
export const ReactionsTooltipText = style({
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// [Gitea #199] Long-press opens the message action sheet on touch screens. On
|
||||||
|
// iOS a long-press otherwise belongs to the system: a link/image callout, or
|
||||||
|
// the text-selection loupe, both of which race (and win against) the 450 ms
|
||||||
|
// press. No callout on message rows, and no selection while a finger is
|
||||||
|
// down; copying text is in the sheet.
|
||||||
|
globalStyle('[data-touch-actions]', { WebkitTouchCallout: 'none' });
|
||||||
|
globalStyle('[data-touch-actions][data-pressing], [data-touch-actions][data-pressing] *', {
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
});
|
||||||
|
|||||||
@@ -17,22 +17,37 @@ export function useLongPress(onLongPress: (x: number, y: number) => void) {
|
|||||||
const timer = useRef<number | undefined>(undefined);
|
const timer = useRef<number | undefined>(undefined);
|
||||||
const start = useRef<{ x: number; y: number } | null>(null);
|
const start = useRef<{ x: number; y: number } | null>(null);
|
||||||
const fired = useRef(false);
|
const fired = useRef(false);
|
||||||
|
// [Gitea #199] The element being pressed carries `data-pressing` (set on the
|
||||||
|
// DOM directly, no re-render) so iOS can't start a text selection under
|
||||||
|
// the finger; see `[data-touch-actions]` in room/message/styles.css.ts.
|
||||||
|
const pressed = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
const stopTimer = useCallback(() => {
|
||||||
if (timer.current !== undefined) window.clearTimeout(timer.current);
|
if (timer.current !== undefined) window.clearTimeout(timer.current);
|
||||||
timer.current = undefined;
|
timer.current = undefined;
|
||||||
start.current = null;
|
start.current = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// The finger is up (or the gesture was cancelled): only now drop
|
||||||
|
// `data-pressing`, so the system selection can't start under a finger that
|
||||||
|
// is still down after the sheet opened.
|
||||||
|
const clear = useCallback(() => {
|
||||||
|
stopTimer();
|
||||||
|
pressed.current?.removeAttribute('data-pressing');
|
||||||
|
pressed.current = null;
|
||||||
|
}, [stopTimer]);
|
||||||
|
|
||||||
const onTouchStart = useCallback(
|
const onTouchStart = useCallback(
|
||||||
(evt: React.TouchEvent) => {
|
(evt: React.TouchEvent) => {
|
||||||
if (!coarse || evt.touches.length !== 1) return;
|
if (!coarse || evt.touches.length !== 1) return;
|
||||||
const t = evt.touches[0];
|
const t = evt.touches[0];
|
||||||
start.current = { x: t.clientX, y: t.clientY };
|
start.current = { x: t.clientX, y: t.clientY };
|
||||||
fired.current = false;
|
fired.current = false;
|
||||||
|
pressed.current = evt.currentTarget as HTMLElement;
|
||||||
|
pressed.current.setAttribute('data-pressing', '');
|
||||||
timer.current = window.setTimeout(() => {
|
timer.current = window.setTimeout(() => {
|
||||||
const s = start.current;
|
const s = start.current;
|
||||||
clear();
|
stopTimer();
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
fired.current = true;
|
fired.current = true;
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@@ -41,7 +56,7 @@ export function useLongPress(onLongPress: (x: number, y: number) => void) {
|
|||||||
onLongPress(s.x, s.y);
|
onLongPress(s.x, s.y);
|
||||||
}, LONG_PRESS_MS);
|
}, LONG_PRESS_MS);
|
||||||
},
|
},
|
||||||
[coarse, clear, onLongPress],
|
[coarse, stopTimer, onLongPress],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onTouchMove = useCallback(
|
const onTouchMove = useCallback(
|
||||||
@@ -72,7 +87,16 @@ export function useLongPress(onLongPress: (x: number, y: number) => void) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
coarse,
|
coarse,
|
||||||
handlers: coarse ? { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: clear } : {},
|
handlers: coarse
|
||||||
|
? {
|
||||||
|
// [Gitea #199] Marks the element for the iOS hardening CSS.
|
||||||
|
'data-touch-actions': '',
|
||||||
|
onTouchStart,
|
||||||
|
onTouchMove,
|
||||||
|
onTouchEnd,
|
||||||
|
onTouchCancel: clear,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
/** True for ~1 s after a long-press fired (drop the Android contextmenu echo). */
|
/** True for ~1 s after a long-press fired (drop the Android contextmenu echo). */
|
||||||
suppressContextMenu: fired,
|
suppressContextMenu: fired,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user