1- import { PointerEvent , ReactNode , useCallback , useEffect , useMemo , useState } from 'react' ;
1+ import { ReactNode , useEffect , useMemo , useRef , useState } from 'react' ;
22import { useAnnotator , useSelection } from '@annotorious/react' ;
3- import { isRevived , TextAnnotation , TextAnnotator } from '@recogito/text-annotator' ;
3+ import { isRevived , NOT_ANNOTATABLE_CLASS , TextAnnotation , TextAnnotator } from '@recogito/text-annotator' ;
44import { isMobile } from './isMobile' ;
55import {
6+ arrow ,
67 autoUpdate ,
78 flip ,
9+ FloatingArrow ,
10+ FloatingArrowProps ,
811 FloatingFocusManager ,
912 FloatingPortal ,
1013 inline ,
@@ -16,12 +19,16 @@ import {
1619 useRole
1720} from '@floating-ui/react' ;
1821
19- import './TextAnnotatorPopup .css' ;
22+ import './TextAnnotationPopup .css' ;
2023
2124interface TextAnnotationPopupProps {
2225
2326 ariaCloseWarning ?: string ;
2427
28+ arrow ?: boolean ;
29+
30+ arrowProps ?: Omit < FloatingArrowProps , 'context' | 'ref' > ;
31+
2532 popup ( props : TextAnnotationPopupContentProps ) : ReactNode ;
2633
2734}
@@ -32,11 +39,11 @@ export interface TextAnnotationPopupContentProps {
3239
3340 editable ?: boolean ;
3441
35- event ?: PointerEvent ;
42+ event ?: PointerEvent | KeyboardEvent ;
3643
3744}
3845
39- export const TextAnnotatorPopup = ( props : TextAnnotationPopupProps ) => {
46+ export const TextAnnotationPopup = ( props : TextAnnotationPopupProps ) => {
4047
4148 const r = useAnnotator < TextAnnotator > ( ) ;
4249
@@ -46,6 +53,22 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
4653
4754 const [ isOpen , setOpen ] = useState ( selected ?. length > 0 ) ;
4855
56+ const arrowRef = useRef ( null ) ;
57+
58+ // Conditional floating-ui middleware
59+ const middleware = useMemo ( ( ) => {
60+ const m = [
61+ inline ( ) ,
62+ offset ( 10 ) ,
63+ flip ( { crossAxis : true } ) ,
64+ shift ( { crossAxis : true , padding : 10 } )
65+ ] ;
66+
67+ return props . arrow
68+ ? [ ...m , arrow ( { element : arrowRef } ) ]
69+ : m ;
70+ } , [ props . arrow ] ) ;
71+
4972 const { refs, floatingStyles, update, context } = useFloating ( {
5073 placement : isMobile ( ) ? 'bottom' : 'top' ,
5174 open : isOpen ,
@@ -55,12 +78,7 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
5578 r ?. cancelSelected ( ) ;
5679 }
5780 } ,
58- middleware : [
59- offset ( 10 ) ,
60- inline ( ) ,
61- flip ( ) ,
62- shift ( { mainAxis : false , crossAxis : true , padding : 10 } )
63- ] ,
81+ middleware,
6482 whileElementsMounted : autoUpdate
6583 } ) ;
6684
@@ -106,12 +124,6 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
106124 } ;
107125 } , [ update ] ) ;
108126
109- // Prevent text-annotator from handling the irrelevant events triggered from the popup
110- const getStopEventsPropagationProps = useCallback (
111- ( ) => ( { onPointerUp : ( event : PointerEvent < HTMLDivElement > ) => event . stopPropagation ( ) } ) ,
112- [ ]
113- ) ;
114-
115127 // Don't shift focus to the floating element if selected via keyboard or on mobile.
116128 const initialFocus = useMemo ( ( ) => {
117129 return ( event ?. type === 'keyup' || event ?. type === 'contextmenu' || isMobile ( ) ) ? - 1 : 0 ;
@@ -128,17 +140,23 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
128140 returnFocus = { false }
129141 initialFocus = { initialFocus } >
130142 < div
131- className = " a9s-popup r6o-popup annotation-popup r6o-text-popup not-annotatable"
143+ className = { ` a9s-popup r6o-popup annotation-popup r6o-text-popup ${ NOT_ANNOTATABLE_CLASS } ` }
132144 ref = { refs . setFloating }
133145 style = { floatingStyles }
134- { ...getFloatingProps ( ) }
135- { ...getStopEventsPropagationProps ( ) } >
146+ { ...getFloatingProps ( getStopEventsPropagationProps ( ) ) } >
136147 { props . popup ( {
137148 annotation : selected [ 0 ] . annotation ,
138149 editable : selected [ 0 ] . editable ,
139150 event
140151 } ) }
141152
153+ { props . arrow && (
154+ < FloatingArrow
155+ ref = { arrowRef }
156+ context = { context }
157+ { ...( props . arrowProps || { } ) } />
158+ ) }
159+
142160 < button className = "r6o-popup-sr-only" aria-live = "assertive" onClick = { onClose } >
143161 { props . ariaCloseWarning || 'Click or leave this dialog to close it.' }
144162 </ button >
@@ -148,3 +166,25 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
148166 ) : null ;
149167
150168}
169+
170+ /**
171+ * Prevent text-annotator from handling the irrelevant events
172+ * triggered from the popup/toolbar/dialog
173+ */
174+ const getStopEventsPropagationProps = < T extends HTMLElement = HTMLElement > ( ) => ( {
175+ onPointerUp : ( event : React . PointerEvent < T > ) => event . stopPropagation ( ) ,
176+ onPointerDown : ( event : React . PointerEvent < T > ) => event . stopPropagation ( ) ,
177+ onMouseDown : ( event : React . MouseEvent < T > ) => event . stopPropagation ( ) ,
178+ onMouseUp : ( event : React . MouseEvent < T > ) => event . stopPropagation ( )
179+ } ) ;
180+
181+ /** For backwards compatibility **/
182+ /** @deprecated Use TextAnnotationPopup instead */
183+ export const TextAnnotatorPopup = ( props : TextAnnotationPopupProps ) => {
184+
185+ useEffect ( ( ) => {
186+ console . warn ( 'TextAnnotatorPopup is deprecated and will be removed in a future version. Please use TextAnnotationPopup instead.' ) ;
187+ } , [ ] ) ;
188+
189+ return < TextAnnotationPopup { ...props } /> ;
190+ } ;
0 commit comments