1- import { PointerEvent , ReactNode , useCallback , useEffect , useMemo , useState } from 'react' ;
2-
1+ import { ReactNode , useEffect , useMemo , useRef , useState } from 'react' ;
32import { useAnnotator , useSelection } from '@annotorious/react' ;
43import {
54 isRevived ,
5+ NOT_ANNOTATABLE_CLASS ,
66 denormalizeRectWithOffset ,
77 toDomRectList ,
88 type TextAnnotation ,
@@ -11,8 +11,11 @@ import {
1111
1212import { isMobile } from './isMobile' ;
1313import {
14+ arrow ,
1415 autoUpdate ,
1516 flip ,
17+ FloatingArrow ,
18+ FloatingArrowProps ,
1619 FloatingFocusManager ,
1720 FloatingPortal ,
1821 inline ,
@@ -24,12 +27,16 @@ import {
2427 useRole
2528} from '@floating-ui/react' ;
2629
27- import './TextAnnotatorPopup .css' ;
30+ import './TextAnnotationPopup .css' ;
2831
2932interface TextAnnotationPopupProps {
3033
3134 ariaCloseWarning ?: string ;
3235
36+ arrow ?: boolean ;
37+
38+ arrowProps ?: Omit < FloatingArrowProps , 'context' | 'ref' > ;
39+
3340 popup ( props : TextAnnotationPopupContentProps ) : ReactNode ;
3441
3542}
@@ -40,11 +47,11 @@ export interface TextAnnotationPopupContentProps {
4047
4148 editable ?: boolean ;
4249
43- event ?: PointerEvent ;
50+ event ?: PointerEvent | KeyboardEvent ;
4451
4552}
4653
47- export const TextAnnotatorPopup = ( props : TextAnnotationPopupProps ) => {
54+ export const TextAnnotationPopup = ( props : TextAnnotationPopupProps ) => {
4855
4956 const r = useAnnotator < TextAnnotator > ( ) ;
5057
@@ -54,6 +61,22 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
5461
5562 const [ isOpen , setOpen ] = useState ( selected ?. length > 0 ) ;
5663
64+ const arrowRef = useRef ( null ) ;
65+
66+ // Conditional floating-ui middleware
67+ const middleware = useMemo ( ( ) => {
68+ const m = [
69+ inline ( ) ,
70+ offset ( 10 ) ,
71+ flip ( { crossAxis : true } ) ,
72+ shift ( { crossAxis : true , padding : 10 } )
73+ ] ;
74+
75+ return props . arrow
76+ ? [ ...m , arrow ( { element : arrowRef } ) ]
77+ : m ;
78+ } , [ props . arrow ] ) ;
79+
5780 const { refs, floatingStyles, update, context } = useFloating ( {
5881 placement : isMobile ( ) ? 'bottom' : 'top' ,
5982 open : isOpen ,
@@ -63,12 +86,7 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
6386 r ?. cancelSelected ( ) ;
6487 }
6588 } ,
66- middleware : [
67- offset ( 10 ) ,
68- inline ( ) ,
69- flip ( ) ,
70- shift ( { mainAxis : false , crossAxis : true , padding : 10 } )
71- ] ,
89+ middleware,
7290 whileElementsMounted : autoUpdate
7391 } ) ;
7492
@@ -121,12 +139,6 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
121139 } ;
122140 } , [ update ] ) ;
123141
124- // Prevent text-annotator from handling the irrelevant events triggered from the popup
125- const getStopEventsPropagationProps = useCallback (
126- ( ) => ( { onPointerUp : ( event : PointerEvent < HTMLDivElement > ) => event . stopPropagation ( ) } ) ,
127- [ ]
128- ) ;
129-
130142 // Don't shift focus to the floating element if selected via keyboard or on mobile.
131143 const initialFocus = useMemo ( ( ) => {
132144 return ( event ?. type === 'keyup' || event ?. type === 'contextmenu' || isMobile ( ) ) ? - 1 : 0 ;
@@ -143,17 +155,23 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
143155 returnFocus = { false }
144156 initialFocus = { initialFocus } >
145157 < div
146- className = " a9s-popup r6o-popup annotation-popup r6o-text-popup not-annotatable"
158+ className = { ` a9s-popup r6o-popup annotation-popup r6o-text-popup ${ NOT_ANNOTATABLE_CLASS } ` }
147159 ref = { refs . setFloating }
148160 style = { floatingStyles }
149- { ...getFloatingProps ( ) }
150- { ...getStopEventsPropagationProps ( ) } >
161+ { ...getFloatingProps ( getStopEventsPropagationProps ( ) ) } >
151162 { props . popup ( {
152163 annotation : selected [ 0 ] . annotation ,
153164 editable : selected [ 0 ] . editable ,
154165 event
155166 } ) }
156167
168+ { props . arrow && (
169+ < FloatingArrow
170+ ref = { arrowRef }
171+ context = { context }
172+ { ...( props . arrowProps || { } ) } />
173+ ) }
174+
157175 < button className = "r6o-popup-sr-only" aria-live = "assertive" onClick = { onClose } >
158176 { props . ariaCloseWarning || 'Click or leave this dialog to close it.' }
159177 </ button >
@@ -163,3 +181,25 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
163181 ) : null ;
164182
165183}
184+
185+ /**
186+ * Prevent text-annotator from handling the irrelevant events
187+ * triggered from the popup/toolbar/dialog
188+ */
189+ const getStopEventsPropagationProps = < T extends HTMLElement = HTMLElement > ( ) => ( {
190+ onPointerUp : ( event : React . PointerEvent < T > ) => event . stopPropagation ( ) ,
191+ onPointerDown : ( event : React . PointerEvent < T > ) => event . stopPropagation ( ) ,
192+ onMouseDown : ( event : React . MouseEvent < T > ) => event . stopPropagation ( ) ,
193+ onMouseUp : ( event : React . MouseEvent < T > ) => event . stopPropagation ( )
194+ } ) ;
195+
196+ /** For backwards compatibility **/
197+ /** @deprecated Use TextAnnotationPopup instead */
198+ export const TextAnnotatorPopup = ( props : TextAnnotationPopupProps ) => {
199+
200+ useEffect ( ( ) => {
201+ console . warn ( 'TextAnnotatorPopup is deprecated and will be removed in a future version. Please use TextAnnotationPopup instead.' ) ;
202+ } , [ ] ) ;
203+
204+ return < TextAnnotationPopup { ...props } /> ;
205+ } ;
0 commit comments