Skip to content

Commit 9790b0e

Browse files
Merge branch 'main' into recogito#115-fix-spans-underlying-offset
# Conflicts: # packages/text-annotator-react/test/index.html
2 parents 614f4c1 + 1b9fbb5 commit 9790b0e

File tree

20 files changed

+613
-396
lines changed

20 files changed

+613
-396
lines changed

package-lock.json

Lines changed: 157 additions & 154 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/extension-tei/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
"CETEIcean": "^1.9.3",
3030
"typescript": "5.6.2",
3131
"vite": "^5.4.8",
32-
"vite-plugin-dts": "^4.2.2"
32+
"vite-plugin-dts": "^4.2.3"
3333
},
3434
"peerDependencies": {
35-
"@annotorious/core": "^3.0.8",
35+
"@annotorious/core": "^3.0.9",
3636
"@recogito/text-annotator": "3.0.0-rc.46"
3737
}
3838
}

packages/text-annotator-react/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
"types": "./dist/index.d.ts",
2626
"devDependencies": {
2727
"@types/react-dom": "^18.3.0",
28-
"@vitejs/plugin-react": "^4.3.1",
28+
"@vitejs/plugin-react": "^4.3.2",
2929
"react": "^18.3.1",
3030
"react-dom": "^18.3.1",
3131
"typescript": "5.6.2",
32-
"vite": "^5.4.6",
33-
"vite-plugin-dts": "^4.2.1",
32+
"vite": "^5.4.8",
33+
"vite-plugin-dts": "^4.2.3",
3434
"vite-tsconfig-paths": "^5.0.1"
3535
},
3636
"peerDependencies": {
@@ -44,11 +44,11 @@
4444
}
4545
},
4646
"dependencies": {
47-
"@annotorious/core": "^3.0.8",
48-
"@annotorious/react": "^3.0.8",
47+
"@annotorious/core": "^3.0.9",
48+
"@annotorious/react": "^3.0.9",
4949
"@floating-ui/react": "^0.26.24",
5050
"@recogito/text-annotator": "3.0.0-rc.46",
5151
"@recogito/text-annotator-tei": "3.0.0-rc.46",
5252
"CETEIcean": "^1.9.3"
5353
}
54-
}
54+
}

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

