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' ;
25import {
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-
1919import './TextAnnotatorPopup.css' ;
2020
2121interface 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,23 +65,22 @@ 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 (
75+ // Selected annotation exists and has a selector?
76+ annotation ?. target . selector &&
77+ // Selector not empty? (Annotations from plugins, general defensive programming)
78+ annotation . target . selector . length > 0 &&
79+ // Range not collapsed? (E.g. lazy loading PDFs. Note that this will have to
80+ // change if we switch from ranges to pre-computed bounds!)
81+ ! annotation . target . selector [ 0 ] . range . collapsed
82+ ) ;
83+ } , [ annotation ] ) ;
8984
9085 useEffect ( ( ) => {
9186 if ( isOpen && annotation ) {
@@ -96,21 +91,14 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
9691 } = annotation ;
9792
9893 refs . setPositionReference ( {
99- getBoundingClientRect : range . getBoundingClientRect . bind ( range ) ,
100- getClientRects : range . getClientRects . bind ( range )
94+ getBoundingClientRect : ( ) => range . getBoundingClientRect ( ) ,
95+ getClientRects : ( ) => range . getClientRects ( )
10196 } ) ;
10297 } else {
103- // Don't leave the reference depending on the previously selected annotation
10498 refs . setPositionReference ( null ) ;
10599 }
106100 } , [ isOpen , annotation , refs ] ) ;
107101
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-
114102 useEffect ( ( ) => {
115103 const config : MutationObserverInit = { attributes : true , childList : true , subtree : true } ;
116104
@@ -125,23 +113,29 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
125113 } ;
126114 } , [ update ] ) ;
127115
128- return isOpen && selected . length > 0 ? (
116+ // Prevent text-annotator from handling the irrelevant events triggered from the popup
117+ const getStopEventsPropagationProps = useCallback (
118+ ( ) => ( { onPointerUp : ( event : PointerEvent < HTMLDivElement > ) => event . stopPropagation ( ) } ) ,
119+ [ ]
120+ ) ;
121+
122+ // Don't shift focus to the floating element if selected via keyboard or on mobile.
123+ const initialFocus = useMemo ( ( ) => {
124+ return ( event ?. type === 'keyup' || event ?. type === 'contextmenu' || isMobile ( ) ) ? - 1 : 0 ;
125+ } , [ event ] ) ;
126+
127+ const onClose = ( ) => r ?. cancelSelected ( ) ;
128+
129+ return isOpen && annotation ? (
129130 < FloatingPortal >
130131 < FloatingFocusManager
131132 context = { context }
132133 modal = { false }
133134 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- }
141135 returnFocus = { false }
142- >
136+ initialFocus = { initialFocus } >
143137 < div
144- className = "annotation -popup text- annotation-popup not-annotatable"
138+ className = "a9s -popup r6o-popup annotation-popup r6o-text -popup not-annotatable"
145139 ref = { refs . setFloating }
146140 style = { floatingStyles }
147141 { ...getFloatingProps ( ) }
@@ -152,13 +146,12 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
152146 event
153147 } ) }
154148
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.
149+ < button className = "r6o-popup-sr-only" aria-live = "assertive" onClick = { onClose } >
150+ { props . ariaCloseWarning || 'Click or leave this dialog to close it.' }
158151 </ button >
159152 </ div >
160153 </ FloatingFocusManager >
161154 </ FloatingPortal >
162155 ) : null ;
163156
164- } ;
157+ }
0 commit comments