fix(ios): suppress native text-selection menu over annotation tools (#4231)
On iOS the system text-selection menu (Copy / Look Up / Translate /
Share) appeared on top of Readest's annotation toolbar. The previous
workaround removed and re-added the selection range on a timer
(makeSelectionOnIOS) to shake the menu off — flaky on iOS 16 and on the
first long-press of a word.
Suppress the menu natively instead, in the native-bridge iOS plugin.
ContextMenuSuppressor swizzles WKContentView so non-editable web
selections produce an empty menu that is never presented:
* editMenuInteraction(_:menuForConfiguration:suggestedActions:) — the
UIEditMenuInteraction delegate WebKit uses to build the menu on
iOS 16+ (the menu users actually see on modern iOS).
* presentEditMenu(with:) — a present-time backstop.
* canPerformAction(_:withSender:) — the legacy UIMenuController gate
for iOS 15 and earlier.
Editable HTML fields keep their native menu (Paste / Select All still
work) via a cut:/paste: probe. Text selection and drag handles are
unaffected, so the annotation toolbar still triggers.
With suppression handled natively, makeSelectionOnIOS is removed and iOS
selections take the same path as desktop.
Closes #4218
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,27 +68,6 @@ export const useTextSelector = (
|
||||
index,
|
||||
});
|
||||
};
|
||||
// FIXME: extremely hacky way to dismiss system selection tools on iOS
|
||||
const makeSelectionOnIOS = async (sel: Selection, index: number) => {
|
||||
isTextSelected.current = true;
|
||||
const range = sel.getRangeAt(0);
|
||||
setTimeout(() => {
|
||||
sel.removeAllRanges();
|
||||
setTimeout(async () => {
|
||||
if (!isTextSelected.current) return;
|
||||
sel.addRange(range);
|
||||
const progress = getProgress(bookKey);
|
||||
setSelection({
|
||||
key: bookKey,
|
||||
text: await getAnnotationText(range),
|
||||
cfi: view?.getCFI(index, range),
|
||||
page: bookData?.isFixedLayout ? index + 1 : progress?.page || 0,
|
||||
range,
|
||||
index,
|
||||
});
|
||||
}, 30);
|
||||
}, 30);
|
||||
};
|
||||
|
||||
const startInstantAnnotating = (ev: PointerEvent) => {
|
||||
isInstantAnnotating.current = true;
|
||||
@@ -175,11 +154,11 @@ export const useTextSelector = (
|
||||
const sel = doc.getSelection() as Selection;
|
||||
if (isValidSelection(sel)) {
|
||||
const isPointerInside = ev && isPointerInsideSelection(sel, ev);
|
||||
const isIOS = osPlatform === 'ios' || appService?.isIOSApp;
|
||||
|
||||
if (isPointerInside && isIOS) {
|
||||
makeSelectionOnIOS(sel, index);
|
||||
} else if (isPointerInside) {
|
||||
// iOS no longer needs a special path: the native plugin
|
||||
// (ContextMenuSuppressor) suppresses the system selection menu, so
|
||||
// iOS selections go through the same path as desktop.
|
||||
if (isPointerInside) {
|
||||
isUpToPopup.current = true;
|
||||
makeSelection(sel, index, true);
|
||||
} else if (appService?.isAndroidApp) {
|
||||
|
||||
Reference in New Issue
Block a user