11/* Copyright 2024 Marimo. All rights reserved. */
22
3- import { useState } from "react" ;
3+ import type { PopoverContentProps } from "@radix-ui/react-popover" ;
4+ import React , { useState } from "react" ;
5+ import { isInVscodeExtension } from "@/core/vscode/is-in-vscode" ;
46import { useEventListener } from "@/hooks/useEventListener" ;
57
8+ const VSCODE_OUTPUT_CONTAINER_SELECTOR = "[data-vscode-output-container]" ;
9+
10+ // vscode has smaller viewport so we need all the max-height we can get.
11+ // Otherwise, we give a 30px buffer to the max-height.
12+ export const MAX_HEIGHT_OFFSET = isInVscodeExtension ( ) ? 0 : 30 ;
13+
614/**
715 * Get the full screen element if we are in full screen mode
816 */
@@ -25,14 +33,120 @@ export function withFullScreenAsRoot<
2533 container ?: Element | DocumentFragment | null ;
2634 } ,
2735> ( Component : React . ComponentType < T > ) {
36+ const FindClosestVscodeOutputContainer = ( props : T ) => {
37+ const [ closest , setClosest ] = React . useState < Element | null > ( null ) ;
38+ const el = React . useRef < HTMLDivElement > ( null ) ;
39+
40+ React . useLayoutEffect ( ( ) => {
41+ if ( ! el . current ) {
42+ return ;
43+ }
44+
45+ const found = closestThroughShadowDOMs (
46+ el . current ,
47+ VSCODE_OUTPUT_CONTAINER_SELECTOR ,
48+ ) ;
49+ setClosest ( found ) ;
50+ } , [ ] ) ;
51+
52+ return (
53+ < >
54+ < div ref = { el } className = "contents invisible" />
55+ < Component { ...props } container = { closest } />
56+ </ >
57+ ) ;
58+ } ;
59+
2860 const Comp = ( props : T ) => {
2961 const fullScreenElement = useFullScreenElement ( ) ;
62+
63+ // If we are in the VSCode extension, we use the VSCode output container
64+ const vscodeOutputContainer = isInVscodeExtension ( ) ;
65+ if ( vscodeOutputContainer ) {
66+ return < FindClosestVscodeOutputContainer { ...props } /> ;
67+ }
68+
3069 if ( ! fullScreenElement ) {
3170 return < Component { ...props } /> ;
3271 }
72+
3373 return < Component { ...props } container = { fullScreenElement } /> ;
3474 } ;
3575
3676 Comp . displayName = Component . displayName ;
3777 return Comp ;
3878}
79+
80+ /**
81+ * HOC wrapping a PortalContent component to set a better collision boundary,
82+ * when inside vscode.
83+ */
84+ export function withSmartCollisionBoundary <
85+ T extends {
86+ collisionBoundary ?: PopoverContentProps [ "collisionBoundary" ] ;
87+ } ,
88+ > ( Component : React . ComponentType < T > ) {
89+ const FindClosestVscodeOutputContainer = ( props : T ) => {
90+ const [ closest , setClosest ] = React . useState < Element | null > ( null ) ;
91+ const el = React . useRef < HTMLDivElement > ( null ) ;
92+
93+ React . useLayoutEffect ( ( ) => {
94+ if ( ! el . current ) {
95+ return ;
96+ }
97+
98+ const found = closestThroughShadowDOMs (
99+ el . current ,
100+ VSCODE_OUTPUT_CONTAINER_SELECTOR ,
101+ ) ;
102+ setClosest ( found ) ;
103+ } , [ ] ) ;
104+
105+ return (
106+ < >
107+ < div ref = { el } className = "contents invisible" />
108+ < Component { ...props } collisionBoundary = { closest } />
109+ </ >
110+ ) ;
111+ } ;
112+
113+ const Comp = ( props : T ) => {
114+ // If we are in the VSCode extension, we use the VSCode output container
115+ const vscodeOutputContainer = isInVscodeExtension ( ) ;
116+ if ( vscodeOutputContainer ) {
117+ return < FindClosestVscodeOutputContainer { ...props } /> ;
118+ }
119+
120+ return < Component { ...props } /> ;
121+ } ;
122+
123+ Comp . displayName = Component . displayName ;
124+ return Comp ;
125+ }
126+
127+ /**
128+ * Find the closest element (with .closest), but through shadow DOMs.
129+ */
130+ function closestThroughShadowDOMs (
131+ element : Element ,
132+ selector : string ,
133+ ) : Element | null {
134+ let currentElement : Element | null = element ;
135+
136+ while ( currentElement ) {
137+ const cellElement = currentElement . closest ( selector ) ;
138+ if ( cellElement ) {
139+ return cellElement ;
140+ }
141+
142+ const root = currentElement . getRootNode ( ) ;
143+ currentElement =
144+ root instanceof ShadowRoot ? root . host : currentElement . parentElement ;
145+
146+ if ( currentElement === root ) {
147+ break ;
148+ }
149+ }
150+
151+ return null ;
152+ }
0 commit comments