Skip to content

Commit 5ec161e

Browse files
committed
feat(code-editor-panel): replace portal-based fullscreen with Radix Dialog
1 parent f08d1c4 commit 5ec161e

3 files changed

Lines changed: 48 additions & 56 deletions

File tree

dashboard/src/components/common/code-editor-panel.tsx

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { useTheme } from '@/app/providers/theme-provider'
22
import { Button } from '@/components/ui/button'
33
import { DEFAULT_MONACO_CODE_EDITOR_OPTIONS } from '@/components/common/code-editor-defaults'
4+
import { Dialog, DialogContent } from '@/components/ui/dialog'
45
import { useIsMobile } from '@/hooks/use-mobile'
56
import { cn } from '@/lib/utils'
67
import { Maximize2, Minimize2 } from 'lucide-react'
78
import { lazy, Suspense, useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
8-
import { createPortal } from 'react-dom'
99
import { useTranslation } from 'react-i18next'
1010

1111
const MonacoEditor = lazy(() => import('@/components/common/monaco-editor'))
@@ -26,7 +26,6 @@ export type CodeEditorPanelProps = {
2626
onDidBlur?: () => void
2727
/** Show maximize/minimize chrome (matches core-config / client-template modals). */
2828
enableFullscreen?: boolean
29-
fullscreenTitle?: ReactNode
3029
/** Notified when fullscreen toggles (e.g. hide modal footer while expanded). */
3130
onFullscreenChange?: (fullscreen: boolean) => void
3231
/**
@@ -65,7 +64,6 @@ export function CodeEditorPanel({
6564
onMount,
6665
onDidBlur,
6766
enableFullscreen = false,
68-
fullscreenTitle,
6967
onFullscreenChange,
7068
dialogOpen = true,
7169
embeddedContainerClassName = 'h-[calc(50vh-1rem)] sm:h-[calc(55vh-1rem)] md:h-[calc(55vh-1rem)]',
@@ -214,52 +212,6 @@ export function CodeEditorPanel({
214212
)
215213
}
216214

217-
// `PageTransition` uses `transform`, so `position: fixed` is trapped; portal to `document.body`.
218-
// Avoid `items-center` on the shell: it sets `align-self: center` on children so they don't stretch,
219-
// `h-full` collapses, and Monaco (`height: 100%`) renders as a thin strip.
220-
const fullscreenLayer =
221-
isEditorFullscreen && typeof document !== 'undefined' ? (
222-
<div className="fixed inset-0 z-[200] flex min-h-0 flex-col bg-background" dir="ltr">
223-
<div className="absolute inset-0 bg-background/95 backdrop-blur-sm" onClick={handleToggleFullscreen} />
224-
{!isEditorReady && (
225-
<div className="absolute inset-0 z-[70] flex items-center justify-center bg-background/80 backdrop-blur-sm">
226-
<span className="h-8 w-8 animate-spin rounded-full border-b-2 border-t-2 border-primary" />
227-
</div>
228-
)}
229-
<div className="relative z-10 flex min-h-0 w-full flex-1 flex-col bg-background sm:mx-auto sm:my-8 sm:h-[calc(100vh-4rem)] sm:max-w-[95vw] sm:flex-none sm:rounded-lg sm:border sm:shadow-xl">
230-
<div className="hidden shrink-0 items-center justify-between rounded-t-lg border-b bg-background px-3 py-2.5 sm:flex">
231-
<div className="flex items-center gap-2">
232-
{fullscreenTitle ? <span className="text-sm font-medium">{fullscreenTitle}</span> : null}
233-
</div>
234-
<Button
235-
type="button"
236-
size="icon"
237-
variant="ghost"
238-
className="h-8 w-8 shrink-0"
239-
onClick={handleToggleFullscreen}
240-
aria-label={t('exitFullscreen', { defaultValue: 'Exit fullscreen' })}
241-
>
242-
<Minimize2 className="h-4 w-4" />
243-
</Button>
244-
</div>
245-
<Button
246-
type="button"
247-
size="icon"
248-
variant="default"
249-
className="absolute right-2 top-2 z-20 h-9 w-9 rounded-full shadow-lg sm:hidden"
250-
onClick={handleToggleFullscreen}
251-
aria-label={t('exitFullscreen', { defaultValue: 'Exit fullscreen' })}
252-
>
253-
<Minimize2 className="h-4 w-4" />
254-
</Button>
255-
<div className="relative min-h-0 w-full flex-1">
256-
{renderEditor()}
257-
</div>
258-
{footer}
259-
</div>
260-
</div>
261-
) : null
262-
263215
return (
264216
<>
265217
{!isEditorFullscreen && (
@@ -292,7 +244,53 @@ export function CodeEditorPanel({
292244
{footer}
293245
</div>
294246
)}
295-
{fullscreenLayer ? createPortal(fullscreenLayer, document.body) : null}
247+
248+
{/* Fullscreen uses a nested Radix Dialog so it gets its own focus trap,
249+
which works correctly even when this component is inside another Dialog. */}
250+
<Dialog open={isEditorFullscreen} onOpenChange={open => { if (!open) handleToggleFullscreen() }}>
251+
<DialogContent
252+
className="flex h-[100dvh] max-h-[100dvh] w-[100dvw] max-w-[100dvw] flex-col gap-0 rounded-none border-none p-0 sm:h-[calc(100vh-4rem)] sm:max-h-[calc(100vh-4rem)] sm:max-w-[95vw] sm:rounded-lg sm:border sm:p-0 [&>button[class*='absolute']]:hidden"
253+
dir="ltr"
254+
onOpenAutoFocus={e => {
255+
e.preventDefault()
256+
// Focus the editor after the dialog opens
257+
setTimeout(() => {
258+
if (editorInstance) {
259+
if (typeof editorInstance.focus === 'function') editorInstance.focus()
260+
}
261+
}, 100)
262+
}}
263+
onPointerDownOutside={e => e.preventDefault()}
264+
onInteractOutside={e => e.preventDefault()}
265+
>
266+
<div className="hidden shrink-0 items-center justify-between border-b bg-background px-3 py-2.5 sm:flex sm:rounded-t-lg">
267+
<Button
268+
type="button"
269+
size="icon"
270+
variant="ghost"
271+
className="h-8 w-8 shrink-0"
272+
onClick={handleToggleFullscreen}
273+
aria-label={t('exitFullscreen', { defaultValue: 'Exit fullscreen' })}
274+
>
275+
<Minimize2 className="h-4 w-4" />
276+
</Button>
277+
</div>
278+
<Button
279+
type="button"
280+
size="icon"
281+
variant="default"
282+
className="absolute right-2 top-2 z-20 h-9 w-9 rounded-full shadow-lg sm:hidden"
283+
onClick={handleToggleFullscreen}
284+
aria-label={t('exitFullscreen', { defaultValue: 'Exit fullscreen' })}
285+
>
286+
<Minimize2 className="h-4 w-4" />
287+
</Button>
288+
<div className="relative min-h-0 w-full flex-1">
289+
{isEditorFullscreen && renderEditor()}
290+
</div>
291+
{footer}
292+
</DialogContent>
293+
</Dialog>
296294
</>
297295
)
298296
}

