@@ -35,6 +35,7 @@ type Props = {
3535 extensionAPI : OnloadArgs [ "extensionAPI" ] ;
3636 trigger ?: JSX . Element ;
3737 isShift ?: boolean ;
38+ menuMaxHeight ?: number ;
3839} ;
3940
4041const NodeMenu = ( {
@@ -44,6 +45,7 @@ const NodeMenu = ({
4445 extensionAPI,
4546 trigger,
4647 isShift,
48+ menuMaxHeight,
4749} : { onClose : ( ) => void } & Props ) => {
4850 const isInitialTextSelected =
4951 ! ! textarea && textarea . selectionStart !== textarea . selectionEnd ;
@@ -71,6 +73,23 @@ const NodeMenu = ({
7173 const [ activeIndex , setActiveIndex ] = useState ( 0 ) ;
7274 const [ isOpen , setIsOpen ] = useState ( ! trigger ) ;
7375
76+ useEffect ( ( ) => {
77+ const container = menuRef . current ;
78+ if ( ! container ) return ;
79+ const activeItem = container . children [ activeIndex ] as
80+ | HTMLElement
81+ | undefined ;
82+ if ( ! activeItem ) return ;
83+ const containerRect = container . getBoundingClientRect ( ) ;
84+ const itemRect = activeItem . getBoundingClientRect ( ) ;
85+ if (
86+ itemRect . bottom > containerRect . bottom ||
87+ itemRect . top < containerRect . top
88+ ) {
89+ activeItem . scrollIntoView ( { block : "nearest" , behavior : "auto" } ) ;
90+ }
91+ } , [ activeIndex ] ) ;
92+
7493 const onSelect = useCallback (
7594 ( index : number ) => {
7695 const menuItem =
@@ -252,14 +271,18 @@ const NodeMenu = ({
252271 className = "relative z-50"
253272 position = { Position . BOTTOM_LEFT }
254273 modifiers = { {
255- flip : { enabled : false } ,
274+ flip : { enabled : true } ,
256275 preventOverflow : { enabled : false } ,
257276 } }
258277 autoFocus = { false }
259278 enforceFocus = { false }
260279 onInteraction = { trigger ? handlePopoverInteraction : undefined }
261280 content = {
262- < Menu ulRef = { menuRef } data-active-index = { activeIndex } >
281+ < Menu
282+ ulRef = { menuRef }
283+ data-active-index = { activeIndex }
284+ style = { { overflowY : "auto" , maxHeight : menuMaxHeight } }
285+ >
263286 { discourseNodes . map ( ( item , i ) => {
264287 const nodeColor =
265288 formatHexColor ( item ?. canvasSettings ?. color ) || "#000" ;
@@ -302,15 +325,25 @@ const NodeMenu = ({
302325
303326export const render = ( props : Props ) => {
304327 if ( ! props . textarea ) return ;
328+ if ( props . textarea . parentElement ?. querySelector ( "[data-discourse-node-menu]" ) )
329+ return ;
305330 const parent = document . createElement ( "span" ) ;
331+ parent . setAttribute ( "data-discourse-node-menu" , "true" ) ;
306332 const coords = getCoordsFromTextarea ( props . textarea ) ;
307333 parent . style . position = "absolute" ;
308334 parent . style . left = `${ coords . left } px` ;
309335 parent . style . top = `${ coords . top } px` ;
310336 props . textarea . parentElement ?. insertBefore ( parent , props . textarea ) ;
337+ const parentTop =
338+ props . textarea . parentElement ?. getBoundingClientRect ( ) . top ?? 0 ;
339+ const menuMaxHeight = Math . max (
340+ window . innerHeight - ( parentTop + coords . top ) - 24 ,
341+ 100 ,
342+ ) ;
311343 ReactDOM . render (
312344 < NodeMenu
313345 { ...props }
346+ menuMaxHeight = { menuMaxHeight }
314347 onClose = { ( ) => {
315348 ReactDOM . unmountComponentAtNode ( parent ) ;
316349 parent . remove ( ) ;
@@ -355,13 +388,19 @@ export const TextSelectionNodeMenu = ({
355388 />
356389 ) ;
357390
391+ const menuMaxHeight = Math . max (
392+ window . innerHeight - textarea . getBoundingClientRect ( ) . bottom - 8 ,
393+ 100 ,
394+ ) ;
395+
358396 return (
359397 < NodeMenu
360398 textarea = { textarea }
361399 extensionAPI = { extensionAPI }
362400 trigger = { trigger }
363401 onClose = { onClose }
364402 isShift
403+ menuMaxHeight = { menuMaxHeight }
365404 />
366405 ) ;
367406} ;
0 commit comments