mirror of
https://github.com/mathuo/dockview
synced 2025-10-13 19:38:16 +00:00
Merge pull request #701 from iammola/tab-accessibility
feat: dockview accessibility
This commit is contained in:
commit
9c04db8093
@ -50,6 +50,7 @@ describe('groupPanelApi', () => {
|
|||||||
const accessor = fromPartial<DockviewComponent>({
|
const accessor = fromPartial<DockviewComponent>({
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -83,6 +84,7 @@ describe('groupPanelApi', () => {
|
|||||||
const accessor = fromPartial<DockviewComponent>({
|
const accessor = fromPartial<DockviewComponent>({
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
});
|
});
|
||||||
|
@ -17,6 +17,7 @@ describe('tabsContainer', () => {
|
|||||||
const accessor = fromPartial<DockviewComponent>({
|
const accessor = fromPartial<DockviewComponent>({
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -71,6 +72,7 @@ describe('tabsContainer', () => {
|
|||||||
id: 'testcomponentid',
|
id: 'testcomponentid',
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -141,6 +143,7 @@ describe('tabsContainer', () => {
|
|||||||
id: 'testcomponentid',
|
id: 'testcomponentid',
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -205,6 +208,7 @@ describe('tabsContainer', () => {
|
|||||||
id: 'testcomponentid',
|
id: 'testcomponentid',
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -269,6 +273,7 @@ describe('tabsContainer', () => {
|
|||||||
id: 'testcomponentid',
|
id: 'testcomponentid',
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -338,6 +343,7 @@ describe('tabsContainer', () => {
|
|||||||
id: 'testcomponentid',
|
id: 'testcomponentid',
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -403,6 +409,7 @@ describe('tabsContainer', () => {
|
|||||||
id: 'testcomponentid',
|
id: 'testcomponentid',
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -468,6 +475,7 @@ describe('tabsContainer', () => {
|
|||||||
options: {},
|
options: {},
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
element: document.createElement('div'),
|
element: document.createElement('div'),
|
||||||
addFloatingGroup: jest.fn(),
|
addFloatingGroup: jest.fn(),
|
||||||
doSetGroupActive: jest.fn(),
|
doSetGroupActive: jest.fn(),
|
||||||
@ -525,6 +533,7 @@ describe('tabsContainer', () => {
|
|||||||
options: {},
|
options: {},
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
element: document.createElement('div'),
|
element: document.createElement('div'),
|
||||||
addFloatingGroup: jest.fn(),
|
addFloatingGroup: jest.fn(),
|
||||||
doSetGroupActive: jest.fn(),
|
doSetGroupActive: jest.fn(),
|
||||||
@ -577,6 +586,7 @@ describe('tabsContainer', () => {
|
|||||||
options: {},
|
options: {},
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
element: document.createElement('div'),
|
element: document.createElement('div'),
|
||||||
addFloatingGroup: jest.fn(),
|
addFloatingGroup: jest.fn(),
|
||||||
getGroupPanel: jest.fn(),
|
getGroupPanel: jest.fn(),
|
||||||
@ -634,6 +644,7 @@ describe('tabsContainer', () => {
|
|||||||
options: {},
|
options: {},
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
element: document.createElement('div'),
|
element: document.createElement('div'),
|
||||||
addFloatingGroup: jest.fn(),
|
addFloatingGroup: jest.fn(),
|
||||||
getGroupPanel: jest.fn(),
|
getGroupPanel: jest.fn(),
|
||||||
@ -702,6 +713,7 @@ describe('tabsContainer', () => {
|
|||||||
options: {},
|
options: {},
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
element: document.createElement('div'),
|
element: document.createElement('div'),
|
||||||
addFloatingGroup: jest.fn(),
|
addFloatingGroup: jest.fn(),
|
||||||
getGroupPanel: jest.fn(),
|
getGroupPanel: jest.fn(),
|
||||||
@ -770,6 +782,7 @@ describe('tabsContainer', () => {
|
|||||||
options: {},
|
options: {},
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
element: document.createElement('div'),
|
element: document.createElement('div'),
|
||||||
addFloatingGroup: jest.fn(),
|
addFloatingGroup: jest.fn(),
|
||||||
getGroupPanel: jest.fn(),
|
getGroupPanel: jest.fn(),
|
||||||
|
@ -6917,4 +6917,91 @@ describe('dockviewComponent', () => {
|
|||||||
dockview.layout(1000, 1000);
|
dockview.layout(1000, 1000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('that arrow keys should activate appropriate tabs', () => {
|
||||||
|
dockview.layout(500, 1000);
|
||||||
|
|
||||||
|
dockview.addPanel({
|
||||||
|
id: 'panel1',
|
||||||
|
component: 'default',
|
||||||
|
});
|
||||||
|
|
||||||
|
dockview.addPanel({
|
||||||
|
id: 'panel2',
|
||||||
|
component: 'default',
|
||||||
|
position: { referencePanel: 'panel1', direction: 'within' },
|
||||||
|
});
|
||||||
|
|
||||||
|
dockview.addPanel({
|
||||||
|
id: 'panel3',
|
||||||
|
component: 'default',
|
||||||
|
});
|
||||||
|
|
||||||
|
dockview.addPanel({
|
||||||
|
id: 'panel4',
|
||||||
|
component: 'default',
|
||||||
|
position: { referencePanel: 'panel3', direction: 'below' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const panel1 = dockview.getGroupPanel('panel1')!;
|
||||||
|
const panel2 = dockview.getGroupPanel('panel2')!;
|
||||||
|
const panel3 = dockview.getGroupPanel('panel3')!;
|
||||||
|
const panel4 = dockview.getGroupPanel('panel4')!;
|
||||||
|
|
||||||
|
panel1.api.setActive();
|
||||||
|
|
||||||
|
expect(panel1.api.isActive).toBeTruthy();
|
||||||
|
expect(panel2.api.isActive).toBeFalsy();
|
||||||
|
expect(panel3.api.isActive).toBeFalsy();
|
||||||
|
expect(panel4.api.isActive).toBeFalsy();
|
||||||
|
|
||||||
|
const tabsContainer = (panel: IDockviewPanel) =>
|
||||||
|
panel.api.group.element.querySelector('.tabs-container')!;
|
||||||
|
|
||||||
|
const event = new KeyboardEvent('keydown', { key: 'ArrowRight' });
|
||||||
|
|
||||||
|
fireEvent(tabsContainer(panel1), event);
|
||||||
|
expect(panel1.api.isActive).toBeFalsy();
|
||||||
|
expect(panel2.api.isActive).toBeTruthy();
|
||||||
|
expect(panel3.api.isActive).toBeFalsy();
|
||||||
|
expect(panel4.api.isActive).toBeFalsy();
|
||||||
|
|
||||||
|
fireEvent(tabsContainer(panel1), event);
|
||||||
|
expect(panel1.api.isActive).toBeFalsy();
|
||||||
|
expect(panel2.api.isActive).toBeFalsy();
|
||||||
|
expect(panel3.api.isActive).toBeTruthy();
|
||||||
|
expect(panel4.api.isActive).toBeFalsy();
|
||||||
|
|
||||||
|
const event2 = new KeyboardEvent('keydown', { key: 'ArrowLeft' });
|
||||||
|
|
||||||
|
fireEvent(tabsContainer(panel1), event2);
|
||||||
|
expect(panel1.api.isActive).toBeFalsy();
|
||||||
|
expect(panel2.api.isActive).toBeTruthy();
|
||||||
|
expect(panel3.api.isActive).toBeFalsy();
|
||||||
|
expect(panel4.api.isActive).toBeFalsy();
|
||||||
|
|
||||||
|
fireEvent(tabsContainer(panel1), event2);
|
||||||
|
expect(panel1.api.isActive).toBeTruthy();
|
||||||
|
expect(panel2.api.isActive).toBeFalsy();
|
||||||
|
expect(panel3.api.isActive).toBeFalsy();
|
||||||
|
expect(panel4.api.isActive).toBeFalsy();
|
||||||
|
|
||||||
|
panel4.api.setActive();
|
||||||
|
expect(panel1.api.isActive).toBeFalsy();
|
||||||
|
expect(panel2.api.isActive).toBeFalsy();
|
||||||
|
expect(panel3.api.isActive).toBeFalsy();
|
||||||
|
expect(panel4.api.isActive).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent(tabsContainer(panel4), event2);
|
||||||
|
expect(panel1.api.isActive).toBeFalsy();
|
||||||
|
expect(panel2.api.isActive).toBeFalsy();
|
||||||
|
expect(panel3.api.isActive).toBeFalsy();
|
||||||
|
expect(panel4.api.isActive).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent(tabsContainer(panel4), event);
|
||||||
|
expect(panel1.api.isActive).toBeFalsy();
|
||||||
|
expect(panel2.api.isActive).toBeFalsy();
|
||||||
|
expect(panel3.api.isActive).toBeFalsy();
|
||||||
|
expect(panel4.api.isActive).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -174,6 +174,8 @@ export class TestPanel implements IDockviewPanel {
|
|||||||
private _group: DockviewGroupPanel | undefined;
|
private _group: DockviewGroupPanel | undefined;
|
||||||
private _params: IGroupPanelInitParameters | undefined;
|
private _params: IGroupPanelInitParameters | undefined;
|
||||||
readonly view: IDockviewPanelModel;
|
readonly view: IDockviewPanelModel;
|
||||||
|
readonly componentElId: string;
|
||||||
|
readonly tabComponentElId: string;
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
return '';
|
return '';
|
||||||
@ -189,6 +191,8 @@ export class TestPanel implements IDockviewPanel {
|
|||||||
|
|
||||||
constructor(public readonly id: string, public api: DockviewPanelApi) {
|
constructor(public readonly id: string, public api: DockviewPanelApi) {
|
||||||
this.view = new TestModel(id);
|
this.view = new TestModel(id);
|
||||||
|
this.tabComponentElId = `tab-${id}`;
|
||||||
|
this.componentElId = `tab-panel-${id}`;
|
||||||
this.init({
|
this.init({
|
||||||
title: `${id}`,
|
title: `${id}`,
|
||||||
params: {},
|
params: {},
|
||||||
@ -266,6 +270,7 @@ describe('dockviewGroupPanelModel', () => {
|
|||||||
removeGroup: removeGroupMock,
|
removeGroup: removeGroupMock,
|
||||||
onDidAddPanel: () => ({ dispose: jest.fn() }),
|
onDidAddPanel: () => ({ dispose: jest.fn() }),
|
||||||
onDidRemovePanel: () => ({ dispose: jest.fn() }),
|
onDidRemovePanel: () => ({ dispose: jest.fn() }),
|
||||||
|
onDidActivePanelChange: () => ({ dispose: jest.fn() }),
|
||||||
overlayRenderContainer: new OverlayRenderContainer(
|
overlayRenderContainer: new OverlayRenderContainer(
|
||||||
document.createElement('div'),
|
document.createElement('div'),
|
||||||
fromPartial<DockviewComponent>({})
|
fromPartial<DockviewComponent>({})
|
||||||
@ -653,6 +658,7 @@ describe('dockviewGroupPanelModel', () => {
|
|||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
||||||
@ -716,6 +722,7 @@ describe('dockviewGroupPanelModel', () => {
|
|||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
||||||
@ -808,6 +815,7 @@ describe('dockviewGroupPanelModel', () => {
|
|||||||
doSetGroupActive: jest.fn(),
|
doSetGroupActive: jest.fn(),
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
overlayRenderContainer: new OverlayRenderContainer(
|
overlayRenderContainer: new OverlayRenderContainer(
|
||||||
document.createElement('div'),
|
document.createElement('div'),
|
||||||
fromPartial<DockviewComponent>({})
|
fromPartial<DockviewComponent>({})
|
||||||
@ -875,6 +883,7 @@ describe('dockviewGroupPanelModel', () => {
|
|||||||
doSetGroupActive: jest.fn(),
|
doSetGroupActive: jest.fn(),
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
overlayRenderContainer: new OverlayRenderContainer(
|
overlayRenderContainer: new OverlayRenderContainer(
|
||||||
document.createElement('div'),
|
document.createElement('div'),
|
||||||
fromPartial<DockviewComponent>({})
|
fromPartial<DockviewComponent>({})
|
||||||
@ -949,6 +958,7 @@ describe('dockviewGroupPanelModel', () => {
|
|||||||
doSetGroupActive: jest.fn(),
|
doSetGroupActive: jest.fn(),
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
overlayRenderContainer: new OverlayRenderContainer(
|
overlayRenderContainer: new OverlayRenderContainer(
|
||||||
document.createElement('div'),
|
document.createElement('div'),
|
||||||
fromPartial<DockviewComponent>({})
|
fromPartial<DockviewComponent>({})
|
||||||
@ -1030,6 +1040,7 @@ describe('dockviewGroupPanelModel', () => {
|
|||||||
return {
|
return {
|
||||||
id: 'testgroupid',
|
id: 'testgroupid',
|
||||||
model: groupView,
|
model: groupView,
|
||||||
|
dispose: jest.fn()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ describe('gridviewPanel', () => {
|
|||||||
return {
|
return {
|
||||||
onDidAddPanel: jest.fn(),
|
onDidAddPanel: jest.fn(),
|
||||||
onDidRemovePanel: jest.fn(),
|
onDidRemovePanel: jest.fn(),
|
||||||
|
onDidActivePanelChange: jest.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
onDidOptionsChange: jest.fn(),
|
onDidOptionsChange: jest.fn(),
|
||||||
} as any;
|
} as any;
|
||||||
|
@ -6,7 +6,7 @@ import { createCloseButton } from '../../../svg';
|
|||||||
export class DefaultTab extends CompositeDisposable implements ITabRenderer {
|
export class DefaultTab extends CompositeDisposable implements ITabRenderer {
|
||||||
private readonly _element: HTMLElement;
|
private readonly _element: HTMLElement;
|
||||||
private readonly _content: HTMLElement;
|
private readonly _content: HTMLElement;
|
||||||
private readonly action: HTMLElement;
|
private readonly action: HTMLButtonElement;
|
||||||
private _title: string | undefined;
|
private _title: string | undefined;
|
||||||
|
|
||||||
get element(): HTMLElement {
|
get element(): HTMLElement {
|
||||||
@ -22,22 +22,38 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
|
|||||||
this._content = document.createElement('div');
|
this._content = document.createElement('div');
|
||||||
this._content.className = 'dv-default-tab-content';
|
this._content.className = 'dv-default-tab-content';
|
||||||
|
|
||||||
this.action = document.createElement('div');
|
this.action = document.createElement('button');
|
||||||
|
this.action.type = 'button';
|
||||||
this.action.className = 'dv-default-tab-action';
|
this.action.className = 'dv-default-tab-action';
|
||||||
|
// originally hide this, so only when it is focused is it read out.
|
||||||
|
// so the SR when focused on the tab, doesn't read "<Tab Content> Close Button"
|
||||||
|
this.action.ariaHidden = 'true';
|
||||||
|
|
||||||
this.action.appendChild(createCloseButton());
|
this.action.appendChild(createCloseButton());
|
||||||
|
|
||||||
this._element.appendChild(this._content);
|
this._element.appendChild(this._content);
|
||||||
this._element.appendChild(this.action);
|
this._element.appendChild(this.action);
|
||||||
|
|
||||||
|
this.addDisposables(
|
||||||
|
addDisposableListener(this.action, 'focus', (event) => {
|
||||||
|
this.action.ariaHidden = 'false';
|
||||||
|
}),
|
||||||
|
addDisposableListener(this.action, 'blur', (event) => {
|
||||||
|
this.action.ariaHidden = 'true';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
init(params: GroupPanelPartInitParameters): void {
|
init(params: GroupPanelPartInitParameters): void {
|
||||||
this._title = params.title;
|
this._title = params.title;
|
||||||
|
this.action.ariaLabel = `Close "${this._title}" tab`;
|
||||||
|
|
||||||
this.addDisposables(
|
this.addDisposables(
|
||||||
params.api.onDidTitleChange((event) => {
|
params.api.onDidTitleChange((event) => {
|
||||||
this._title = event.title;
|
this._title = event.title;
|
||||||
|
this.action.ariaLabel = `Close "${event.title}" tab`;
|
||||||
this.render();
|
this.render();
|
||||||
}),
|
}),
|
||||||
addDisposableListener(this.action, 'pointerdown', (ev) => {
|
addDisposableListener(this.action, 'pointerdown', (ev) => {
|
||||||
@ -50,6 +66,18 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
|
|||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
params.api.close();
|
params.api.close();
|
||||||
|
}),
|
||||||
|
addDisposableListener(this.action, 'keydown', (ev) => {
|
||||||
|
if (ev.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ev.key) {
|
||||||
|
case 'Enter':
|
||||||
|
case 'Space':
|
||||||
|
params.api.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -52,6 +52,9 @@ export class Tab extends CompositeDisposable {
|
|||||||
|
|
||||||
private readonly _onPointDown = new Emitter<MouseEvent>();
|
private readonly _onPointDown = new Emitter<MouseEvent>();
|
||||||
readonly onPointerDown: Event<MouseEvent> = this._onPointDown.event;
|
readonly onPointerDown: Event<MouseEvent> = this._onPointDown.event;
|
||||||
|
|
||||||
|
private readonly _onKeyDown = new Emitter<KeyboardEvent>();
|
||||||
|
readonly onKeyDown: Event<KeyboardEvent> = this._onKeyDown.event;
|
||||||
|
|
||||||
private readonly _onDropped = new Emitter<DroptargetEvent>();
|
private readonly _onDropped = new Emitter<DroptargetEvent>();
|
||||||
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
|
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
|
||||||
@ -73,9 +76,13 @@ export class Tab extends CompositeDisposable {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
this._element = document.createElement('div');
|
this._element = document.createElement('div');
|
||||||
|
this._element.id = this.panel.tabComponentElId;
|
||||||
this._element.className = 'dv-tab';
|
this._element.className = 'dv-tab';
|
||||||
this._element.tabIndex = 0;
|
this._element.role = 'tab';
|
||||||
|
this._element.tabIndex = -1;
|
||||||
this._element.draggable = true;
|
this._element.draggable = true;
|
||||||
|
this._element.ariaSelected = 'false';
|
||||||
|
this._element.setAttribute('aria-controls', this.panel.componentElId);
|
||||||
|
|
||||||
toggleClass(this.element, 'dv-inactive-tab', true);
|
toggleClass(this.element, 'dv-inactive-tab', true);
|
||||||
|
|
||||||
@ -139,6 +146,9 @@ export class Tab extends CompositeDisposable {
|
|||||||
addDisposableListener(this._element, 'pointerdown', (event) => {
|
addDisposableListener(this._element, 'pointerdown', (event) => {
|
||||||
this._onPointDown.fire(event);
|
this._onPointDown.fire(event);
|
||||||
}),
|
}),
|
||||||
|
addDisposableListener(this._element, 'keydown', (event) => {
|
||||||
|
this._onKeyDown.fire(event);
|
||||||
|
}),
|
||||||
this.dropTarget.onDrop((event) => {
|
this.dropTarget.onDrop((event) => {
|
||||||
this._onDropped.fire(event);
|
this._onDropped.fire(event);
|
||||||
}),
|
}),
|
||||||
@ -147,6 +157,9 @@ export class Tab extends CompositeDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setActive(isActive: boolean): void {
|
public setActive(isActive: boolean): void {
|
||||||
|
this.element.tabIndex = isActive ? 0 : -1;
|
||||||
|
this.element.ariaSelected = isActive.toString();
|
||||||
|
|
||||||
toggleClass(this.element, 'dv-active-tab', isActive);
|
toggleClass(this.element, 'dv-active-tab', isActive);
|
||||||
toggleClass(this.element, 'dv-inactive-tab', !isActive);
|
toggleClass(this.element, 'dv-inactive-tab', !isActive);
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,7 @@ export class Tabs extends CompositeDisposable {
|
|||||||
|
|
||||||
this._tabsList = document.createElement('div');
|
this._tabsList = document.createElement('div');
|
||||||
this._tabsList.className = 'dv-tabs-container dv-horizontal';
|
this._tabsList.className = 'dv-tabs-container dv-horizontal';
|
||||||
|
this._tabsList.ariaOrientation = 'horizontal';
|
||||||
|
|
||||||
this.showTabsOverflowControl = options.showTabsOverflowControl;
|
this.showTabsOverflowControl = options.showTabsOverflowControl;
|
||||||
|
|
||||||
@ -109,12 +110,23 @@ export class Tabs extends CompositeDisposable {
|
|||||||
this.addDisposables(scrollbar);
|
this.addDisposables(scrollbar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.element.role = 'tablist';
|
||||||
|
this.element.ariaLabel =
|
||||||
|
'Use the Left Arrow to select the previous tab, Right Arrow for the next tab, Home for the first tab, and End for the last tab. Press Enter to select the focused tab.';
|
||||||
|
|
||||||
this.addDisposables(
|
this.addDisposables(
|
||||||
this._onOverflowTabsChange,
|
this._onOverflowTabsChange,
|
||||||
this._observerDisposable,
|
this._observerDisposable,
|
||||||
this._onWillShowOverlay,
|
this._onWillShowOverlay,
|
||||||
this._onDrop,
|
this._onDrop,
|
||||||
this._onTabDragStart,
|
this._onTabDragStart,
|
||||||
|
this.accessor.onDidActivePanelChange((e) => {
|
||||||
|
if (e?.api.group === this.group) {
|
||||||
|
this.selectedIndex = this.indexOf(e.id);
|
||||||
|
} else {
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
}
|
||||||
|
}),
|
||||||
addDisposableListener(this.element, 'pointerdown', (event) => {
|
addDisposableListener(this.element, 'pointerdown', (event) => {
|
||||||
if (event.defaultPrevented) {
|
if (event.defaultPrevented) {
|
||||||
return;
|
return;
|
||||||
@ -154,6 +166,7 @@ export class Tabs extends CompositeDisposable {
|
|||||||
for (const tab of this._tabs) {
|
for (const tab of this._tabs) {
|
||||||
const isActivePanel = panel.id === tab.value.panel.id;
|
const isActivePanel = panel.id === tab.value.panel.id;
|
||||||
tab.value.setActive(isActivePanel);
|
tab.value.setActive(isActivePanel);
|
||||||
|
tab.value.panel.runEvents();
|
||||||
|
|
||||||
if (isActivePanel) {
|
if (isActivePanel) {
|
||||||
const element = tab.value.element;
|
const element = tab.value.element;
|
||||||
@ -224,6 +237,40 @@ export class Tabs extends CompositeDisposable {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
tab.onKeyDown((event) => {
|
||||||
|
if (event.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.indexOf(tab.panel.id);
|
||||||
|
let nextTab: Tab | undefined = undefined;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
nextTab = this.tabs[Math.max(0, index - 1)];
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
nextTab = this.tabs[Math.min(this.size - 1, index + 1)];
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
nextTab = this.tabs[0];
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
nextTab = this.tabs[this.size - 1];
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
case 'Space':
|
||||||
|
nextTab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextTab != null &&
|
||||||
|
this.group.activePanel !== nextTab.panel
|
||||||
|
) {
|
||||||
|
nextTab.element.focus();
|
||||||
|
this.group.model.openPanel(nextTab.panel);
|
||||||
|
}
|
||||||
|
}),
|
||||||
tab.onDrop((event) => {
|
tab.onDrop((event) => {
|
||||||
this._onDrop.fire({
|
this._onDrop.fire({
|
||||||
event: event.nativeEvent,
|
event: event.nativeEvent,
|
||||||
|
@ -20,6 +20,9 @@
|
|||||||
&.dv-active-group {
|
&.dv-active-group {
|
||||||
> .dv-tabs-and-actions-container {
|
> .dv-tabs-and-actions-container {
|
||||||
.dv-tabs-container > .dv-tab {
|
.dv-tabs-container > .dv-tab {
|
||||||
|
&:focus {
|
||||||
|
outline: 1px solid var(--dv-paneview-active-outline-color);
|
||||||
|
}
|
||||||
&.dv-active-tab {
|
&.dv-active-tab {
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--dv-activegroup-visiblepanel-tab-background-color
|
--dv-activegroup-visiblepanel-tab-background-color
|
||||||
@ -38,6 +41,9 @@
|
|||||||
&.dv-inactive-group {
|
&.dv-inactive-group {
|
||||||
> .dv-tabs-and-actions-container {
|
> .dv-tabs-and-actions-container {
|
||||||
.dv-tabs-container > .dv-tab {
|
.dv-tabs-container > .dv-tab {
|
||||||
|
&:focus {
|
||||||
|
outline: 1px solid var(--dv-paneview-active-outline-color);
|
||||||
|
}
|
||||||
&.dv-active-tab {
|
&.dv-active-tab {
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--dv-inactivegroup-visiblepanel-tab-background-color
|
--dv-inactivegroup-visiblepanel-tab-background-color
|
||||||
|
@ -19,6 +19,8 @@ export interface IDockviewPanel extends IDisposable, IPanel {
|
|||||||
readonly api: DockviewPanelApi;
|
readonly api: DockviewPanelApi;
|
||||||
readonly title: string | undefined;
|
readonly title: string | undefined;
|
||||||
readonly params: Parameters | undefined;
|
readonly params: Parameters | undefined;
|
||||||
|
readonly componentElId: string;
|
||||||
|
readonly tabComponentElId: string;
|
||||||
readonly minimumWidth?: number;
|
readonly minimumWidth?: number;
|
||||||
readonly minimumHeight?: number;
|
readonly minimumHeight?: number;
|
||||||
readonly maximumWidth?: number;
|
readonly maximumWidth?: number;
|
||||||
@ -39,6 +41,18 @@ export class DockviewPanel
|
|||||||
implements IDockviewPanel
|
implements IDockviewPanel
|
||||||
{
|
{
|
||||||
readonly api: DockviewPanelApiImpl;
|
readonly api: DockviewPanelApiImpl;
|
||||||
|
/**
|
||||||
|
* The unique DOM id for the rendered panel element
|
||||||
|
*
|
||||||
|
* Used for accessibility attributes
|
||||||
|
*/
|
||||||
|
readonly componentElId: string;
|
||||||
|
/**
|
||||||
|
* The unique DOM id for the rendered tab element
|
||||||
|
*
|
||||||
|
* Used for accessibility attributes
|
||||||
|
*/
|
||||||
|
readonly tabComponentElId: string;
|
||||||
|
|
||||||
private _group: DockviewGroupPanel;
|
private _group: DockviewGroupPanel;
|
||||||
private _params?: Parameters;
|
private _params?: Parameters;
|
||||||
@ -100,6 +114,11 @@ export class DockviewPanel
|
|||||||
this._maximumWidth = options.maximumWidth;
|
this._maximumWidth = options.maximumWidth;
|
||||||
this._maximumHeight = options.maximumHeight;
|
this._maximumHeight = options.maximumHeight;
|
||||||
|
|
||||||
|
const randomId = Math.random().toString(36).slice(2);
|
||||||
|
|
||||||
|
this.tabComponentElId = `tab-${id}-${randomId}`;
|
||||||
|
this.componentElId = `tab-panel-${id}-${randomId}`;
|
||||||
|
|
||||||
this.api = new DockviewPanelApiImpl(
|
this.api = new DockviewPanelApiImpl(
|
||||||
this,
|
this,
|
||||||
this._group,
|
this._group,
|
||||||
|
@ -642,7 +642,7 @@ export class Gridview implements IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._root = root;
|
this._root = root;
|
||||||
this.element.appendChild(this._root.element);
|
this.element.prepend(this._root.element);
|
||||||
this.disposable.value = this._root.onDidChange((e) => {
|
this.disposable.value = this._root.onDidChange((e) => {
|
||||||
this._onDidChange.fire(e);
|
this._onDidChange.fire(e);
|
||||||
});
|
});
|
||||||
@ -698,7 +698,7 @@ export class Gridview implements IDisposable {
|
|||||||
this._root.addChild(oldRoot, Sizing.Distribute, 0);
|
this._root.addChild(oldRoot, Sizing.Distribute, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.element.appendChild(this._root.element);
|
this.element.prepend(this._root.element);
|
||||||
|
|
||||||
this.disposable.value = this._root.onDidChange((e) => {
|
this.disposable.value = this._root.onDidChange((e) => {
|
||||||
this._onDidChange.fire(e);
|
this._onDidChange.fire(e);
|
||||||
|
@ -73,12 +73,14 @@ export class OverlayRenderContainer extends CompositeDisposable {
|
|||||||
if (!this.map[panel.api.id]) {
|
if (!this.map[panel.api.id]) {
|
||||||
const element = createFocusableElement();
|
const element = createFocusableElement();
|
||||||
element.className = 'dv-render-overlay';
|
element.className = 'dv-render-overlay';
|
||||||
|
element.role = 'tabpanel';
|
||||||
|
element.tabIndex = 0;
|
||||||
|
element.setAttribute('aria-labelledby', panel.tabComponentElId);
|
||||||
|
|
||||||
this.map[panel.api.id] = {
|
this.map[panel.api.id] = {
|
||||||
panel,
|
panel,
|
||||||
disposable: Disposable.NONE,
|
disposable: Disposable.NONE,
|
||||||
destroy: Disposable.NONE,
|
destroy: Disposable.NONE,
|
||||||
|
|
||||||
element,
|
element,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,10 @@ export class Splitview {
|
|||||||
? 'dv-horizontal'
|
? 'dv-horizontal'
|
||||||
: 'dv-vertical'
|
: 'dv-vertical'
|
||||||
);
|
);
|
||||||
|
this.element.ariaOrientation =
|
||||||
|
this.orientation == Orientation.HORIZONTAL
|
||||||
|
? 'horizontal'
|
||||||
|
: 'vertical';
|
||||||
}
|
}
|
||||||
|
|
||||||
get minimumSize(): number {
|
get minimumSize(): number {
|
||||||
@ -1148,6 +1152,10 @@ export class Splitview {
|
|||||||
? 'dv-horizontal'
|
? 'dv-horizontal'
|
||||||
: 'dv-vertical';
|
: 'dv-vertical';
|
||||||
element.className = `dv-split-view-container ${orientationClassname}`;
|
element.className = `dv-split-view-container ${orientationClassname}`;
|
||||||
|
element.ariaOrientation =
|
||||||
|
this._orientation == Orientation.HORIZONTAL
|
||||||
|
? 'horizontal'
|
||||||
|
: 'vertical';
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user