diff --git a/packages/dockview-core/src/__tests__/__test_utils__/utils.ts b/packages/dockview-core/src/__tests__/__test_utils__/utils.ts index e0c7e7ada..16d73ba3e 100644 --- a/packages/dockview-core/src/__tests__/__test_utils__/utils.ts +++ b/packages/dockview-core/src/__tests__/__test_utils__/utils.ts @@ -44,3 +44,30 @@ export function createOffsetDragOverEvent(params: { export function exhaustMicrotaskQueue(): Promise { return new Promise((resolve) => resolve()); } + +export const mockGetBoundingClientRect = ({ + left, + top, + height, + width, +}: { + left: number; + top: number; + height: number; + width: number; +}) => { + const result = { + left, + top, + height, + width, + right: left + width, + bottom: top + height, + x: left, + y: top, + }; + return { + ...result, + toJSON: () => result, + }; +}; diff --git a/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts index df57caa66..95ce9f4a4 100644 --- a/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts @@ -70,8 +70,8 @@ describe('abstractDragHandler', () => { expect(span.style.pointerEvents).toBeFalsy(); fireEvent.dragEnd(element); - expect(iframe.style.pointerEvents).toBe('auto'); - expect(webview.style.pointerEvents).toBe('auto'); + expect(iframe.style.pointerEvents).toBe(''); + expect(webview.style.pointerEvents).toBe(''); expect(span.style.pointerEvents).toBeFalsy(); handler.dispose(); @@ -114,8 +114,8 @@ describe('abstractDragHandler', () => { expect(span.style.pointerEvents).toBeFalsy(); handler.dispose(); - expect(iframe.style.pointerEvents).toBe('auto'); - expect(webview.style.pointerEvents).toBe('auto'); + expect(iframe.style.pointerEvents).toBe(''); + expect(webview.style.pointerEvents).toBe(''); expect(span.style.pointerEvents).toBeFalsy(); }); @@ -172,7 +172,7 @@ describe('abstractDragHandler', () => { const event = new Event('dragstart'); const spy = jest.spyOn(event, 'preventDefault'); fireEvent(element, event); - expect(spy).toBeCalledTimes(0); + expect(spy).toHaveBeenCalledTimes(0); handler.dispose(); }); diff --git a/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts index c1ab25d94..838c480a7 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts @@ -1,5 +1,4 @@ import { fireEvent } from '@testing-library/dom'; -import { Emitter, Event } from '../../../../events'; import { ContentContainer } from '../../../../dockview/components/panel/content'; import { GroupPanelPartInitParameters, @@ -10,9 +9,9 @@ import { PanelUpdateEvent } from '../../../../panel/types'; import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel'; import { DockviewComponent } from '../../../../dockview/dockviewComponent'; -import { OverlayRenderContainer } from '../../../../overlayRenderContainer'; import { fromPartial } from '@total-typescript/shoehorn'; import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel'; +import { OverlayRenderContainer } from '../../../../overlay/overlayRenderContainer'; class TestContentRenderer extends CompositeDisposable @@ -58,7 +57,8 @@ describe('contentContainer', () => { const disposable = new CompositeDisposable(); const overlayRenderContainer = new OverlayRenderContainer( - document.createElement('div') + document.createElement('div'), + fromPartial({}) ); const cut = new ContentContainer( diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/voidContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/voidContainer.spec.ts new file mode 100644 index 000000000..64a8381a2 --- /dev/null +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/voidContainer.spec.ts @@ -0,0 +1,20 @@ +import { VoidContainer } from '../../../../dockview/components/titlebar/voidContainer'; +import { fromPartial } from '@total-typescript/shoehorn'; +import { DockviewComponent } from '../../../../dockview/dockviewComponent'; +import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel'; +import { fireEvent } from '@testing-library/dom'; + +describe('voidContainer', () => { + test('that `pointerDown` triggers activation', () => { + const accessor = fromPartial({ + doSetGroupActive: jest.fn(), + }); + const group = fromPartial({}); + const cut = new VoidContainer(accessor, group); + + expect(accessor.doSetGroupActive).not.toHaveBeenCalled(); + + fireEvent.pointerDown(cut.element); + expect(accessor.doSetGroupActive).toHaveBeenCalledWith(group); + }); +}); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 59e76b1e4..1f83b8221 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -30,7 +30,7 @@ import { createOffsetDragOverEvent } from '../__test_utils__/utils'; import { DockviewPanelRenderer, OverlayRenderContainer, -} from '../../overlayRenderContainer'; +} from '../../overlay/overlayRenderContainer'; import { DockviewGroupPanelFloatingChangeEvent } from '../../api/dockviewGroupPanelApi'; import { SizeEvent } from '../../api/gridviewPanelApi'; import { @@ -285,7 +285,8 @@ describe('dockviewGroupPanelModel', () => { onDidAddPanel: () => ({ dispose: jest.fn() }), onDidRemovePanel: () => ({ dispose: jest.fn() }), overlayRenderContainer: new OverlayRenderContainer( - document.createElement('div') + document.createElement('div'), + fromPartial({}) ), }); @@ -823,7 +824,8 @@ describe('dockviewGroupPanelModel', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), overlayRenderContainer: new OverlayRenderContainer( - document.createElement('div') + document.createElement('div'), + fromPartial({}) ), }); @@ -894,7 +896,8 @@ describe('dockviewGroupPanelModel', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), overlayRenderContainer: new OverlayRenderContainer( - document.createElement('div') + document.createElement('div'), + fromPartial({}) ), }); @@ -966,7 +969,8 @@ describe('dockviewGroupPanelModel', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), overlayRenderContainer: new OverlayRenderContainer( - document.createElement('div') + document.createElement('div'), + fromPartial({}) ), }); diff --git a/packages/dockview-core/src/__tests__/dom.spec.ts b/packages/dockview-core/src/__tests__/dom.spec.ts index c8eff360a..2ad46a4fe 100644 --- a/packages/dockview-core/src/__tests__/dom.spec.ts +++ b/packages/dockview-core/src/__tests__/dom.spec.ts @@ -1,4 +1,5 @@ import { + disableIframePointEvents, isInDocument, quasiDefaultPrevented, quasiPreventDefault, @@ -45,4 +46,38 @@ describe('dom', () => { expect(isInDocument(el2)).toBeTruthy(); }); + + test('disableIframePointEvents', () => { + const el1 = document.createElement('iframe'); + const el2 = document.createElement('iframe'); + const el3 = document.createElement('webview'); + const el4 = document.createElement('webview'); + + document.body.appendChild(el1); + document.body.appendChild(el2); + document.body.appendChild(el3); + document.body.appendChild(el4); + + el1.style.pointerEvents = 'inherit'; + el3.style.pointerEvents = 'inherit'; + + expect(el1.style.pointerEvents).toBe('inherit'); + expect(el2.style.pointerEvents).toBe(''); + expect(el3.style.pointerEvents).toBe('inherit'); + expect(el4.style.pointerEvents).toBe(''); + + const f = disableIframePointEvents(); + + expect(el1.style.pointerEvents).toBe('none'); + expect(el2.style.pointerEvents).toBe('none'); + expect(el3.style.pointerEvents).toBe('none'); + expect(el4.style.pointerEvents).toBe('none'); + + f.release(); + + expect(el1.style.pointerEvents).toBe('inherit'); + expect(el2.style.pointerEvents).toBe(''); + expect(el3.style.pointerEvents).toBe('inherit'); + expect(el4.style.pointerEvents).toBe(''); + }); }); diff --git a/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts b/packages/dockview-core/src/__tests__/overlay/overlay.spec.ts similarity index 78% rename from packages/dockview-core/src/__tests__/dnd/overlay.spec.ts rename to packages/dockview-core/src/__tests__/overlay/overlay.spec.ts index f7f7fca9c..90bdbec99 100644 --- a/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts +++ b/packages/dockview-core/src/__tests__/overlay/overlay.spec.ts @@ -1,31 +1,5 @@ -import { Overlay } from '../../dnd/overlay'; - -const mockGetBoundingClientRect = ({ - left, - top, - height, - width, -}: { - left: number; - top: number; - height: number; - width: number; -}) => { - const result = { - left, - top, - height, - width, - right: left + width, - bottom: top + height, - x: left, - y: top, - }; - return { - ...result, - toJSON: () => result, - }; -}; +import { Overlay } from '../../overlay/overlay'; +import { mockGetBoundingClientRect } from '../__test_utils__/utils'; describe('overlay', () => { test('toJSON, top left', () => { @@ -76,6 +50,8 @@ describe('overlay', () => { width: 40, height: 50, }); + + cut.dispose(); }); test('toJSON, bottom right', () => { @@ -126,6 +102,8 @@ describe('overlay', () => { width: 40, height: 50, }); + + cut.dispose(); }); test('that out-of-bounds dimensions are fixed, top left', () => { @@ -176,6 +154,8 @@ describe('overlay', () => { width: 40, height: 50, }); + + cut.dispose(); }); test('that out-of-bounds dimensions are fixed, bottom right', () => { @@ -226,6 +206,8 @@ describe('overlay', () => { width: 40, height: 50, }); + + cut.dispose(); }); test('setBounds, top left', () => { @@ -276,6 +258,8 @@ describe('overlay', () => { expect(element.style.width).toBe('200px'); expect(element.style.left).toBe('300px'); expect(element.style.top).toBe('400px'); + + cut.dispose(); }); test('setBounds, bottom right', () => { @@ -326,6 +310,8 @@ describe('overlay', () => { expect(element.style.width).toBe('200px'); expect(element.style.right).toBe('300px'); expect(element.style.bottom).toBe('400px'); + + cut.dispose(); }); test('that the resize handles are added', () => { @@ -364,4 +350,66 @@ describe('overlay', () => { cut.dispose(); }); + + test('aria-level attributes and corresponding z-index', () => { + const container = document.createElement('div'); + const content = document.createElement('div'); + + const createOverlay = () => + new Overlay({ + height: 500, + width: 500, + left: 100, + top: 200, + minimumInViewportWidth: 0, + minimumInViewportHeight: 0, + container, + content, + }); + + const overlay1 = createOverlay(); + + expect(overlay1.element.getAttribute('aria-level')).toBe('0'); + expect(overlay1.element.style.zIndex).toBe('999'); + + const overlay2 = createOverlay(); + const overlay3 = createOverlay(); + + expect(overlay1.element.getAttribute('aria-level')).toBe('0'); + expect(overlay2.element.getAttribute('aria-level')).toBe('1'); + expect(overlay3.element.getAttribute('aria-level')).toBe('2'); + expect(overlay1.element.style.zIndex).toBe('999'); + expect(overlay2.element.style.zIndex).toBe('1001'); + expect(overlay3.element.style.zIndex).toBe('1003'); + + overlay2.bringToFront(); + + expect(overlay1.element.getAttribute('aria-level')).toBe('0'); + expect(overlay2.element.getAttribute('aria-level')).toBe('2'); + expect(overlay3.element.getAttribute('aria-level')).toBe('1'); + expect(overlay1.element.style.zIndex).toBe('999'); + expect(overlay2.element.style.zIndex).toBe('1003'); + expect(overlay3.element.style.zIndex).toBe('1001'); + + overlay1.bringToFront(); + + expect(overlay1.element.getAttribute('aria-level')).toBe('2'); + expect(overlay2.element.getAttribute('aria-level')).toBe('1'); + expect(overlay3.element.getAttribute('aria-level')).toBe('0'); + expect(overlay1.element.style.zIndex).toBe('1003'); + expect(overlay2.element.style.zIndex).toBe('1001'); + expect(overlay3.element.style.zIndex).toBe('999'); + + overlay2.dispose(); + + expect(overlay1.element.getAttribute('aria-level')).toBe('1'); + expect(overlay3.element.getAttribute('aria-level')).toBe('0'); + expect(overlay1.element.style.zIndex).toBe('1001'); + expect(overlay3.element.style.zIndex).toBe('999'); + + overlay1.dispose(); + + expect(overlay3.element.getAttribute('aria-level')).toBe('0'); + expect(overlay3.element.style.zIndex).toBe('999'); + }); }); diff --git a/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts b/packages/dockview-core/src/__tests__/overlay/overlayRenderContainer.spec.ts similarity index 88% rename from packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts rename to packages/dockview-core/src/__tests__/overlay/overlayRenderContainer.spec.ts index 81310060b..1fa37cab8 100644 --- a/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/overlay/overlayRenderContainer.spec.ts @@ -1,9 +1,13 @@ -import { Droptarget } from '../dnd/droptarget'; -import { IDockviewPanel } from '../dockview/dockviewPanel'; -import { Emitter } from '../events'; -import { IRenderable, OverlayRenderContainer } from '../overlayRenderContainer'; +import { Droptarget } from '../../dnd/droptarget'; +import { IDockviewPanel } from '../../dockview/dockviewPanel'; +import { Emitter } from '../../events'; +import { + IRenderable, + OverlayRenderContainer, +} from '../../overlay/overlayRenderContainer'; import { fromPartial } from '@total-typescript/shoehorn'; -import { Writable, exhaustMicrotaskQueue } from './__test_utils__/utils'; +import { Writable, exhaustMicrotaskQueue } from '../__test_utils__/utils'; +import { DockviewComponent } from '../../dockview/dockviewComponent'; describe('overlayRenderContainer', () => { let referenceContainer: IRenderable; @@ -18,7 +22,10 @@ describe('overlayRenderContainer', () => { dropTarget: fromPartial({}), }; - cut = new OverlayRenderContainer(parentContainer); + cut = new OverlayRenderContainer( + parentContainer, + fromPartial({}) + ); }); test('that attach(...) and detach(...) mutate the DOM as expected', () => { @@ -26,12 +33,14 @@ describe('overlayRenderContainer', () => { const onDidVisibilityChange = new Emitter(); const onDidDimensionsChange = new Emitter(); + const onDidLocationChange = new Emitter(); const panel = fromPartial({ api: { id: 'test_panel_id', onDidVisibilityChange: onDidVisibilityChange.event, onDidDimensionsChange: onDidDimensionsChange.event, + onDidLocationChange: onDidLocationChange.event, isVisible: true, }, view: { @@ -62,12 +71,14 @@ describe('overlayRenderContainer', () => { const onDidVisibilityChange = new Emitter(); const onDidDimensionsChange = new Emitter(); + const onDidLocationChange = new Emitter(); const panel = fromPartial({ api: { id: 'test_panel_id', onDidVisibilityChange: onDidVisibilityChange.event, onDidDimensionsChange: onDidDimensionsChange.event, + onDidLocationChange: onDidLocationChange.event, isVisible: true, }, view: { diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index e640bc716..7afe192e5 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -5,7 +5,7 @@ import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { DockviewPanel } from '../dockview/dockviewPanel'; import { DockviewComponent } from '../dockview/dockviewComponent'; import { Position } from '../dnd/droptarget'; -import { DockviewPanelRenderer } from '../overlayRenderContainer'; +import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; import { DockviewGroupPanelFloatingChangeEvent } from './dockviewGroupPanelApi'; import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel'; diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index 306ab236e..21298f74b 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -1,4 +1,4 @@ -import { getElementsByTagName } from '../dom'; +import { disableIframePointEvents, getElementsByTagName } from '../dom'; import { addDisposableListener, Emitter } from '../events'; import { CompositeDisposable, @@ -40,23 +40,14 @@ export abstract class DragHandler extends CompositeDisposable { return; } - const iframes = [ - ...getElementsByTagName('iframe'), - ...getElementsByTagName('webview'), - ]; + const iframes = disableIframePointEvents(); this.pointerEventsDisposable.value = { dispose: () => { - for (const iframe of iframes) { - iframe.style.pointerEvents = 'auto'; - } + iframes.release(); }, }; - for (const iframe of iframes) { - iframe.style.pointerEvents = 'none'; - } - this.el.classList.add('dv-dragged'); setTimeout(() => this.el.classList.remove('dv-dragged'), 0); diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index c39d141bb..bece6440b 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -3,7 +3,12 @@ import { IDisposable, MutableDisposable, } from '../../../lifecycle'; -import { Emitter, Event } from '../../../events'; +import { + addDisposableListener, + addDisposableWindowListener, + Emitter, + Event, +} from '../../../events'; import { trackFocus } from '../../../dom'; import { IDockviewPanel } from '../../dockviewPanel'; import { DockviewComponent } from '../../dockviewComponent'; diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index 5709273a8..308580a63 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts @@ -42,7 +42,7 @@ export class VoidContainer extends CompositeDisposable { this.addDisposables( this._onDrop, this._onDragStart, - addDisposableListener(this._element, 'click', () => { + addDisposableListener(this._element, 'pointerdown', () => { this.accessor.doSetGroupActive(this.group); }) ); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index be0af1dba..c86e05d17 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -50,7 +50,7 @@ import { DockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewPanelModel } from './dockviewPanelModel'; import { getPanelData } from '../dnd/dataTransfer'; import { Parameters } from '../panel/types'; -import { Overlay } from '../dnd/overlay'; +import { Overlay } from '../overlay/overlay'; import { addTestId, toggleClass, watchElementResize } from '../dom'; import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel'; import { @@ -65,7 +65,7 @@ import { import { DockviewPanelRenderer, OverlayRenderContainer, -} from '../overlayRenderContainer'; +} from '../overlay/overlayRenderContainer'; import { PopoutWindow } from '../popoutWindow'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { @@ -357,6 +357,10 @@ export class DockviewComponent return this.gridview.margin; } + get floatingGroups(): DockviewFloatingGroupPanel[] { + return this._floatingGroups; + } + constructor(parentElement: HTMLElement, options: DockviewComponentOptions) { super({ proportionalLayout: true, @@ -371,11 +375,14 @@ export class DockviewComponent className: options.className, }); - const gready = document.createElement('div'); - gready.className = 'dv-overlay-render-container'; - this.gridview.element.appendChild(gready); + // const gready = document.createElement('div'); + // gready.className = 'dv-overlay-render-container'; + // this.gridview.element.appendChild(gready); - this.overlayRenderContainer = new OverlayRenderContainer(gready); + this.overlayRenderContainer = new OverlayRenderContainer( + this.gridview.element, + this + ); toggleClass(this.gridview.element, 'dv-dockview', true); toggleClass(this.element, 'dv-debug', !!options.debug); @@ -639,7 +646,8 @@ export class DockviewComponent gready.className = 'dv-overlay-render-container'; const overlayRenderContainer = new OverlayRenderContainer( - gready + gready, + this ); const referenceGroup = @@ -837,8 +845,6 @@ export class DockviewComponent } } - group.model.location = { type: 'floating' }; - function getAnchoredBox(): AnchoredBox { if (options?.position) { const result: any = {}; @@ -928,10 +934,17 @@ export class DockviewComponent overlay ); - const disposable = watchElementResize(group.element, (entry) => { - const { width, height } = entry.contentRect; - group.layout(width, height); // let the group know it's size is changing so it can fire events to the panel - }); + const disposable = new CompositeDisposable( + group.api.onDidActiveChange((event) => { + if (event.isActive) { + overlay.bringToFront(); + } + }), + watchElementResize(group.element, (entry) => { + const { width, height } = entry.contentRect; + group.layout(width, height); // let the group know it's size is changing so it can fire events to the panel + }) + ); floatingGroupPanel.addDisposables( overlay.onDidChange(() => { @@ -953,8 +966,8 @@ export class DockviewComponent dispose: () => { disposable.dispose(); - group.model.location = { type: 'grid' }; remove(this._floatingGroups, floatingGroupPanel); + group.model.location = { type: 'grid' }; this.updateWatermark(); }, } @@ -962,6 +975,8 @@ export class DockviewComponent this._floatingGroups.push(floatingGroupPanel); + group.model.location = { type: 'floating' }; + if (!options?.skipActiveGroup) { this.doSetGroupAndPanelActive(group); } diff --git a/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts index 9f4dacaa9..1d9172552 100644 --- a/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts @@ -1,4 +1,4 @@ -import { Overlay } from '../dnd/overlay'; +import { Overlay } from '../overlay/overlay'; import { CompositeDisposable } from '../lifecycle'; import { AnchoredBox } from '../types'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; @@ -10,7 +10,8 @@ export interface IDockviewFloatingGroupPanel { export class DockviewFloatingGroupPanel extends CompositeDisposable - implements IDockviewFloatingGroupPanel { + implements IDockviewFloatingGroupPanel +{ constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) { super(); this.addDisposables(overlay); diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 6f573fb74..574441eba 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -36,7 +36,7 @@ import { DockviewUnhandledDragOverEvent, IHeaderActionsRenderer, } from './options'; -import { OverlayRenderContainer } from '../overlayRenderContainer'; +import { OverlayRenderContainer } from '../overlay/overlayRenderContainer'; import { TitleEvent } from '../api/dockviewPanelApi'; interface GroupMoveEvent { @@ -964,7 +964,7 @@ export class DockviewGroupPanelModel }); this.watermark = watermark; - addDisposableListener(this.watermark.element, 'click', () => { + addDisposableListener(this.watermark.element, 'pointerdown', () => { if (!this.isActive) { this.accessor.doSetGroupActive(this.groupPanel); } diff --git a/packages/dockview-core/src/dockview/dockviewPanel.ts b/packages/dockview-core/src/dockview/dockviewPanel.ts index 2bcb32afc..63f27d62f 100644 --- a/packages/dockview-core/src/dockview/dockviewPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanel.ts @@ -9,7 +9,7 @@ import { CompositeDisposable, IDisposable } from '../lifecycle'; import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types'; import { IDockviewPanelModel } from './dockviewPanelModel'; import { DockviewComponent } from './dockviewComponent'; -import { DockviewPanelRenderer } from '../overlayRenderContainer'; +import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; import { WillFocusEvent } from '../api/panelApi'; export interface IDockviewPanel extends IDisposable, IPanel { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 0e8565aa1..5fce93ebf 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -12,7 +12,7 @@ import { GroupOptions, } from './dockviewGroupPanelModel'; import { IDockviewPanel } from './dockviewPanel'; -import { DockviewPanelRenderer } from '../overlayRenderContainer'; +import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; import { IGroupHeaderProps } from './framework'; import { AnchoredBox } from '../types'; import { FloatingGroupOptions } from './dockviewComponent'; diff --git a/packages/dockview-core/src/dockview/types.ts b/packages/dockview-core/src/dockview/types.ts index 8bff4e2fc..99c7c38d9 100644 --- a/packages/dockview-core/src/dockview/types.ts +++ b/packages/dockview-core/src/dockview/types.ts @@ -3,7 +3,7 @@ import { PanelInitParameters, IPanel } from '../panel/types'; import { DockviewApi } from '../api/component.api'; import { Optional } from '../types'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; -import { DockviewPanelRenderer } from '../overlayRenderContainer'; +import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; export interface HeaderPartInitParameters { title: string; diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index 7953d94dd..249b84181 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -257,3 +257,26 @@ export function isInDocument(element: Element): boolean { export function addTestId(element: HTMLElement, id: string): void { element.setAttribute('data-testid', id); } + +export function disableIframePointEvents() { + const iframes: HTMLElement[] = [ + ...getElementsByTagName('iframe'), + ...getElementsByTagName('webview'), + ]; + + const original = new WeakMap(); // don't hold onto HTMLElement references longer than required + + for (const iframe of iframes) { + original.set(iframe, iframe.style.pointerEvents); + iframe.style.pointerEvents = 'none'; + } + + return { + release: () => { + for (const iframe of iframes) { + iframe.style.pointerEvents = original.get(iframe) ?? 'auto'; + } + iframes.splice(0, iframes.length); // don't hold onto HTMLElement references longer than required + }, + }; +} diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index 57f9e3f00..b0f9a9ac7 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -64,7 +64,7 @@ export * from './splitview/splitviewPanel'; export * from './paneview/paneviewPanel'; export * from './dockview/types'; -export { DockviewPanelRenderer } from './overlayRenderContainer'; +export { DockviewPanelRenderer } from './overlay/overlayRenderContainer'; export { Position, diff --git a/packages/dockview-core/src/dnd/overlay.scss b/packages/dockview-core/src/overlay/overlay.scss similarity index 97% rename from packages/dockview-core/src/dnd/overlay.scss rename to packages/dockview-core/src/overlay/overlay.scss index 5f95b379a..971d8629c 100644 --- a/packages/dockview-core/src/dnd/overlay.scss +++ b/packages/dockview-core/src/overlay/overlay.scss @@ -29,10 +29,6 @@ position: absolute; z-index: 997; - &.dv-bring-to-front { - z-index: 998; - } - border: 1px solid var(--dv-tab-divider-color); box-shadow: var(--dv-floating-box-shadow); diff --git a/packages/dockview-core/src/dnd/overlay.ts b/packages/dockview-core/src/overlay/overlay.ts similarity index 94% rename from packages/dockview-core/src/dnd/overlay.ts rename to packages/dockview-core/src/overlay/overlay.ts index c10e2ea80..0f39961ce 100644 --- a/packages/dockview-core/src/dnd/overlay.ts +++ b/packages/dockview-core/src/overlay/overlay.ts @@ -1,5 +1,5 @@ import { - getElementsByTagName, + disableIframePointEvents, quasiDefaultPrevented, toggleClass, } from '../dom'; @@ -13,20 +13,36 @@ import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { clamp } from '../math'; import { AnchoredBox } from '../types'; -const bringElementToFront = (() => { - let previous: HTMLElement | null = null; +export const DEFAULT_OVERLAY_Z_INDEX = 999; - function pushToTop(element: HTMLElement) { - if (previous !== element && previous !== null) { - toggleClass(previous, 'dv-bring-to-front', false); - } +class AriaLevelTracker { + private _orderedList: HTMLElement[] = []; - toggleClass(element, 'dv-bring-to-front', true); - previous = element; + push(element: HTMLElement): void { + this._orderedList = [ + ...this._orderedList.filter((item) => item !== element), + element, + ]; + + this.update(); } - return pushToTop; -})(); + destroy(element: HTMLElement): void { + this._orderedList = this._orderedList.filter( + (item) => item !== element + ); + this.update(); + } + + private update(): void { + for (let i = 0; i < this._orderedList.length; i++) { + this._orderedList[i].setAttribute('aria-level', `${i}`); + this._orderedList[i].style.zIndex = `${DEFAULT_OVERLAY_Z_INDEX + i * 2}`; + } + } +} + +const arialLevelTracker = new AriaLevelTracker(); export class Overlay extends CompositeDisposable { private _element: HTMLElement = document.createElement('div'); @@ -51,6 +67,10 @@ export class Overlay extends CompositeDisposable { this.options.minimumInViewportHeight = value; } + get element(): HTMLElement { + return this._element; + } + constructor( private readonly options: AnchoredBox & { container: HTMLElement; @@ -86,6 +106,12 @@ export class Overlay extends CompositeDisposable { ...('left' in this.options && { left: this.options.left }), ...('right' in this.options && { right: this.options.right }), }); + + arialLevelTracker.push(this._element); + } + + bringToFront(): void { + arialLevelTracker.push(this._element); } setBounds(bounds: Partial = {}): void { @@ -207,21 +233,12 @@ export class Overlay extends CompositeDisposable { const track = () => { let offset: { x: number; y: number } | null = null; - const iframes = [ - ...getElementsByTagName('iframe'), - ...getElementsByTagName('webview'), - ]; - - for (const iframe of iframes) { - iframe.style.pointerEvents = 'none'; - } + const iframes = disableIframePointEvents(); move.value = new CompositeDisposable( { dispose: () => { - for (const iframe of iframes) { - iframe.style.pointerEvents = 'auto'; - } + iframes.release(); }, }, addDisposableWindowListener(window, 'mousemove', (e) => { @@ -362,14 +379,12 @@ export class Overlay extends CompositeDisposable { this.options.content, 'mousedown', () => { - bringElementToFront(this._element); + arialLevelTracker.push(this._element); }, true ) ); - bringElementToFront(this._element); - if (options.inDragMode) { track(); } @@ -404,14 +419,7 @@ export class Overlay extends CompositeDisposable { originalWidth: number; } | null = null; - const iframes = [ - ...getElementsByTagName('iframe'), - ...getElementsByTagName('webview'), - ]; - - for (const iframe of iframes) { - iframe.style.pointerEvents = 'none'; - } + const iframes = disableIframePointEvents(); move.value = new CompositeDisposable( addDisposableWindowListener(window, 'mousemove', (e) => { @@ -582,9 +590,7 @@ export class Overlay extends CompositeDisposable { }), { dispose: () => { - for (const iframe of iframes) { - iframe.style.pointerEvents = 'auto'; - } + iframes.release(); }, }, addDisposableWindowListener(window, 'mouseup', () => { @@ -611,6 +617,7 @@ export class Overlay extends CompositeDisposable { } override dispose(): void { + arialLevelTracker.destroy(this._element); this._element.remove(); super.dispose(); } diff --git a/packages/dockview-core/src/overlayReadyContainer.scss b/packages/dockview-core/src/overlay/overlayReadyContainer.scss similarity index 69% rename from packages/dockview-core/src/overlayReadyContainer.scss rename to packages/dockview-core/src/overlay/overlayReadyContainer.scss index 7e08072e4..fbd978841 100644 --- a/packages/dockview-core/src/overlayReadyContainer.scss +++ b/packages/dockview-core/src/overlay/overlayReadyContainer.scss @@ -4,7 +4,11 @@ height: 100%; &.dv-render-overlay-float { - z-index: 999; + z-index: 998; + + &.dv-render-overlay-active { + // z-index: 1000; + } } } diff --git a/packages/dockview-core/src/overlayRenderContainer.ts b/packages/dockview-core/src/overlay/overlayRenderContainer.ts similarity index 71% rename from packages/dockview-core/src/overlayRenderContainer.ts rename to packages/dockview-core/src/overlay/overlayRenderContainer.ts index c7eb1d2b5..a0d024a47 100644 --- a/packages/dockview-core/src/overlayRenderContainer.ts +++ b/packages/dockview-core/src/overlay/overlayRenderContainer.ts @@ -1,8 +1,15 @@ -import { DragAndDropObserver } from './dnd/dnd'; -import { Droptarget } from './dnd/droptarget'; -import { getDomNodePagePosition, toggleClass } from './dom'; -import { CompositeDisposable, Disposable, IDisposable } from './lifecycle'; -import { IDockviewPanel } from './dockview/dockviewPanel'; +import { DragAndDropObserver } from '../dnd/dnd'; +import { Droptarget } from '../dnd/droptarget'; +import { getDomNodePagePosition, toggleClass } from '../dom'; +import { + CompositeDisposable, + Disposable, + IDisposable, + MutableDisposable, +} from '../lifecycle'; +import { IDockviewPanel } from '../dockview/dockviewPanel'; +import { DockviewComponent } from '../dockview/dockviewComponent'; +import { DEFAULT_OVERLAY_Z_INDEX } from './overlay'; export type DockviewPanelRenderer = 'onlyWhenVisible' | 'always'; @@ -30,7 +37,10 @@ export class OverlayRenderContainer extends CompositeDisposable { private _disposed = false; - constructor(private readonly element: HTMLElement) { + constructor( + readonly element: HTMLElement, + readonly accessor: DockviewComponent + ) { super(); this.addDisposables( @@ -108,7 +118,10 @@ export class OverlayRenderContainer extends CompositeDisposable { focusContainer.style.display = panel.api.isVisible ? '' : 'none'; }; + const observerDisposable = new MutableDisposable(); + const disposable = new CompositeDisposable( + observerDisposable, /** * since container is positioned absoutely we must explicitly forward * the dnd events for the expect behaviours to continue to occur in terms of dnd @@ -148,6 +161,49 @@ export class OverlayRenderContainer extends CompositeDisposable { } resize(); + }), + panel.api.onDidLocationChange((event) => { + const isFloating = event.location.type === 'floating'; + + if (isFloating) { + queueMicrotask(() => { + const floatingGroup = this.accessor.floatingGroups.find( + (group) => group.group === panel.api.group + ); + + if (!floatingGroup) { + return; + } + + const element = floatingGroup.overlay.element; + + const update = () => { + const level = Number( + element.getAttribute('aria-level') + ); + focusContainer.style.zIndex = `${ + DEFAULT_OVERLAY_Z_INDEX + level * 2 + 1 + }`; + }; + + const observer = new MutationObserver(() => { + update(); + }); + + observerDisposable.value = Disposable.from(() => + observer.disconnect() + ); + + observer.observe(element, { + attributeFilter: ['aria-level'], + attributes: true, + }); + + update(); + }); + } else { + focusContainer.style.zIndex = ''; // reset the z-index, perhaps CSS will take over here + } }) ); diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 268a17e40..20b52c2c1 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -8,6 +8,7 @@ import { addClasses, toggleClass, getElementsByTagName, + disableIframePointEvents, } from '../dom'; import { Event, Emitter } from '../events'; import { pushToStart, pushToEnd, firstIndex } from '../array'; @@ -437,14 +438,7 @@ export class Splitview { item.enabled = false; } - const iframes = [ - ...getElementsByTagName('iframe'), - ...getElementsByTagName('webview'), - ]; - - for (const iframe of iframes) { - iframe.style.pointerEvents = 'none'; - } + const iframes = disableIframePointEvents(); const start = this._orientation === Orientation.HORIZONTAL @@ -553,9 +547,7 @@ export class Splitview { item.enabled = true; } - for (const iframe of iframes) { - iframe.style.pointerEvents = 'auto'; - } + iframes.release(); this.saveProportions(); diff --git a/tsconfig.base.json b/tsconfig.base.json index b418181bf..cd01be6f2 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,6 +20,8 @@ "ES2017.String", "ES2018.Promise", "ES2019", + "ES2020", + "ES2021", "DOM" ] },