Fix frozen pane drags across webviews (#2453)

- Keep Electron webviews in pointer passthrough for renderer-owned tab and
  pane drags so they cannot steal the pointer stream
- Clean up terminal pane drag state on pointer cancel, lost capture, blur, or
  manager destroy to remove stale overlays and unfreeze input
- Cover nested drag passthrough and pane-drag cancellation paths with tests
This commit is contained in:
Jinjing 2026-05-20 15:19:40 -07:00 committed by GitHub
parent 2fee04a05b
commit ee27616388
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 491 additions and 34 deletions

View File

@ -90,6 +90,31 @@ describe('webview registry drag listeners', () => {
expect(unregisterGuestMock).toHaveBeenCalledWith({ browserPageId: 'page-2' })
})
it('releases native drag passthrough when the last webview is destroyed', async () => {
const { destroyPersistentWebview, registerPersistentWebview } =
await import('./webview-registry')
const firstWebview = createWebview()
firstWebview.style.pointerEvents = 'auto'
registerPersistentWebview('page-1', firstWebview)
const dragStart = addedListeners.find((entry) => entry.type === 'dragstart')?.listener
if (typeof dragStart === 'function') {
dragStart(new Event('dragstart'))
} else {
throw new Error('dragstart listener missing')
}
expect(firstWebview.style.pointerEvents).toBe('none')
destroyPersistentWebview('page-1')
const secondWebview = createWebview()
secondWebview.style.pointerEvents = 'auto'
registerPersistentWebview('page-2', secondWebview)
expect(secondWebview.style.pointerEvents).toBe('auto')
})
it('keeps one listener set across repeated registrations', async () => {
const { registerPersistentWebview } = await import('./webview-registry')
@ -99,6 +124,50 @@ describe('webview registry drag listeners', () => {
expect(addedListeners).toHaveLength(3)
})
it('keeps webviews in passthrough until every renderer drag releases', async () => {
const { acquireWebviewsDragPassthrough, registerPersistentWebview } =
await import('./webview-registry')
const activeWebview = createWebview()
activeWebview.style.pointerEvents = 'auto'
const lockedWebview = createWebview()
lockedWebview.style.pointerEvents = 'none'
registerPersistentWebview('page-1', activeWebview)
registerPersistentWebview('page-2', lockedWebview)
const releaseFirstDrag = acquireWebviewsDragPassthrough()
const releaseSecondDrag = acquireWebviewsDragPassthrough()
expect(activeWebview.style.pointerEvents).toBe('none')
expect(lockedWebview.style.pointerEvents).toBe('none')
releaseFirstDrag()
expect(activeWebview.style.pointerEvents).toBe('none')
expect(lockedWebview.style.pointerEvents).toBe('none')
releaseSecondDrag()
releaseSecondDrag()
expect(activeWebview.style.pointerEvents).toBe('auto')
expect(lockedWebview.style.pointerEvents).toBe('none')
})
it('applies active passthrough to webviews registered mid-drag', async () => {
const { acquireWebviewsDragPassthrough, registerPersistentWebview } =
await import('./webview-registry')
const releaseDrag = acquireWebviewsDragPassthrough()
const webview = createWebview()
webview.style.pointerEvents = 'auto'
registerPersistentWebview('page-1', webview)
expect(webview.style.pointerEvents).toBe('none')
releaseDrag()
expect(webview.style.pointerEvents).toBe('auto')
})
it('moves focus back to the renderer before detaching the focused webview', async () => {
const { moveFocusToRendererBeforeWebviewDetach } = await import('./webview-registry')
const webview = createWebview()

View File

@ -14,6 +14,9 @@ export const MAX_PARKED_WEBVIEWS = 6
let hiddenContainer: HTMLDivElement | null = null
const DRAG_LISTENER_KEY = '__orcaBrowserPaneDragListeners'
let dragListenersAttached = false
let nativeDragPassthroughRelease: (() => void) | null = null
const dragPassthroughTokens = new Set<symbol>()
const dragPassthroughPreviousPointerEvents = new Map<Electron.WebviewTag, string>()
type DragListenerRegistry = {
dragstart: () => void
@ -39,6 +42,8 @@ function removeDragListeners(): void {
window.removeEventListener('drop', existingListeners.drop, true)
delete listenerHost[DRAG_LISTENER_KEY]
dragListenersAttached = false
nativeDragPassthroughRelease?.()
nativeDragPassthroughRelease = null
}
function ensureDragListeners(): void {
@ -79,21 +84,80 @@ export function getHiddenContainer(): HTMLDivElement {
return hiddenContainer
}
export function setWebviewsDragPassthrough(passthrough: boolean): void {
function applyWebviewsDragPassthrough(): void {
const passthrough = dragPassthroughTokens.size > 0
for (const webview of webviewRegistry.values()) {
webview.style.pointerEvents = passthrough ? 'none' : ''
if (passthrough) {
if (!dragPassthroughPreviousPointerEvents.has(webview)) {
dragPassthroughPreviousPointerEvents.set(webview, webview.style.pointerEvents)
}
webview.style.pointerEvents = 'none'
continue
}
const previous = dragPassthroughPreviousPointerEvents.get(webview)
if (previous !== undefined) {
webview.style.pointerEvents = previous
dragPassthroughPreviousPointerEvents.delete(webview)
}
}
}
export function acquireWebviewsDragPassthrough(): () => void {
// Why: renderer-owned pointer drags (dnd-kit tab drags, terminal pane
// reorders) do not emit HTML dragstart/dragend, but Electron webviews can
// still steal the pointer stream unless they are temporarily transparent.
const token = Symbol('webview-drag-passthrough')
let released = false
dragPassthroughTokens.add(token)
applyWebviewsDragPassthrough()
return () => {
if (released) {
return
}
released = true
dragPassthroughTokens.delete(token)
applyWebviewsDragPassthrough()
}
}
export function setWebviewsDragPassthrough(passthrough: boolean): void {
if (passthrough) {
if (!nativeDragPassthroughRelease) {
nativeDragPassthroughRelease = acquireWebviewsDragPassthrough()
}
return
}
nativeDragPassthroughRelease?.()
nativeDragPassthroughRelease = null
}
function applyCurrentDragPassthroughToWebview(webview: Electron.WebviewTag): void {
if (dragPassthroughTokens.size === 0) {
return
}
if (!dragPassthroughPreviousPointerEvents.has(webview)) {
dragPassthroughPreviousPointerEvents.set(webview, webview.style.pointerEvents)
}
webview.style.pointerEvents = 'none'
}
export function registerPersistentWebview(
browserTabId: string,
webview: Electron.WebviewTag
): void {
webviewRegistry.set(browserTabId, webview)
applyCurrentDragPassthroughToWebview(webview)
ensureDragListeners()
}
export function unregisterPersistentWebview(browserTabId: string): void {
const webview = webviewRegistry.get(browserTabId)
if (webview) {
dragPassthroughPreviousPointerEvents.delete(webview)
}
webviewRegistry.delete(browserTabId)
if (webviewRegistry.size === 0) {
removeDragListeners()

View File

@ -1,7 +1,7 @@
/* oxlint-disable max-lines -- Why: the drag-split hook co-locates drop-zone
* resolution, same-group reordering, and cross-group handoff so state
* transitions stay readable in one place. */
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
closestCenter,
pointerWithin,
@ -28,6 +28,7 @@ import {
useHoveredTabInsertion,
type HoveredTabInsertion
} from './tab-insertion'
import { acquireWebviewsDragPassthrough } from '../browser-pane/webview-registry'
export type { HoveredTabInsertion }
@ -203,6 +204,7 @@ export function useTabDragSplit({
const dropUnifiedTab = useAppStore((state) => state.dropUnifiedTab)
const [activeDrag, setActiveDrag] = useState<TabDragItemData | null>(null)
const [hoveredDropTarget, setHoveredDropTarget] = useState<HoveredTabDropTarget | null>(null)
const releaseWebviewDragPassthroughRef = useRef<(() => void) | null>(null)
const tabInsertion = useHoveredTabInsertion(isTabDragData, getDragCenter)
// Why: hidden worktrees stay mounted so their PTYs survive worktree
@ -216,11 +218,26 @@ export function useTabDragSplit({
})
const sensors = useSensors(pointerSensor)
const releaseWebviewDragPassthrough = useCallback(() => {
releaseWebviewDragPassthroughRef.current?.()
releaseWebviewDragPassthroughRef.current = null
}, [])
const acquireWebviewDragPassthrough = useCallback(() => {
// Why: dnd-kit tab drags are pointer-driven, so the native drag listeners
// in webview-registry never fire. Put webviews in passthrough explicitly.
releaseWebviewDragPassthrough()
releaseWebviewDragPassthroughRef.current = acquireWebviewsDragPassthrough()
}, [releaseWebviewDragPassthrough])
useEffect(() => () => releaseWebviewDragPassthrough(), [releaseWebviewDragPassthrough])
const clearDragState = useCallback(() => {
releaseWebviewDragPassthrough()
setActiveDrag(null)
setHoveredDropTarget(null)
tabInsertion.clear()
}, [tabInsertion])
}, [releaseWebviewDragPassthrough, tabInsertion])
const updateHoveredPane = useCallback(
(event: DragMoveEvent | DragOverEvent) => {
@ -279,8 +296,9 @@ export function useTabDragSplit({
}
setActiveDrag(dragData)
acquireWebviewDragPassthrough()
},
[clearDragState, worktreeId]
[acquireWebviewDragPassthrough, clearDragState, worktreeId]
)
const onDragMove = useCallback(

View File

@ -57,6 +57,7 @@ import {
type SplitTerminalPaneDetail,
type CloseTerminalPaneDetail
} from '@/constants/terminal'
import { acquireWebviewsDragPassthrough } from '../browser-pane/webview-registry'
type UseTerminalPaneLifecycleDeps = {
tabId: string
@ -433,6 +434,8 @@ export function useTerminalPaneLifecycle({
const fileOpenLinkHint = getTerminalFileOpenHint()
const urlOpenLinkHint = getTerminalUrlOpenHint()
let releaseWebviewDragPassthrough: (() => void) | null = null
const manager = new PaneManager(container, {
// Why: `spawnHints` carries the resolved cwd from Cmd+D / context-menu
// Split actions so the new PTY inherits the source pane's live cwd.
@ -779,6 +782,15 @@ export function useTerminalPaneLifecycle({
persistLayoutSnapshot()
}
},
onPaneDragActiveChange: (active) => {
if (active) {
releaseWebviewDragPassthrough?.()
releaseWebviewDragPassthrough = acquireWebviewsDragPassthrough()
return
}
releaseWebviewDragPassthrough?.()
releaseWebviewDragPassthrough = null
},
terminalOptions: () => {
const currentSettings = settingsRef.current
const terminalFontWeights = resolveTerminalFontWeights(currentSettings?.terminalFontWeight)
@ -1076,6 +1088,8 @@ export function useTerminalPaneLifecycle({
panePtyBindings.clear()
paneTransports.clear()
manager.destroy()
releaseWebviewDragPassthrough?.()
releaseWebviewDragPassthrough = null
managerRef.current = null
if (e2eConfig.exposeStore) {
window.__paneManagers?.delete(tabId)

View File

@ -0,0 +1,221 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ManagedPaneInternal } from './pane-manager-types'
import { attachPaneDrag, createDragReorderState } from './pane-drag-reorder'
import type { TerminalLeafId } from '../../../../shared/stable-pane-id'
const detachPaneFromTree = vi.hoisted(() => vi.fn())
const insertPaneNextTo = vi.hoisted(() => vi.fn())
vi.mock('./pane-tree-ops', () => ({
detachPaneFromTree,
insertPaneNextTo
}))
type FakeListener = (event: PointerEvent) => void
class FakeClassList {
private readonly values = new Set<string>()
constructor(initial: readonly string[] = []) {
for (const value of initial) {
this.values.add(value)
}
}
add(value: string): void {
this.values.add(value)
}
remove(value: string): void {
this.values.delete(value)
}
contains(value: string): boolean {
return this.values.has(value)
}
}
class FakeElement {
readonly classList: FakeClassList
readonly style: Record<string, string> = {}
readonly dataset: Record<string, string> = {}
private readonly listeners = new Map<string, Set<FakeListener>>()
private readonly capturedPointerIds = new Set<number>()
removed = false
constructor(
classNames: readonly string[] = [],
private readonly rect = { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
) {
this.classList = new FakeClassList(classNames)
}
addEventListener(type: string, listener: FakeListener): void {
const listeners = this.listeners.get(type) ?? new Set<FakeListener>()
listeners.add(listener)
this.listeners.set(type, listeners)
}
removeEventListener(type: string, listener: FakeListener): void {
this.listeners.get(type)?.delete(listener)
}
dispatchPointer(type: string, event: Partial<PointerEvent>): void {
for (const listener of this.listeners.get(type) ?? []) {
listener(event as PointerEvent)
}
}
setPointerCapture(pointerId: number): void {
this.capturedPointerIds.add(pointerId)
}
releasePointerCapture(pointerId: number): void {
this.capturedPointerIds.delete(pointerId)
}
hasPointerCapture(pointerId: number): boolean {
return this.capturedPointerIds.has(pointerId)
}
getBoundingClientRect(): DOMRect {
return this.rect as DOMRect
}
remove(): void {
this.removed = true
}
}
function pointerEvent(args: Partial<PointerEvent>): PointerEvent {
return {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
pointerId: 1,
clientX: 0,
clientY: 0,
...args
} as unknown as PointerEvent
}
function createPane(id: number, container: FakeElement): ManagedPaneInternal {
const leafId =
`${id}${id}${id}${id}${id}${id}${id}${id}-${id}${id}${id}${id}-4${id}${id}${id}-8${id}${id}${id}-${id}${id}${id}${id}${id}${id}${id}${id}${id}${id}${id}${id}` as TerminalLeafId
container.dataset.paneId = String(id)
container.dataset.leafId = leafId
return {
id,
leafId,
stablePaneId: leafId,
terminal: {} as never,
container: container as unknown as HTMLElement,
xtermContainer: {} as never,
linkTooltip: {} as never,
terminalGpuAcceleration: 'auto',
gpuRenderingEnabled: true,
webglAttachmentDeferred: false,
webglDisabledAfterContextLoss: false,
hasComplexScriptOutput: false,
webglAddon: null,
ligaturesAddon: null,
fitResizeObserver: null,
pendingObservedFitRafId: null,
fitAddon: {} as never,
searchAddon: {} as never,
serializeAddon: {} as never,
unicode11Addon: {} as never,
webLinksAddon: {} as never,
compositionHandler: null,
pendingSplitScrollState: null,
debugLabel: null
}
}
describe('attachPaneDrag', () => {
let appendedElements: FakeElement[]
beforeEach(() => {
vi.clearAllMocks()
appendedElements = []
vi.stubGlobal('document', {
createElement: () => new FakeElement(['pane-drop-overlay']),
body: {
appendChild: (element: FakeElement) => {
appendedElements.push(element)
}
}
})
vi.stubGlobal('window', {
scrollX: 0,
scrollY: 0,
addEventListener: vi.fn(),
removeEventListener: vi.fn()
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('cleans pane drag state when pointer capture is cancelled', () => {
const handle = new FakeElement()
const root = new FakeElement(['pane-manager-root'])
const sourceContainer = new FakeElement(['pane'], {
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100
})
const targetContainer = new FakeElement(['pane'], {
left: 0,
top: 100,
right: 100,
bottom: 200,
width: 100,
height: 100
})
const sourcePane = createPane(1, sourceContainer)
const targetPane = createPane(2, targetContainer)
const panes = new Map<number, ManagedPaneInternal>([
[sourcePane.id, sourcePane],
[targetPane.id, targetPane]
])
const onDragActiveChange = vi.fn()
const state = createDragReorderState()
attachPaneDrag(handle as unknown as HTMLElement, sourcePane.id, state, {
getPanes: () => panes,
getRoot: () => root as unknown as HTMLElement,
getStyleOptions: () => ({}),
isDestroyed: () => false,
safeFit: vi.fn(),
applyPaneOpacity: vi.fn(),
applyDividerStyles: vi.fn(),
refitPanesUnder: vi.fn(),
onDragActiveChange
})
handle.dispatchPointer('pointerdown', pointerEvent({ clientX: 10, clientY: 10 }))
handle.dispatchPointer('pointermove', pointerEvent({ clientX: 50, clientY: 150 }))
expect(root.classList.contains('is-pane-dragging')).toBe(true)
expect(sourceContainer.classList.contains('is-drag-source')).toBe(true)
expect(state.currentDropTarget).toEqual({ paneId: targetPane.id, zone: 'top' })
expect(appendedElements).toHaveLength(1)
expect(onDragActiveChange).toHaveBeenCalledWith(true)
handle.dispatchPointer('pointercancel', pointerEvent({ pointerId: 1 }))
expect(root.classList.contains('is-pane-dragging')).toBe(false)
expect(sourceContainer.classList.contains('is-drag-source')).toBe(false)
expect(appendedElements[0].removed).toBe(true)
expect(state.dragSourcePaneId).toBeNull()
expect(state.currentDropTarget).toBeNull()
expect(state.cleanupActiveDrag).toBeNull()
expect(onDragActiveChange).toHaveBeenLastCalledWith(false)
expect(detachPaneFromTree).not.toHaveBeenCalled()
expect(insertPaneNextTo).not.toHaveBeenCalled()
})
})

View File

@ -10,6 +10,7 @@ export type DragReorderState = {
dragSourcePaneId: number | null
dropOverlay: HTMLElement | null
currentDropTarget: { paneId: number; zone: DropZone } | null
cleanupActiveDrag: ((commitDrop: boolean) => void) | null
}
export type DragReorderCallbacks = {
@ -22,13 +23,15 @@ export type DragReorderCallbacks = {
applyDividerStyles: () => void
refitPanesUnder: (el: HTMLElement) => void
onLayoutChanged?: () => void
onDragActiveChange?: (active: boolean) => void
}
export function createDragReorderState(): DragReorderState {
return {
dragSourcePaneId: null,
dropOverlay: null,
currentDropTarget: null
currentDropTarget: null,
cleanupActiveDrag: null
}
}
@ -42,6 +45,7 @@ export function attachPaneDrag(
let dragging = false
let startX = 0
let startY = 0
let activePointerId: number | null = null
const DRAG_THRESHOLD = 5
const onPointerDown = (e: PointerEvent): void => {
@ -52,17 +56,70 @@ export function attachPaneDrag(
e.preventDefault()
e.stopPropagation()
handle.setPointerCapture(e.pointerId)
activePointerId = e.pointerId
startX = e.clientX
startY = e.clientY
dragging = false
const cleanupDrag = (commitDrop: boolean): void => {
const pointerId = activePointerId
handle.removeEventListener('pointermove', onPointerMoveOuter)
handle.removeEventListener('pointerup', onPointerUpOuter)
handle.removeEventListener('pointercancel', onPointerCancelOuter)
handle.removeEventListener('lostpointercapture', onLostPointerCaptureOuter)
window.removeEventListener('blur', onWindowBlur, true)
activePointerId = null
state.cleanupActiveDrag = null
if (pointerId !== null && handle.hasPointerCapture(pointerId)) {
handle.releasePointerCapture(pointerId)
}
if (!dragging) {
return
}
dragging = false
callbacks.getRoot().classList.remove('is-pane-dragging')
const sourcePane = callbacks.getPanes().get(paneId)
if (sourcePane) {
sourcePane.container.classList.remove('is-drag-source')
}
try {
if (commitDrop && state.currentDropTarget && state.dragSourcePaneId !== null) {
handlePaneDrop(
state.dragSourcePaneId,
state.currentDropTarget.paneId,
state.currentDropTarget.zone,
state,
callbacks
)
}
} finally {
// Why: pointer capture can be lost when a terminal-pane drag crosses
// an Electron webview. Always clear the visual/input drag state so the
// terminal does not stay frozen behind the blue drop overlay.
callbacks.onDragActiveChange?.(false)
hideDropOverlay(state)
state.dragSourcePaneId = null
state.currentDropTarget = null
}
}
const onPointerMoveOuter = (ev: PointerEvent): void => {
if (ev.pointerId !== activePointerId || callbacks.isDestroyed()) {
if (callbacks.isDestroyed()) {
cleanupDrag(false)
}
return
}
const dx = ev.clientX - startX
const dy = ev.clientY - startY
if (!dragging && Math.hypot(dx, dy) >= DRAG_THRESHOLD) {
dragging = true
state.dragSourcePaneId = paneId
callbacks.getRoot().classList.add('is-pane-dragging')
callbacks.onDragActiveChange?.(true)
const sourcePane = callbacks.getPanes().get(paneId)
if (sourcePane) {
sourcePane.container.classList.add('is-drag-source')
@ -75,41 +132,51 @@ export function attachPaneDrag(
}
const onPointerUpOuter = (ev: PointerEvent): void => {
handle.releasePointerCapture(ev.pointerId)
handle.removeEventListener('pointermove', onPointerMoveOuter)
handle.removeEventListener('pointerup', onPointerUpOuter)
if (dragging) {
callbacks.getRoot().classList.remove('is-pane-dragging')
const sourcePane = callbacks.getPanes().get(paneId)
if (sourcePane) {
sourcePane.container.classList.remove('is-drag-source')
}
// Execute the drop
if (state.currentDropTarget && state.dragSourcePaneId !== null) {
handlePaneDrop(
state.dragSourcePaneId,
state.currentDropTarget.paneId,
state.currentDropTarget.zone,
state,
callbacks
)
}
hideDropOverlay(state)
state.dragSourcePaneId = null
state.currentDropTarget = null
if (ev.pointerId !== activePointerId) {
return
}
cleanupDrag(true)
}
const onPointerCancelOuter = (ev: PointerEvent): void => {
if (ev.pointerId !== activePointerId) {
return
}
cleanupDrag(false)
}
const onLostPointerCaptureOuter = (ev: PointerEvent): void => {
if (ev.pointerId !== activePointerId) {
return
}
cleanupDrag(false)
}
const onWindowBlur = (): void => {
cleanupDrag(false)
}
state.cleanupActiveDrag = cleanupDrag
handle.addEventListener('pointermove', onPointerMoveOuter)
handle.addEventListener('pointerup', onPointerUpOuter)
handle.addEventListener('pointercancel', onPointerCancelOuter)
handle.addEventListener('lostpointercapture', onLostPointerCaptureOuter)
window.addEventListener('blur', onWindowBlur, true)
}
handle.addEventListener('pointerdown', onPointerDown)
}
export function cancelActivePaneDrag(state: DragReorderState): void {
if (state.cleanupActiveDrag) {
state.cleanupActiveDrag(false)
return
}
hideDropOverlay(state)
state.dragSourcePaneId = null
state.currentDropTarget = null
}
/** Move a pane from its current position to a new position relative to a target pane. */
export function handlePaneDrop(
sourcePaneId: number,

View File

@ -33,6 +33,9 @@ export type PaneManagerOptions = {
onPaneClosed?: (paneId: number, closedPane?: ClosedPaneInfo) => void
onActivePaneChange?: (pane: ManagedPane) => void
onLayoutChanged?: () => void
/** Why: Electron webviews can steal pointer streams from renderer-owned
* pane drags unless callers temporarily put them in pointer passthrough. */
onPaneDragActiveChange?: (active: boolean) => void
terminalOptions?: (paneId: number) => Partial<ITerminalOptions>
onLinkClick?: (event: MouseEvent | undefined, url: string) => void
initialRenderingSuspended?: boolean

View File

@ -12,7 +12,7 @@ import {
applyPaneOpacity,
applyRootBackground
} from './pane-divider'
import { createDragReorderState, hideDropOverlay, handlePaneDrop } from './pane-drag-reorder'
import { cancelActivePaneDrag, createDragReorderState, handlePaneDrop } from './pane-drag-reorder'
import { createPaneDOM, openTerminal, setLigaturesEnabled, disposePane } from './pane-lifecycle'
import { shouldFollowMouseFocus } from './focus-follows-mouse'
import {
@ -257,7 +257,7 @@ export class PaneManager {
destroy(): void {
this.destroyed = true
hideDropOverlay(this.dragState)
cancelActivePaneDrag(this.dragState)
for (const pane of this.panes.values()) {
disposePane(pane, this.panes)
}
@ -335,7 +335,8 @@ export class PaneManager {
applyPaneOpacity(this.panes.values(), this.activePaneId, this.styleOptions),
applyDividerStyles: () => applyDividerStyles(this.root, this.styleOptions),
refitPanesUnder: (el: HTMLElement) => refitPanesUnder(el, this.panes),
onLayoutChanged: this.options.onLayoutChanged
onLayoutChanged: this.options.onLayoutChanged,
onDragActiveChange: this.options.onPaneDragActiveChange
}
}
}