Merge pull request #679 from mathuo/656-renderer-always-causes-floating-group-z-index-bug

656 renderer always causes floating group z index bug
This commit is contained in:
mathuo 2024-08-11 11:04:33 +01:00 committed by GitHub
commit 619b0b3276
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 379 additions and 142 deletions

View File

@ -44,3 +44,30 @@ export function createOffsetDragOverEvent(params: {
export function exhaustMicrotaskQueue(): Promise<void> { export function exhaustMicrotaskQueue(): Promise<void> {
return new Promise<void>((resolve) => resolve()); return new Promise<void>((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,
};
};

View File

@ -70,8 +70,8 @@ describe('abstractDragHandler', () => {
expect(span.style.pointerEvents).toBeFalsy(); expect(span.style.pointerEvents).toBeFalsy();
fireEvent.dragEnd(element); fireEvent.dragEnd(element);
expect(iframe.style.pointerEvents).toBe('auto'); expect(iframe.style.pointerEvents).toBe('');
expect(webview.style.pointerEvents).toBe('auto'); expect(webview.style.pointerEvents).toBe('');
expect(span.style.pointerEvents).toBeFalsy(); expect(span.style.pointerEvents).toBeFalsy();
handler.dispose(); handler.dispose();
@ -114,8 +114,8 @@ describe('abstractDragHandler', () => {
expect(span.style.pointerEvents).toBeFalsy(); expect(span.style.pointerEvents).toBeFalsy();
handler.dispose(); handler.dispose();
expect(iframe.style.pointerEvents).toBe('auto'); expect(iframe.style.pointerEvents).toBe('');
expect(webview.style.pointerEvents).toBe('auto'); expect(webview.style.pointerEvents).toBe('');
expect(span.style.pointerEvents).toBeFalsy(); expect(span.style.pointerEvents).toBeFalsy();
}); });
@ -172,7 +172,7 @@ describe('abstractDragHandler', () => {
const event = new Event('dragstart'); const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault'); const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event); fireEvent(element, event);
expect(spy).toBeCalledTimes(0); expect(spy).toHaveBeenCalledTimes(0);
handler.dispose(); handler.dispose();
}); });

View File

@ -1,5 +1,4 @@
import { fireEvent } from '@testing-library/dom'; import { fireEvent } from '@testing-library/dom';
import { Emitter, Event } from '../../../../events';
import { ContentContainer } from '../../../../dockview/components/panel/content'; import { ContentContainer } from '../../../../dockview/components/panel/content';
import { import {
GroupPanelPartInitParameters, GroupPanelPartInitParameters,
@ -10,9 +9,9 @@ import { PanelUpdateEvent } from '../../../../panel/types';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel'; import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel';
import { DockviewComponent } from '../../../../dockview/dockviewComponent'; import { DockviewComponent } from '../../../../dockview/dockviewComponent';
import { OverlayRenderContainer } from '../../../../overlayRenderContainer';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel'; import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel';
import { OverlayRenderContainer } from '../../../../overlay/overlayRenderContainer';
class TestContentRenderer class TestContentRenderer
extends CompositeDisposable extends CompositeDisposable
@ -58,7 +57,8 @@ describe('contentContainer', () => {
const disposable = new CompositeDisposable(); const disposable = new CompositeDisposable();
const overlayRenderContainer = new OverlayRenderContainer( const overlayRenderContainer = new OverlayRenderContainer(
document.createElement('div') document.createElement('div'),
fromPartial<DockviewComponent>({})
); );
const cut = new ContentContainer( const cut = new ContentContainer(

View File

@ -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<DockviewComponent>({
doSetGroupActive: jest.fn(),
});
const group = fromPartial<DockviewGroupPanel>({});
const cut = new VoidContainer(accessor, group);
expect(accessor.doSetGroupActive).not.toHaveBeenCalled();
fireEvent.pointerDown(cut.element);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(group);
});
});

View File

@ -30,7 +30,7 @@ import { createOffsetDragOverEvent } from '../__test_utils__/utils';
import { import {
DockviewPanelRenderer, DockviewPanelRenderer,
OverlayRenderContainer, OverlayRenderContainer,
} from '../../overlayRenderContainer'; } from '../../overlay/overlayRenderContainer';
import { DockviewGroupPanelFloatingChangeEvent } from '../../api/dockviewGroupPanelApi'; import { DockviewGroupPanelFloatingChangeEvent } from '../../api/dockviewGroupPanelApi';
import { SizeEvent } from '../../api/gridviewPanelApi'; import { SizeEvent } from '../../api/gridviewPanelApi';
import { import {
@ -285,7 +285,8 @@ describe('dockviewGroupPanelModel', () => {
onDidAddPanel: () => ({ dispose: jest.fn() }), onDidAddPanel: () => ({ dispose: jest.fn() }),
onDidRemovePanel: () => ({ dispose: jest.fn() }), onDidRemovePanel: () => ({ dispose: jest.fn() }),
overlayRenderContainer: new OverlayRenderContainer( overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div') document.createElement('div'),
fromPartial<DockviewComponent>({})
), ),
}); });
@ -823,7 +824,8 @@ describe('dockviewGroupPanelModel', () => {
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer( overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div') document.createElement('div'),
fromPartial<DockviewComponent>({})
), ),
}); });
@ -894,7 +896,8 @@ describe('dockviewGroupPanelModel', () => {
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer( overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div') document.createElement('div'),
fromPartial<DockviewComponent>({})
), ),
}); });
@ -966,7 +969,8 @@ describe('dockviewGroupPanelModel', () => {
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer( overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div') document.createElement('div'),
fromPartial<DockviewComponent>({})
), ),
}); });

