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,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+ }
0 commit comments