Skip to content

Commit 85e351a

Browse files
committed
Mobile fixes (Android in particular)
1 parent 4c5b09d commit 85e351a

File tree

3 files changed

+50
-54
lines changed

3 files changed

+50
-54
lines changed

packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PointerEvent, ReactNode, useCallback, useEffect, useState } from 'react';
22
import { useAnnotator, useSelection } from '@annotorious/react';
33
import type { TextAnnotation, TextAnnotator } from '@recogito/text-annotator';
4+
import { isMobile } from './isMobile';
45
import {
56
autoUpdate,
67
flip,
@@ -46,14 +47,12 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
4647
const [isOpen, setOpen] = useState(selected?.length > 0);
4748

4849
const { refs, floatingStyles, update, context } = useFloating({
49-
placement: 'top',
50+
placement: isMobile() ? 'bottom' : 'top',
5051
open: isOpen,
5152
onOpenChange: (open, _event, reason) => {
52-
setOpen(open);
53-
54-
if (!open) {
55-
if (reason === 'escape-key' || reason === 'focus-out')
56-
r?.cancelSelected();
53+
if (!open && (reason === 'escape-key' || reason === 'focus-out')) {
54+
setOpen(open);
55+
r?.cancelSelected();
5756
}
5857
},
5958
middleware: [
@@ -77,6 +76,9 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
7776

7877
useEffect(() => {
7978
if (isOpen && annotation) {
79+
// Extra precaution - shouldn't normally happen
80+
if (!annotation.target.selector || annotation.target.selector.length < 1) return;
81+
8082
const {
8183
target: {
8284
selector: [{ range }]
@@ -120,8 +122,9 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
120122
closeOnFocusOut={true}
121123
returnFocus={false}
122124
initialFocus={
123-
// Don't shift focus to the floating element when selected via keyboard
124-
event?.type === 'keydown' ? -1 : 0
125+
// Don't shift focus to the floating element if selected via keyboard
126+
// or on iPad/Android.
127+
(event?.type === 'keydown' || isMobile()) ? -1 : 0
125128
}>
126129
<div
127130
className="annotation-popup text-annotation-popup not-annotatable"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// https://stackoverflow.com/questions/21741841/detecting-ios-android-operating-system
2+
export const isMobile = () => {
3+
// @ts-ignore
4+
var userAgent: string = navigator.userAgent || navigator.vendor || window.opera;
5+
6+
if (/android/i.test(userAgent))
7+
return true;
8+
9+
// @ts-ignore
10+
// Note: as of recently, this NO LONGER DETECTS FIREFOX ON iOS!
11+
// This means FF/iOS will behave like on the desktop, and loose
12+
// selection handlebars after the popup opens.
13+
if (/iPad|iPhone/.test(userAgent) && !window.MSStream)
14+
return true;
15+
16+
return false;
17+
}

packages/text-annotator/src/SelectionHandler.ts

Lines changed: 22 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,20 @@ export const SelectionHandler = (
162162
isLeftClick = lastDownEvent.button === 0;
163163
};
164164

165+
// Helper
166+
const upsertCurrentTarget = () => {
167+
const exists = store.getAnnotation(currentTarget.annotation);
168+
if (exists) {
169+
store.updateTarget(currentTarget);
170+
} else {
171+
store.addAnnotation({
172+
id: currentTarget.annotation,
173+
bodies: [],
174+
target: currentTarget
175+
});
176+
}
177+
}
178+
165179
const onPointerUp = (evt: PointerEvent) => {
166180
const annotatable = !(evt.target as Node).parentElement?.closest(NOT_ANNOTATABLE_SELECTOR);
167181
if (!annotatable || !isLeftClick) return;
@@ -204,23 +218,8 @@ export const SelectionHandler = (
204218
currentTarget = undefined;
205219
clickSelect();
206220
} else if (currentTarget) {
207-
// Proper lifecycle management: clear selection first...
208221
selection.clear();
209-
210-
const exists = store.getAnnotation(currentTarget.annotation);
211-
if (exists) {
212-
// ...then add annotation to store...
213-
store.updateTarget(currentTarget);
214-
} else {
215-
// ...then add annotation to store...
216-
store.addAnnotation({
217-
id: currentTarget.annotation,
218-
bodies: [],
219-
target: currentTarget
220-
});
221-
}
222-
223-
// ...then make the new annotation the current selection
222+
upsertCurrentTarget();
224223
selection.userSelect(currentTarget.annotation, clonePointerEvent(evt));
225224
}
226225
});
@@ -231,20 +230,12 @@ export const SelectionHandler = (
231230

232231
if (sel?.isCollapsed) return;
233232

234-
const exists = store.getAnnotation(currentTarget.annotation);
235-
if (exists) {
236-
// ...then add annotation to store...
237-
store.updateTarget(currentTarget);
238-
} else {
239-
selection.clear();
240-
241-
// ...then add annotation to store...
242-
store.addAnnotation({
243-
id: currentTarget.annotation,
244-
bodies: [],
245-
target: currentTarget
246-
});
247-
}
233+
// When selecting the initial word, Chrome Android fires `contextmenu`
234+
// before selectionChanged.
235+
if (!currentTarget || currentTarget.selector.length === 0)
236+
onSelectionChange(evt);
237+
238+
upsertCurrentTarget();
248239

249240
selection.userSelect(currentTarget.annotation, clonePointerEvent(evt));
250241
}
@@ -254,23 +245,8 @@ export const SelectionHandler = (
254245
const sel = document.getSelection();
255246

256247
if (!sel.isCollapsed) {
257-
// Proper lifecycle management: clear selection first...
258248
selection.clear();
259-
260-
const exists = store.getAnnotation(currentTarget.annotation);
261-
if (exists) {
262-
// ...then add annotation to store...
263-
store.updateTarget(currentTarget);
264-
} else {
265-
// ...then add annotation to store...
266-
store.addAnnotation({
267-
id: currentTarget.annotation,
268-
bodies: [],
269-
target: currentTarget
270-
});
271-
}
272-
273-
// ...then make the new annotation the current selection
249+
upsertCurrentTarget();
274250
selection.userSelect(currentTarget.annotation, cloneKeyboardEvent(evt));
275251
}
276252
}

0 commit comments

Comments
 (0)