Skip to content

Commit 4be9e85

Browse files
authored
Merge pull request #151 from recogito/revised-selection-behavior
Revised Selection Behavior
2 parents ead4840 + 6758d7d commit 4be9e85

File tree

7 files changed

+168
-81
lines changed

7 files changed

+168
-81
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* or the screen reader users as the popup behavior hint
44
* Inspired by https://gist.github.com/ffoodd/000b59f431e3e64e4ce1a24d5bb36034
55
*/
6-
.popup-close-message {
6+
.r6o-popup-sr-only {
77
border: 0 !important;
88
clip: rect(1px, 1px, 1px, 1px);
99
-webkit-clip-path: inset(50%);
@@ -17,8 +17,8 @@
1717
white-space: nowrap;
1818
}
1919

20-
.popup-close-message:focus,
21-
.popup-close-message:active {
20+
.r6o-popup-sr-only:focus,
21+
.r6o-popup-sr-only:active {
2222
clip: auto;
2323
-webkit-clip-path: none;
2424
clip-path: none;
Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import React, { PointerEvent, ReactNode, useCallback, useEffect, useState } from 'react';
1+
import { PointerEvent, ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
2+
import { useAnnotator, useSelection } from '@annotorious/react';
3+
import type { TextAnnotation, TextAnnotator } from '@recogito/text-annotator';
4+
import { isMobile } from './isMobile';
25
import {
36
autoUpdate,
47
flip,
@@ -13,13 +16,12 @@ import {
1316
useRole
1417
} from '@floating-ui/react';
1518

16-
import { useAnnotator, useSelection } from '@annotorious/react';
17-
import type { TextAnnotation, TextAnnotator } from '@recogito/text-annotator';
18-
1919
import './TextAnnotatorPopup.css';
2020

2121
interface TextAnnotationPopupProps {
2222

23+
ariaCloseWarning?: string;
24+
2325
popup(props: TextAnnotationPopupContentProps): ReactNode;
2426

2527
}
@@ -39,24 +41,18 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
3941
const r = useAnnotator<TextAnnotator>();
4042

4143
const { selected, event } = useSelection<TextAnnotation>();
44+
4245
const annotation = selected[0]?.annotation;
4346

4447
const [isOpen, setOpen] = useState(selected?.length > 0);
4548

46-
const handleClose = () => {
47-
r?.cancelSelected();
48-
};
49-
5049
const { refs, floatingStyles, update, context } = useFloating({
51-
placement: 'top',
50+
placement: isMobile() ? 'bottom' : 'top',
5251
open: isOpen,
5352
onOpenChange: (open, _event, reason) => {
54-
setOpen(open);
55-
56-
if (!open) {
57-
if (reason === 'escape-key' || reason === 'focus-out') {
58-
r?.cancelSelected();
59-
}
53+
if (!open && (reason === 'escape-key' || reason === 'focus-out')) {
54+
setOpen(open);
55+
r?.cancelSelected();
6056
}
6157
},
6258
middleware: [
@@ -69,48 +65,35 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
6965
});
7066

7167
const dismiss = useDismiss(context);
68+
7269
const role = useRole(context, { role: 'dialog' });
73-
const { getFloatingProps } = useInteractions([dismiss, role]);
7470

75-
const selectedKey = selected.map(a => a.annotation.id).join('-');
76-
useEffect(() => {
77-
// Ignore all selection changes except those accompanied by a user event.
78-
if (selected.length > 0 && event) {
79-
setOpen(event.type === 'pointerup' || event.type === 'keydown');
80-
}
81-
}, [selectedKey, event]);
71+
const { getFloatingProps } = useInteractions([dismiss, role]);
8272

8373
useEffect(() => {
84-
// Close the popup if the selection is cleared
85-
if (selected.length === 0 && isOpen) {
86-
setOpen(false);
87-
}
88-
}, [isOpen, selectedKey]);
74+
setOpen(selected.length > 0);
75+
}, [selected.map(a => a.annotation.id).join('-')]);
8976

9077
useEffect(() => {
9178
if (isOpen && annotation) {
79+
// Extra precaution - shouldn't normally happen
80+
if (!annotation.target.selector || annotation.target.selector.length < 1) return;
81+
9282
const {
9383
target: {
9484
selector: [{ range }]
9585
}
9686
} = annotation;
9787

9888
refs.setPositionReference({
99-
getBoundingClientRect: range.getBoundingClientRect.bind(range),
100-
getClientRects: range.getClientRects.bind(range)
89+
getBoundingClientRect: () => range.getBoundingClientRect(),
90+
getClientRects: () => range.getClientRects()
10191
});
10292
} else {
103-
// Don't leave the reference depending on the previously selected annotation
10493
refs.setPositionReference(null);
10594
}
10695
}, [isOpen, annotation, refs]);
10796

108-
// Prevent text-annotator from handling the irrelevant events triggered from the popup
109-
const getStopEventsPropagationProps = useCallback(
110-
() => ({ onPointerUp: (event: PointerEvent<HTMLDivElement>) => event.stopPropagation() }),
111-
[]
112-
);
113-
11497
useEffect(() => {
11598
const config: MutationObserverInit = { attributes: true, childList: true, subtree: true };
11699

@@ -125,21 +108,27 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
125108
};
126109
}, [update]);
127110

111+
// Prevent text-annotator from handling the irrelevant events triggered from the popup
112+
const getStopEventsPropagationProps = useCallback(
113+
() => ({ onPointerUp: (event: PointerEvent<HTMLDivElement>) => event.stopPropagation() }),
114+
[]
115+
);
116+
117+
// Don't shift focus to the floating element if selected via keyboard or on mobile.
118+
const initialFocus = useMemo(() => {
119+
return (event?.type === 'keyup' || event?.type === 'contextmenu' || isMobile()) ? -1 : 0;
120+
}, [event]);
121+
122+
const onClose = () => r?.cancelSelected();
123+
128124
return isOpen && selected.length > 0 ? (
129125
<FloatingPortal>
130126
<FloatingFocusManager
131127
context={context}
132128
modal={false}
133129
closeOnFocusOut={true}
134-
initialFocus={
135-
/**
136-
* Don't shift focus to the floating element
137-
* when the selection performed with the keyboard
138-
*/
139-
event?.type === 'keydown' ? -1 : 0
140-
}
141130
returnFocus={false}
142-
>
131+
initialFocus={initialFocus}>
143132
<div
144133
className="annotation-popup text-annotation-popup not-annotatable"
145134
ref={refs.setFloating}
@@ -152,13 +141,12 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
152141
event
153142
})}
154143

155-
{/* It lets keyboard/sr users to know that the dialog closes when they focus out of it */}
156-
<button className="popup-close-message" onClick={handleClose}>
157-
This dialog closes when you leave it.
144+
<button className="r6o-popup-sr-only" aria-live="assertive" onClick={onClose}>
145+
{props.ariaCloseWarning || 'Click or leave this dialog to close it.'}
158146
</button>
159147
</div>
160148
</FloatingFocusManager>
161149
</FloatingPortal>
162150
) : null;
163151

164-
};
152+
}
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-react/test/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { FC, useCallback, useEffect } from 'react';
2-
import { AnnotationBody, Annotorious, useAnnotationStore, useAnnotator, useSelection } from '@annotorious/react';
3-
import { TextAnnotator, TextAnnotatorPopup, type TextAnnotationPopupContentProps } from '../src';
2+
import { AnnotationBody, Annotorious, useAnnotationStore, useAnnotator } from '@annotorious/react';
3+
import { TextAnnotationPopupContentProps, TextAnnotator, TextAnnotatorPopup } from '../src';
44
import { W3CTextFormat, type TextAnnotation, type TextAnnotator as RecogitoTextAnnotator } from '@recogito/text-annotator';
55

66
const TestPopup: FC<TextAnnotationPopupContentProps> = (props) => {

0 commit comments

Comments
 (0)