Lines changed: 0 additions & 124 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Close message should be visible only to the keyboard
3+
* or the screen reader users as the popup behavior hint
4+
* Inspired by https://gist.github.com/ffoodd/000b59f431e3e64e4ce1a24d5bb36034
5+
*/
6+
.popup-close-message {
7+
border: 0 !important;
8+
clip: rect(1px, 1px, 1px, 1px);
9+
-webkit-clip-path: inset(50%);
10+
clip-path: inset(50%);
11+
height: 1px;
12+
margin: -1px;
13+
overflow: hidden;
14+
padding: 0;
15+
position: absolute;
16+
width: 1px;
17+
white-space: nowrap;
18+
}
19+
20+
.popup-close-message:focus,
21+
.popup-close-message:active {
22+
clip: auto;
23+
-webkit-clip-path: none;
24+
clip-path: none;
25+
height: auto;
26+
margin: auto;
27+
overflow: visible;
28+
width: auto;
29+
white-space: normal;
30+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import React, { PointerEvent, ReactNode, useCallback, useEffect, useState } from 'react';
2+
import {
3+
autoUpdate,
4+
flip,
5+
FloatingFocusManager,
6+
FloatingPortal,
7+
inline,
8+
offset,
9+
shift,
10+
useDismiss,
11+
useFloating,
12+
useInteractions,
13+
useRole
14+
} from '@floating-ui/react';
15+
16+
import { useAnnotator, useSelection } from '@annotorious/react';
17+
import type { TextAnnotation, TextAnnotator } from '@recogito/text-annotator';
18+
19+
import './TextAnnotatorPopup.css';
20+
21+
interface TextAnnotationPopupProps {
22+
23+
popup(props: TextAnnotationPopupContentProps): ReactNode;
24+
25+
}
26+
27+
export interface TextAnnotationPopupContentProps {
28+
29+
annotation: TextAnnotation;
30+
31+
editable?: boolean;
32+
33+
event?: PointerEvent;
34+
35+
}
36+
37+
export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
38+
39+
const r = useAnnotator<TextAnnotator>();
40+
41+
const { selected, event } = useSelection<TextAnnotation>();
42+
const annotation = selected[0]?.annotation;
43+
44+
const [isOpen, setOpen] = useState(selected?.length > 0);
45+
46+
const handleClose = () => {
47+
r?.cancelSelected();
48+
};
49+
50+
const { refs, floatingStyles, update, context } = useFloating({
51+
placement: 'top',
52+
open: isOpen,
53+
onOpenChange: (open, _event, reason) => {
54+
setOpen(open);
55+
56+
if (!open) {
57+
if (reason === 'escape-key' || reason === 'focus-out') {
58+
r?.cancelSelected();
59+
}
60+
}
61+
},
62+
middleware: [
63+
offset(10),
64+
inline(),
65+
flip(),
66+
shift({ mainAxis: false, crossAxis: true, padding: 10 })
67+
],
68+
whileElementsMounted: autoUpdate
69+
});
70+
71+
const dismiss = useDismiss(context);
72+
const role = useRole(context, { role: 'dialog' });
73+
const { getFloatingProps } = useInteractions([dismiss, role]);
74+
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]);
82+
83+
useEffect(() => {
84+
// Close the popup if the selection is cleared
85+
if (selected.length === 0 && isOpen) {
86+
setOpen(false);
87+
}
88+
}, [isOpen, selectedKey]);
89+
90+
useEffect(() => {
91+
if (isOpen && annotation) {
92+
const {
93+
target: {
94+
selector: [{ range }]
95+
}
96+
} = annotation;
97+
98+
refs.setPositionReference({
99+
getBoundingClientRect: range.getBoundingClientRect.bind(range),
100+
getClientRects: range.getClientRects.bind(range)
101+
});
102+
} else {
103+
// Don't leave the reference depending on the previously selected annotation
104+
refs.setPositionReference(null);
105+
}
106+
}, [isOpen, annotation, refs]);
107+
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+
114+
useEffect(() => {
115+
const config: MutationObserverInit = { attributes: true, childList: true, subtree: true };
116+
117+
const mutationObserver = new MutationObserver(() => update());
118+
mutationObserver.observe(document.body, config);
119+
120+
window.document.addEventListener('scroll', update, true);
121+
122+
return () => {
123+
mutationObserver.disconnect();
124+
window.document.removeEventListener('scroll', update, true);
125+
};
126+
}, [update]);
127+
128+
return isOpen && selected.length > 0 ? (
129+
<FloatingPortal>
130+
<FloatingFocusManager
131+
context={context}
132+
modal={false}
133+
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+
}
141+
returnFocus={false}
142+
>
143+
<div
144+
className="annotation-popup text-annotation-popup not-annotatable"
145+
ref={refs.setFloating}
146+
style={floatingStyles}
147+
{...getFloatingProps()}
148+
{...getStopEventsPropagationProps()}>
149+
{props.popup({
150+
annotation: selected[0].annotation,
151+
editable: selected[0].editable,
152+
event
153+
})}
154+
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.
158+
</button>
159+
</div>
160+
</FloatingFocusManager>
161+
</FloatingPortal>
162+
) : null;
163+
164+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './TextAnnotatorPopup';

packages/text-annotator-react/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export type {
3030
export {
3131
createBody,
3232
Origin,
33-
UserSelectAction,
33+
UserSelectAction
3434
} from '@annotorious/core';
3535

3636
// Essential re-exports from @annotorious/react

0 commit comments

Comments
 (0)