dashboard/src/features/nodes/dialogs/core-config-modal.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -841,11 +841,6 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
841841
dialogOpen={isDialogOpen}
842842
onFullscreenChange={setIsCodeEditorFullscreen}
843843
embeddedContainerClassName="h-[calc(50vh-1rem)] sm:h-[calc(55vh-1rem)] md:h-[600px]"
844-
fullscreenTitle={
845-
isXrayBackend
846-
? t('coreConfigModal.xrayConfigurationTitle', { defaultValue: 'Xray Core Configuration' })
847-
: t('coreConfigModal.wireguardConfigurationTitle', { defaultValue: 'WireGuard Core Configuration' })
848-
}
849844
/>
850845
</FormControl>
851846
{validation.error && !validation.isValid && <FormMessage>{validation.error}</FormMessage>}

dashboard/src/features/templates/dialogs/client-template-modal.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,6 @@ export default function ClientTemplateModal({ isDialogOpen, onOpenChange, form,
205205
dialogOpen={isDialogOpen}
206206
onFullscreenChange={setIsCodeEditorFullscreen}
207207
embeddedContainerClassName="h-[calc(50vh-1rem)] sm:h-[calc(55vh-1rem)] md:min-h-[450px]"
208-
fullscreenTitle={title}
209208
/>
210209
</FormControl>
211210
{validation.error && !validation.isValid && <FormMessage>{validation.error}</FormMessage>}

0 commit comments

Comments
 (0)