@@ -468,30 +468,31 @@ describe('MathNodeView', () => {
468468 } ) ;
469469
470470 describe ( 'toolbar positioning' , ( ) => {
471- it ( 'positions relative to the editor element using coordsAtPos' , async ( ) => {
471+ it ( 'positions relative to portal container using coordsAtPos' , async ( ) => {
472472 const { container } = render ( < MathNodeView { ...defaultProps } selected = { true } /> ) ;
473473 await waitFor ( ( ) => {
474474 const toolbar = container . querySelector ( '[data-toolbar-for]' ) ;
475475 expect ( toolbar ) . toBeInTheDocument ( ) ;
476- expect ( toolbar . style . top ) . toBe ( '140px ' ) ;
476+ expect ( toolbar . style . top ) . toBe ( '100px ' ) ;
477477 expect ( toolbar . style . left ) . toBe ( '50px' ) ;
478478 } ) ;
479479 } ) ;
480480
481- it ( 'accounts for editor scroll offset when calculating toolbar position' , async ( ) => {
482- const editorElement = createEditorElement ( { top : - 200 , left : 0 , width : 600 , height : 400 } ) ;
481+ it ( 'offsets position by portal container getBoundingClientRect' , async ( ) => {
482+ const containerEl = document . createElement ( 'div' ) ;
483+ containerEl . getBoundingClientRect = jest . fn ( ( ) => ( { top : 100 , left : 50 , width : 600 , height : 400 } ) ) ;
483484
484485 const editor = {
485486 ...defaultProps . editor ,
486- options : { element : editorElement } ,
487+ _tiptapContainerEl : containerEl ,
487488 } ;
488489
489490 const { container } = render ( < MathNodeView { ...defaultProps } editor = { editor } selected = { true } /> ) ;
490491 await waitFor ( ( ) => {
491492 const toolbar = container . querySelector ( '[data-toolbar-for]' ) ;
492493 expect ( toolbar ) . toBeInTheDocument ( ) ;
493- expect ( toolbar . style . top ) . toBe ( '340px ' ) ;
494- expect ( toolbar . style . left ) . toBe ( '50px ' ) ;
494+ expect ( toolbar . style . top ) . toBe ( '0px ' ) ;
495+ expect ( toolbar . style . left ) . toBe ( '0px ' ) ;
495496 } ) ;
496497 } ) ;
497498
@@ -504,6 +505,15 @@ describe('MathNodeView', () => {
504505 } ) ;
505506 } ) ;
506507
508+ it ( 'renders above other editor overlays with a high z-index' , async ( ) => {
509+ const { container } = render ( < MathNodeView { ...defaultProps } selected = { true } /> ) ;
510+ await waitFor ( ( ) => {
511+ const toolbar = container . querySelector ( '[data-toolbar-for]' ) ;
512+ expect ( toolbar ) . toBeInTheDocument ( ) ;
513+ expect ( toolbar . style . zIndex ) . toBe ( '1000' ) ;
514+ } ) ;
515+ } ) ;
516+
507517 it ( 'updates position from coordsAtPos when selection changes' , async ( ) => {
508518 const editor = {
509519 ...defaultProps . editor ,
@@ -518,11 +528,64 @@ describe('MathNodeView', () => {
518528 await waitFor ( ( ) => {
519529 const toolbar = container . querySelector ( '[data-toolbar-for]' ) ;
520530 expect ( toolbar ) . toBeInTheDocument ( ) ;
521- expect ( toolbar . style . top ) . toBe ( '240px ' ) ;
531+ expect ( toolbar . style . top ) . toBe ( '200px ' ) ;
522532 expect ( toolbar . style . left ) . toBe ( '150px' ) ;
523533 } ) ;
524534 } ) ;
525535
536+ it ( 'clamps toolbar position to viewport margins' , async ( ) => {
537+ const originalInnerHeight = window . innerHeight ;
538+ const originalInnerWidth = window . innerWidth ;
539+
540+ Object . defineProperty ( window , 'innerHeight' , { configurable : true , writable : true , value : 200 } ) ;
541+ Object . defineProperty ( window , 'innerWidth' , { configurable : true , writable : true , value : 300 } ) ;
542+
543+ const editor = {
544+ ...defaultProps . editor ,
545+ view : {
546+ ...defaultProps . editor . view ,
547+ coordsAtPos : jest . fn ( ( ) => ( { top : 190 , left : 280 , bottom : 195 } ) ) ,
548+ dispatch : jest . fn ( ) ,
549+ } ,
550+ } ;
551+
552+ const { container } = render ( < MathNodeView { ...defaultProps } editor = { editor } selected = { true } /> ) ;
553+
554+ let toolbar ;
555+ await waitFor ( ( ) => {
556+ toolbar = container . querySelector ( '[data-toolbar-for]' ) ;
557+ expect ( toolbar ) . toBeInTheDocument ( ) ;
558+ } ) ;
559+
560+ Object . defineProperty ( toolbar , 'offsetHeight' , { configurable : true , value : 100 } ) ;
561+ Object . defineProperty ( toolbar , 'offsetWidth' , { configurable : true , value : 150 } ) ;
562+
563+ await act ( async ( ) => {
564+ window . dispatchEvent ( new Event ( 'resize' ) ) ;
565+ await new Promise ( ( resolve ) => requestAnimationFrame ( resolve ) ) ;
566+ } ) ;
567+
568+ await waitFor ( ( ) => {
569+ expect ( parseInt ( toolbar . style . top , 10 ) ) . toBeLessThanOrEqual ( 200 - 100 - 8 ) ;
570+ expect ( parseInt ( toolbar . style . left , 10 ) ) . toBeLessThanOrEqual ( 300 - 150 - 8 ) ;
571+ expect ( parseInt ( toolbar . style . top , 10 ) ) . toBeGreaterThanOrEqual ( 8 ) ;
572+ expect ( parseInt ( toolbar . style . left , 10 ) ) . toBeGreaterThanOrEqual ( 8 ) ;
573+ } ) ;
574+
575+ Object . defineProperty ( window , 'innerHeight' , { configurable : true , writable : true , value : originalInnerHeight } ) ;
576+ Object . defineProperty ( window , 'innerWidth' , { configurable : true , writable : true , value : originalInnerWidth } ) ;
577+ } ) ;
578+
579+ it ( 'attaches scroll and resize listeners while toolbar is open' , async ( ) => {
580+ const addSpy = jest . spyOn ( window , 'addEventListener' ) ;
581+ render ( < MathNodeView { ...defaultProps } selected = { true } /> ) ;
582+ await waitFor ( ( ) => {
583+ expect ( addSpy ) . toHaveBeenCalledWith ( 'scroll' , expect . any ( Function ) , true ) ;
584+ expect ( addSpy ) . toHaveBeenCalledWith ( 'resize' , expect . any ( Function ) ) ;
585+ } ) ;
586+ addSpy . mockRestore ( ) ;
587+ } ) ;
588+
526589 it ( 'portals toolbar into _tiptapContainerEl when available' , async ( ) => {
527590 const containerEl = document . createElement ( 'div' ) ;
528591 containerEl . getBoundingClientRect = jest . fn ( ( ) => ( { top : 0 , left : 0 , width : 600 , height : 400 } ) ) ;
0 commit comments