View File

@ -1,4 +1,5 @@
import { import {
disableIframePointEvents,
isInDocument, isInDocument,
quasiDefaultPrevented, quasiDefaultPrevented,
quasiPreventDefault, quasiPreventDefault,
@ -45,4 +46,38 @@ describe('dom', () => {
expect(isInDocument(el2)).toBeTruthy(); 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('');
});
}); });

View File

@ -1,31 +1,5 @@
import { Overlay } from '../../dnd/overlay'; import { Overlay } from '../../overlay/overlay';
import { mockGetBoundingClientRect } from '../__test_utils__/utils';
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,
};
};
describe('overlay', () => { describe('overlay', () => {
test('toJSON, top left', () => { test('toJSON, top left', () => {
@ -76,6 +50,8 @@ describe('overlay', () => {
width: 40, width: 40,
height: 50, height: 50,
}); });
cut.dispose();
}); });
test('toJSON, bottom right', () => { test('toJSON, bottom right', () => {
@ -126,6 +102,8 @@ describe('overlay', () => {
width: 40, width: 40,
height: 50, height: 50,
}); });
cut.dispose();
}); });
test('that out-of-bounds dimensions are fixed, top left', () => { test('that out-of-bounds dimensions are fixed, top left', () => {
@ -176,6 +154,8 @@ describe('overlay', () => {
width: 40, width: 40,
height: 50, height: 50,
}); });
cut.dispose();
}); });
test('that out-of-bounds dimensions are fixed, bottom right', () => { test('that out-of-bounds dimensions are fixed, bottom right', () => {
@ -226,6 +206,8 @@ describe('overlay', () => {
width: 40, width: 40,
height: 50, height: 50,
}); });
cut.dispose();
}); });
test('setBounds, top left', () => { test('setBounds, top left', () => {
@ -276,6 +258,8 @@ describe('overlay', () => {
expect(element.style.width).toBe('200px'); expect(element.style.width).toBe('200px');
expect(element.style.left).toBe('300px'); expect(element.style.left).toBe('300px');
expect(element.style.top).toBe('400px'); expect(element.style.top).toBe('400px');
cut.dispose();
}); });
test('setBounds, bottom right', () => { test('setBounds, bottom right', () => {
@ -326,6 +310,8 @@ describe('overlay', () => {
expect(element.style.width).toBe('200px'); expect(element.style.width).toBe('200px');
expect(element.style.right).toBe('300px'); expect(element.style.right).toBe('300px');
expect(element.style.bottom).toBe('400px'); expect(element.style.bottom).toBe('400px');
cut.dispose();
}); });
test('that the resize handles are added', () => { test('that the resize handles are added', () => {
@ -364,4 +350,66 @@ describe('overlay', () => {
cut.dispose(); 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');
});
}); });

View File

@ -1,9 +1,13 @@
import { Droptarget } from '../dnd/droptarget'; import { Droptarget } from '../../dnd/droptarget';
import { IDockviewPanel } from '../dockview/dockviewPanel'; import { IDockviewPanel } from '../../dockview/dockviewPanel';
import { Emitter } from '../events'; import { Emitter } from '../../events';
import { IRenderable, OverlayRenderContainer } from '../overlayRenderContainer'; import {
IRenderable,
OverlayRenderContainer,
} from '../../overlay/overlayRenderContainer';
import { fromPartial } from '@total-typescript/shoehorn'; 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', () => { describe('overlayRenderContainer', () => {
let referenceContainer: IRenderable; let referenceContainer: IRenderable;
@ -18,7 +22,10 @@ describe('overlayRenderContainer', () => {
dropTarget: fromPartial<Droptarget>({}), dropTarget: fromPartial<Droptarget>({}),
}; };
cut = new OverlayRenderContainer(parentContainer); cut = new OverlayRenderContainer(
parentContainer,
fromPartial<DockviewComponent>({})
);
}); });
test('that attach(...) and detach(...) mutate the DOM as expected', () => { test('that attach(...) and detach(...) mutate the DOM as expected', () => {
@ -26,12 +33,14 @@ describe('overlayRenderContainer', () => {
const onDidVisibilityChange = new Emitter<any>(); const onDidVisibilityChange = new Emitter<any>();
const onDidDimensionsChange = new Emitter<any>(); const onDidDimensionsChange = new Emitter<any>();
const onDidLocationChange = new Emitter<any>();
const panel = fromPartial<IDockviewPanel>({ const panel = fromPartial<IDockviewPanel>({
api: { api: {
id: 'test_panel_id', id: 'test_panel_id',
onDidVisibilityChange: onDidVisibilityChange.event, onDidVisibilityChange: onDidVisibilityChange.event,
onDidDimensionsChange: onDidDimensionsChange.event, onDidDimensionsChange: onDidDimensionsChange.event,
onDidLocationChange: onDidLocationChange.event,
isVisible: true, isVisible: true,
}, },
view: { view: {
@ -62,12 +71,14 @@ describe('overlayRenderContainer', () => {
const onDidVisibilityChange = new Emitter<any>(); const onDidVisibilityChange = new Emitter<any>();
const onDidDimensionsChange = new Emitter<any>(); const onDidDimensionsChange = new Emitter<any>();
const onDidLocationChange = new Emitter<any>();
const panel = fromPartial<IDockviewPanel>({ const panel = fromPartial<IDockviewPanel>({
api: { api: {
id: 'test_panel_id', id: 'test_panel_id',
onDidVisibilityChange: onDidVisibilityChange.event, onDidVisibilityChange: onDidVisibilityChange.event,
onDidDimensionsChange: onDidDimensionsChange.event, onDidDimensionsChange: onDidDimensionsChange.event,
onDidLocationChange: onDidLocationChange.event,
isVisible: true, isVisible: true,
}, },
view: { view: {

View File

@ -5,7 +5,7 @@ import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { DockviewPanel } from '../dockview/dockviewPanel'; import { DockviewPanel } from '../dockview/dockviewPanel';
import { DockviewComponent } from '../dockview/dockviewComponent'; import { DockviewComponent } from '../dockview/dockviewComponent';
import { Position } from '../dnd/droptarget'; import { Position } from '../dnd/droptarget';
import { DockviewPanelRenderer } from '../overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import { DockviewGroupPanelFloatingChangeEvent } from './dockviewGroupPanelApi'; import { DockviewGroupPanelFloatingChangeEvent } from './dockviewGroupPanelApi';
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel'; import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';

View File

@ -1,4 +1,4 @@
import { getElementsByTagName } from '../dom'; import { disableIframePointEvents, getElementsByTagName } from '../dom';
import { addDisposableListener, Emitter } from '../events'; import { addDisposableListener, Emitter } from '../events';
import { import {
CompositeDisposable, CompositeDisposable,
@ -40,23 +40,14 @@ export abstract class DragHandler extends CompositeDisposable {
return; return;
} }
const iframes = [ const iframes = disableIframePointEvents();
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
this.pointerEventsDisposable.value = { this.pointerEventsDisposable.value = {
dispose: () => { dispose: () => {
for (const iframe of iframes) { iframes.release();
iframe.style.pointerEvents = 'auto';
}
}, },
}; };
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
this.el.classList.add('dv-dragged'); this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0); setTimeout(() => this.el.classList.remove('dv-dragged'), 0);

View File

@ -3,7 +3,12 @@ import {
IDisposable, IDisposable,
MutableDisposable, MutableDisposable,
} from '../../../lifecycle'; } from '../../../lifecycle';
import { Emitter, Event } from '../../../events'; import {
addDisposableListener,
addDisposableWindowListener,
Emitter,
Event,
} from '../../../events';
import { trackFocus } from '../../../dom'; import { trackFocus } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel'; import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent'; import { DockviewComponent } from '../../dockviewComponent';

View File

@ -42,7 +42,7 @@ export class VoidContainer extends CompositeDisposable {
this.addDisposables( this.addDisposables(
this._onDrop, this._onDrop,
this._onDragStart, this._onDragStart,
addDisposableListener(this._element, 'click', () => { addDisposableListener(this._element, 'pointerdown', () => {
this.accessor.doSetGroupActive(this.group); this.accessor.doSetGroupActive(this.group);
}) })
); );

View File

@ -50,7 +50,7 @@ import { DockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelModel } from './dockviewPanelModel'; import { DockviewPanelModel } from './dockviewPanelModel';
import { getPanelData } from '../dnd/dataTransfer'; import { getPanelData } from '../dnd/dataTransfer';
import { Parameters } from '../panel/types'; import { Parameters } from '../panel/types';
import { Overlay } from '../dnd/overlay'; import { Overlay } from '../overlay/overlay';
import { addTestId, toggleClass, watchElementResize } from '../dom'; import { addTestId, toggleClass, watchElementResize } from '../dom';
import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel'; import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel';
import { import {
@ -65,7 +65,7 @@ import {
import { import {
DockviewPanelRenderer, DockviewPanelRenderer,
OverlayRenderContainer, OverlayRenderContainer,
} from '../overlayRenderContainer'; } from '../overlay/overlayRenderContainer';
import { PopoutWindow } from '../popoutWindow'; import { PopoutWindow } from '../popoutWindow';
const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = {
@ -357,6 +357,10 @@ export class DockviewComponent
return this.gridview.margin; return this.gridview.margin;
} }
get floatingGroups(): DockviewFloatingGroupPanel[] {
return this._floatingGroups;
}
constructor(parentElement: HTMLElement, options: DockviewComponentOptions) { constructor(parentElement: HTMLElement, options: DockviewComponentOptions) {
super({ super({
proportionalLayout: true, proportionalLayout: true,
@ -371,11 +375,14 @@ export class DockviewComponent
className: options.className, className: options.className,
}); });
const gready = document.createElement('div'); // const gready = document.createElement('div');
gready.className = 'dv-overlay-render-container'; // gready.className = 'dv-overlay-render-container';
this.gridview.element.appendChild(gready); // 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.gridview.element, 'dv-dockview', true);
toggleClass(this.element, 'dv-debug', !!options.debug); toggleClass(this.element, 'dv-debug', !!options.debug);
@ -639,7 +646,8 @@ export class DockviewComponent
gready.className = 'dv-overlay-render-container'; gready.className = 'dv-overlay-render-container';
const overlayRenderContainer = new OverlayRenderContainer( const overlayRenderContainer = new OverlayRenderContainer(
gready gready,
this
); );
const referenceGroup = const referenceGroup =
@ -837,8 +845,6 @@ export class DockviewComponent
} }
} }
group.model.location = { type: 'floating' };
function getAnchoredBox(): AnchoredBox { function getAnchoredBox(): AnchoredBox {
if (options?.position) { if (options?.position) {
const result: any = {}; const result: any = {};
@ -928,10 +934,17 @@ export class DockviewComponent
overlay overlay
); );
const disposable = watchElementResize(group.element, (entry) => { const disposable = new CompositeDisposable(
const { width, height } = entry.contentRect; group.api.onDidActiveChange((event) => {
group.layout(width, height); // let the group know it's size is changing so it can fire events to the panel 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( floatingGroupPanel.addDisposables(
overlay.onDidChange(() => { overlay.onDidChange(() => {
@ -953,8 +966,8 @@ export class DockviewComponent
dispose: () => { dispose: () => {
disposable.dispose(); disposable.dispose();
group.model.location = { type: 'grid' };
remove(this._floatingGroups, floatingGroupPanel); remove(this._floatingGroups, floatingGroupPanel);
group.model.location = { type: 'grid' };
this.updateWatermark(); this.updateWatermark();
}, },
} }
@ -962,6 +975,8 @@ export class DockviewComponent
this._floatingGroups.push(floatingGroupPanel); this._floatingGroups.push(floatingGroupPanel);
group.model.location = { type: 'floating' };
if (!options?.skipActiveGroup) { if (!options?.skipActiveGroup) {
this.doSetGroupAndPanelActive(group); this.doSetGroupAndPanelActive(group);
} }

View File

@ -1,4 +1,4 @@
import { Overlay } from '../dnd/overlay'; import { Overlay } from '../overlay/overlay';
import { CompositeDisposable } from '../lifecycle'; import { CompositeDisposable } from '../lifecycle';
import { AnchoredBox } from '../types'; import { AnchoredBox } from '../types';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
@ -10,7 +10,8 @@ export interface IDockviewFloatingGroupPanel {
export class DockviewFloatingGroupPanel export class DockviewFloatingGroupPanel
extends CompositeDisposable extends CompositeDisposable
implements IDockviewFloatingGroupPanel { implements IDockviewFloatingGroupPanel
{
constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) { constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) {
super(); super();
this.addDisposables(overlay); this.addDisposables(overlay);

View File

@ -36,7 +36,7 @@ import {
DockviewUnhandledDragOverEvent, DockviewUnhandledDragOverEvent,
IHeaderActionsRenderer, IHeaderActionsRenderer,
} from './options'; } from './options';
import { OverlayRenderContainer } from '../overlayRenderContainer'; import { OverlayRenderContainer } from '../overlay/overlayRenderContainer';
import { TitleEvent } from '../api/dockviewPanelApi'; import { TitleEvent } from '../api/dockviewPanelApi';
interface GroupMoveEvent { interface GroupMoveEvent {
@ -964,7 +964,7 @@ export class DockviewGroupPanelModel
}); });
this.watermark = watermark; this.watermark = watermark;
addDisposableListener(this.watermark.element, 'click', () => { addDisposableListener(this.watermark.element, 'pointerdown', () => {
if (!this.isActive) { if (!this.isActive) {
this.accessor.doSetGroupActive(this.groupPanel); this.accessor.doSetGroupActive(this.groupPanel);
} }

View File

@ -9,7 +9,7 @@ import { CompositeDisposable, IDisposable } from '../lifecycle';
import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types'; import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types';
import { IDockviewPanelModel } from './dockviewPanelModel'; import { IDockviewPanelModel } from './dockviewPanelModel';
import { DockviewComponent } from './dockviewComponent'; import { DockviewComponent } from './dockviewComponent';
import { DockviewPanelRenderer } from '../overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import { WillFocusEvent } from '../api/panelApi'; import { WillFocusEvent } from '../api/panelApi';
export interface IDockviewPanel extends IDisposable, IPanel { export interface IDockviewPanel extends IDisposable, IPanel {

View File

@ -12,7 +12,7 @@ import {
GroupOptions, GroupOptions,
} from './dockviewGroupPanelModel'; } from './dockviewGroupPanelModel';
import { IDockviewPanel } from './dockviewPanel'; import { IDockviewPanel } from './dockviewPanel';
import { DockviewPanelRenderer } from '../overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import { IGroupHeaderProps } from './framework'; import { IGroupHeaderProps } from './framework';
import { AnchoredBox } from '../types'; import { AnchoredBox } from '../types';
import { FloatingGroupOptions } from './dockviewComponent'; import { FloatingGroupOptions } from './dockviewComponent';

View File

@ -3,7 +3,7 @@ import { PanelInitParameters, IPanel } from '../panel/types';
import { DockviewApi } from '../api/component.api'; import { DockviewApi } from '../api/component.api';
import { Optional } from '../types'; import { Optional } from '../types';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelRenderer } from '../overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
export interface HeaderPartInitParameters { export interface HeaderPartInitParameters {
title: string; title: string;

View File

@ -257,3 +257,26 @@ export function isInDocument(element: Element): boolean {
export function addTestId(element: HTMLElement, id: string): void { export function addTestId(element: HTMLElement, id: string): void {
element.setAttribute('data-testid', id); element.setAttribute('data-testid', id);
} }
export function disableIframePointEvents() {
const iframes: HTMLElement[] = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
const original = new WeakMap<HTMLElement, string>(); // 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
},
};
}

View File

@ -64,7 +64,7 @@ export * from './splitview/splitviewPanel';
export * from './paneview/paneviewPanel'; export * from './paneview/paneviewPanel';
export * from './dockview/types'; export * from './dockview/types';
export { DockviewPanelRenderer } from './overlayRenderContainer'; export { DockviewPanelRenderer } from './overlay/overlayRenderContainer';
export { export {
Position, Position,

View File

@ -29,10 +29,6 @@
position: absolute; position: absolute;
z-index: 997; z-index: 997;
&.dv-bring-to-front {
z-index: 998;
}
border: 1px solid var(--dv-tab-divider-color); border: 1px solid var(--dv-tab-divider-color);
box-shadow: var(--dv-floating-box-shadow); box-shadow: var(--dv-floating-box-shadow);

View File

@ -1,5 +1,5 @@
import { import {
getElementsByTagName, disableIframePointEvents,
quasiDefaultPrevented, quasiDefaultPrevented,
toggleClass, toggleClass,
} from '../dom'; } from '../dom';
@ -13,20 +13,36 @@ import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math'; import { clamp } from '../math';
import { AnchoredBox } from '../types'; import { AnchoredBox } from '../types';
const bringElementToFront = (() => { export const DEFAULT_OVERLAY_Z_INDEX = 999;
let previous: HTMLElement | null = null;
function pushToTop(element: HTMLElement) { class AriaLevelTracker {
if (previous !== element && previous !== null) { private _orderedList: HTMLElement[] = [];
toggleClass(previous, 'dv-bring-to-front', false);
}
toggleClass(element, 'dv-bring-to-front', true); push(element: HTMLElement): void {
previous = element; 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 { export class Overlay extends CompositeDisposable {
private _element: HTMLElement = document.createElement('div'); private _element: HTMLElement = document.createElement('div');
@ -51,6 +67,10 @@ export class Overlay extends CompositeDisposable {
this.options.minimumInViewportHeight = value; this.options.minimumInViewportHeight = value;
} }
get element(): HTMLElement {
return this._element;
}
constructor( constructor(
private readonly options: AnchoredBox & { private readonly options: AnchoredBox & {
container: HTMLElement; container: HTMLElement;
@ -86,6 +106,12 @@ export class Overlay extends CompositeDisposable {
...('left' in this.options && { left: this.options.left }), ...('left' in this.options && { left: this.options.left }),
...('right' in this.options && { right: this.options.right }), ...('right' in this.options && { right: this.options.right }),
}); });
arialLevelTracker.push(this._element);
}
bringToFront(): void {
arialLevelTracker.push(this._element);
} }
setBounds(bounds: Partial<AnchoredBox> = {}): void { setBounds(bounds: Partial<AnchoredBox> = {}): void {
@ -207,21 +233,12 @@ export class Overlay extends CompositeDisposable {
const track = () => { const track = () => {
let offset: { x: number; y: number } | null = null; let offset: { x: number; y: number } | null = null;
const iframes = [ const iframes = disableIframePointEvents();
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
move.value = new CompositeDisposable( move.value = new CompositeDisposable(
{ {
dispose: () => { dispose: () => {
for (const iframe of iframes) { iframes.release();
iframe.style.pointerEvents = 'auto';
}
}, },
}, },
addDisposableWindowListener(window, 'mousemove', (e) => { addDisposableWindowListener(window, 'mousemove', (e) => {
@ -362,14 +379,12 @@ export class Overlay extends CompositeDisposable {
this.options.content, this.options.content,
'mousedown', 'mousedown',
() => { () => {
bringElementToFront(this._element); arialLevelTracker.push(this._element);
}, },
true true
) )
); );
bringElementToFront(this._element);
if (options.inDragMode) { if (options.inDragMode) {
track(); track();
} }
@ -404,14 +419,7 @@ export class Overlay extends CompositeDisposable {
originalWidth: number; originalWidth: number;
} | null = null; } | null = null;
const iframes = [ const iframes = disableIframePointEvents();
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
move.value = new CompositeDisposable( move.value = new CompositeDisposable(
addDisposableWindowListener(window, 'mousemove', (e) => { addDisposableWindowListener(window, 'mousemove', (e) => {
@ -582,9 +590,7 @@ export class Overlay extends CompositeDisposable {
}), }),
{ {
dispose: () => { dispose: () => {
for (const iframe of iframes) { iframes.release();
iframe.style.pointerEvents = 'auto';
}
}, },
}, },
addDisposableWindowListener(window, 'mouseup', () => { addDisposableWindowListener(window, 'mouseup', () => {
@ -611,6 +617,7 @@ export class Overlay extends CompositeDisposable {
} }
override dispose(): void { override dispose(): void {
arialLevelTracker.destroy(this._element);
this._element.remove(); this._element.remove();
super.dispose(); super.dispose();
} }

View File

@ -4,7 +4,11 @@
height: 100%; height: 100%;
&.dv-render-overlay-float { &.dv-render-overlay-float {
z-index: 999; z-index: 998;
&.dv-render-overlay-active {
// z-index: 1000;
}
} }
} }

View File

@ -1,8 +1,15 @@
import { DragAndDropObserver } from './dnd/dnd'; import { DragAndDropObserver } from '../dnd/dnd';
import { Droptarget } from './dnd/droptarget'; import { Droptarget } from '../dnd/droptarget';
import { getDomNodePagePosition, toggleClass } from './dom'; import { getDomNodePagePosition, toggleClass } from '../dom';
import { CompositeDisposable, Disposable, IDisposable } from './lifecycle'; import {
import { IDockviewPanel } from './dockview/dockviewPanel'; 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'; export type DockviewPanelRenderer = 'onlyWhenVisible' | 'always';
@ -30,7 +37,10 @@ export class OverlayRenderContainer extends CompositeDisposable {
private _disposed = false; private _disposed = false;
constructor(private readonly element: HTMLElement) { constructor(
readonly element: HTMLElement,
readonly accessor: DockviewComponent
) {
super(); super();
this.addDisposables( this.addDisposables(
@ -108,7 +118,10 @@ export class OverlayRenderContainer extends CompositeDisposable {
focusContainer.style.display = panel.api.isVisible ? '' : 'none'; focusContainer.style.display = panel.api.isVisible ? '' : 'none';
}; };
const observerDisposable = new MutableDisposable();
const disposable = new CompositeDisposable( const disposable = new CompositeDisposable(
observerDisposable,
/** /**
* since container is positioned absoutely we must explicitly forward * since container is positioned absoutely we must explicitly forward
* the dnd events for the expect behaviours to continue to occur in terms of dnd * 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(); 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
}
}) })
); );

View File

@ -8,6 +8,7 @@ import {
addClasses, addClasses,
toggleClass, toggleClass,
getElementsByTagName, getElementsByTagName,
disableIframePointEvents,
} from '../dom'; } from '../dom';
import { Event, Emitter } from '../events'; import { Event, Emitter } from '../events';
import { pushToStart, pushToEnd, firstIndex } from '../array'; import { pushToStart, pushToEnd, firstIndex } from '../array';
@ -437,14 +438,7 @@ export class Splitview {
item.enabled = false; item.enabled = false;
} }
const iframes = [ const iframes = disableIframePointEvents();
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
const start = const start =
this._orientation === Orientation.HORIZONTAL this._orientation === Orientation.HORIZONTAL
@ -553,9 +547,7 @@ export class Splitview {
item.enabled = true; item.enabled = true;
} }
for (const iframe of iframes) { iframes.release();
iframe.style.pointerEvents = 'auto';
}
this.saveProportions(); this.saveProportions();

View File

@ -20,6 +20,8 @@
"ES2017.String", "ES2017.String",
"ES2018.Promise", "ES2018.Promise",
"ES2019", "ES2019",
"ES2020",
"ES2021",
"DOM" "DOM"
] ]
}, },