@@ -22,7 +22,7 @@ import fakeRestDataProvider from 'ra-data-fakerest';
2222import { Routes , Route } from 'react-router-dom' ;
2323import Mention from '@tiptap/extension-mention' ;
2424import { Editor , ReactRenderer } from '@tiptap/react' ;
25- import tippy , { Instance as TippyInstance } from 'tippy.js ' ;
25+ import { computePosition , flip , shift , offset } from '@floating-ui/dom ' ;
2626import {
2727 DefaultEditorOptions ,
2828 RichTextInput ,
@@ -375,13 +375,13 @@ export const CustomOptions = () => (
375375 </ TestMemoryRouter >
376376) ;
377377
378- const MentionList = React . forwardRef <
379- MentionListRef ,
380- {
381- items : string [ ] ;
382- command : ( props : { id : string } ) => void ;
383- }
384- > ( ( props , ref ) => {
378+ const MentionList = ( props : {
379+ items : string [ ] ;
380+ command : ( props : { id : string } ) => void ;
381+ onKeyDownRef : React . MutableRefObject <
382+ ( ( props : { event : KeyboardEvent } ) => boolean ) | null
383+ > ;
384+ } ) => {
385385 const [ selectedIndex , setSelectedIndex ] = React . useState ( 0 ) ;
386386
387387 const selectItem = index => {
@@ -392,42 +392,30 @@ const MentionList = React.forwardRef<
392392 }
393393 } ;
394394
395- const upHandler = ( ) => {
396- setSelectedIndex (
397- ( selectedIndex + props . items . length - 1 ) % props . items . length
398- ) ;
399- } ;
400-
401- const downHandler = ( ) => {
402- setSelectedIndex ( ( selectedIndex + 1 ) % props . items . length ) ;
403- } ;
404-
405- const enterHandler = ( ) => {
406- selectItem ( selectedIndex ) ;
407- } ;
408-
409395 React . useEffect ( ( ) => setSelectedIndex ( 0 ) , [ props . items ] ) ;
410396
411- React . useImperativeHandle ( ref , ( ) => ( {
412- onKeyDown : ( { event } ) => {
397+ React . useEffect ( ( ) => {
398+ props . onKeyDownRef . current = ( { event } ) => {
413399 if ( event . key === 'ArrowUp' ) {
414- upHandler ( ) ;
400+ setSelectedIndex (
401+ i => ( i + props . items . length - 1 ) % props . items . length
402+ ) ;
415403 return true ;
416404 }
417405
418406 if ( event . key === 'ArrowDown' ) {
419- downHandler ( ) ;
407+ setSelectedIndex ( i => ( i + 1 ) % props . items . length ) ;
420408 return true ;
421409 }
422410
423411 if ( event . key === 'Enter' ) {
424- enterHandler ( ) ;
412+ selectItem ( selectedIndex ) ;
425413 return true ;
426414 }
427415
428416 return false ;
429- } ,
430- } ) ) ;
417+ } ;
418+ } ) ;
431419
432420 return (
433421 < Paper >
@@ -438,7 +426,10 @@ const MentionList = React.forwardRef<
438426 dense
439427 selected = { index === selectedIndex }
440428 key = { index }
441- onClick = { ( ) => selectItem ( index ) }
429+ onMouseDown = { e => {
430+ e . preventDefault ( ) ;
431+ selectItem ( index ) ;
432+ } }
442433 >
443434 { item }
444435 </ ListItemButton >
@@ -451,10 +442,6 @@ const MentionList = React.forwardRef<
451442 </ List >
452443 </ Paper >
453444 ) ;
454- } ) ;
455-
456- type MentionListRef = {
457- onKeyDown : ( props : { event : React . KeyboardEvent } ) => boolean ;
458445} ;
459446const suggestions = tags => {
460447 return {
@@ -467,75 +454,84 @@ const suggestions = tags => {
467454 } ,
468455
469456 render : ( ) => {
470- let component : ReactRenderer < MentionListRef > ;
471- let popup : TippyInstance [ ] ;
457+ let component : ReactRenderer ;
458+ let floatingEl : HTMLElement ;
459+ const onKeyDownRef : React . MutableRefObject <
460+ ( ( props : { event : KeyboardEvent } ) => boolean ) | null
461+ > = { current : null } ;
462+
463+ const updatePosition = ( clientRect : ( ) => DOMRect ) => {
464+ if ( ! floatingEl ) return ;
465+ const virtualEl = {
466+ getBoundingClientRect : clientRect ,
467+ } ;
468+ computePosition ( virtualEl , floatingEl , {
469+ placement : 'bottom-start' ,
470+ middleware : [ offset ( 8 ) , flip ( ) , shift ( ) ] ,
471+ } ) . then ( ( { x, y } ) => {
472+ Object . assign ( floatingEl . style , {
473+ left : `${ x } px` ,
474+ top : `${ y } px` ,
475+ } ) ;
476+ } ) ;
477+ } ;
472478
473479 return {
474480 onStart : props => {
475481 component = new ReactRenderer ( MentionList , {
476- props,
482+ props : { ... props , onKeyDownRef } ,
477483 editor : props . editor ,
478484 } ) ;
479485
480- if ( ! props . clientRect ) {
481- return ;
482- }
486+ floatingEl = document . createElement ( 'div' ) ;
487+ floatingEl . style . position = 'absolute' ;
488+ floatingEl . style . zIndex = '1300' ;
489+ floatingEl . addEventListener ( 'mousedown' , e =>
490+ e . preventDefault ( )
491+ ) ;
492+ floatingEl . appendChild ( component . element ) ;
493+ props . editor . view . dom . parentElement . appendChild ( floatingEl ) ;
483494
484- popup = tippy ( 'body' , {
485- getReferenceClientRect : props . clientRect ,
486- appendTo : ( ) => document . body ,
487- content : component . element ,
488- showOnCreate : true ,
489- interactive : true ,
490- trigger : 'manual' ,
491- placement : 'bottom-start' ,
492- } ) ;
495+ if ( props . clientRect ) {
496+ updatePosition ( props . clientRect ) ;
497+ }
493498 } ,
494499
495500 onUpdate ( props ) {
496501 if ( component ) {
497- component . updateProps ( props ) ;
502+ component . updateProps ( { ... props , onKeyDownRef } ) ;
498503 }
499504
500- if ( ! props . clientRect ) {
501- return ;
502- }
503-
504- if ( popup && popup [ 0 ] ) {
505- popup [ 0 ] . setProps ( {
506- getReferenceClientRect : props . clientRect ,
507- } ) ;
505+ if ( props . clientRect ) {
506+ updatePosition ( props . clientRect ) ;
508507 }
509508 } ,
510509
511510 onKeyDown ( props ) {
512- if ( popup && popup [ 0 ] && props . event . key === 'Escape' ) {
513- popup [ 0 ] . hide ( ) ;
514-
511+ if ( props . event . key === 'Escape' ) {
512+ if ( floatingEl ) {
513+ floatingEl . style . display = 'none' ;
514+ }
515515 return true ;
516516 }
517517
518- if ( ! component . ref ) {
518+ if ( ! onKeyDownRef . current ) {
519519 return false ;
520520 }
521521
522- return component . ref . onKeyDown ( props ) ;
522+ return onKeyDownRef . current ( props ) ;
523523 } ,
524524
525525 onExit ( ) {
526+ onKeyDownRef . current = null ;
526527 queueMicrotask ( ( ) => {
527- if ( popup && popup [ 0 ] && ! popup [ 0 ] . state . isDestroyed ) {
528- popup [ 0 ] . destroy ( ) ;
529- }
530528 if ( component ) {
531529 component . destroy ( ) ;
532530 }
533- // Remove references to the old popup and component upon destruction/exit.
534- // (This should prevent redundant calls to `popup.destroy()`, which Tippy
535- // warns in the console is a sign of a memory leak, as the `suggestion`
536- // plugin seems to call `onExit` both when a suggestion menu is closed after
537- // a user chooses an option, *and* when the editor itself is destroyed.)
538- popup = undefined ;
531+ if ( floatingEl && floatingEl . parentNode ) {
532+ floatingEl . parentNode . removeChild ( floatingEl ) ;
533+ }
534+ floatingEl = undefined ;
539535 component = undefined ;
540536 } ) ;
541537 } ,
0 commit comments