Skip to content

Commit 9c04db8

Browse files
authored
Merge pull request #701 from iammola/tab-accessibility
feat: dockview accessibility
2 parents 88254df + eead24c commit 9c04db8

File tree

13 files changed

+244
-7
lines changed

13 files changed

+244
-7
lines changed

packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe('groupPanelApi', () => {
5050
const accessor = fromPartial<DockviewComponent>({
5151
onDidAddPanel: jest.fn(),
5252
onDidRemovePanel: jest.fn(),
53+
onDidActivePanelChange: jest.fn(),
5354
options: {},
5455
onDidOptionsChange: jest.fn(),
5556
});
@@ -83,6 +84,7 @@ describe('groupPanelApi', () => {
8384
const accessor = fromPartial<DockviewComponent>({
8485
onDidAddPanel: jest.fn(),
8586
onDidRemovePanel: jest.fn(),
87+
onDidActivePanelChange: jest.fn(),
8688
options: {},
8789
onDidOptionsChange: jest.fn(),
8890
});

packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('tabsContainer', () => {
1717
const accessor = fromPartial<DockviewComponent>({
1818
onDidAddPanel: jest.fn(),
1919
onDidRemovePanel: jest.fn(),
20+
onDidActivePanelChange: jest.fn(),
2021
options: {},
2122
onDidOptionsChange: jest.fn(),
2223
});
@@ -71,6 +72,7 @@ describe('tabsContainer', () => {
7172
id: 'testcomponentid',
7273
onDidAddPanel: jest.fn(),
7374
onDidRemovePanel: jest.fn(),
75+
onDidActivePanelChange: jest.fn(),
7476
options: {},
7577
onDidOptionsChange: jest.fn(),
7678
});
@@ -141,6 +143,7 @@ describe('tabsContainer', () => {
141143
id: 'testcomponentid',
142144
onDidAddPanel: jest.fn(),
143145
onDidRemovePanel: jest.fn(),
146+
onDidActivePanelChange: jest.fn(),
144147
options: {},
145148
onDidOptionsChange: jest.fn(),
146149
});
@@ -205,6 +208,7 @@ describe('tabsContainer', () => {
205208
id: 'testcomponentid',
206209
onDidAddPanel: jest.fn(),
207210
onDidRemovePanel: jest.fn(),
211+
onDidActivePanelChange: jest.fn(),
208212
options: {},
209213
onDidOptionsChange: jest.fn(),
210214
});
@@ -269,6 +273,7 @@ describe('tabsContainer', () => {
269273
id: 'testcomponentid',
270274
onDidAddPanel: jest.fn(),
271275
onDidRemovePanel: jest.fn(),
276+
onDidActivePanelChange: jest.fn(),
272277
options: {},
273278
onDidOptionsChange: jest.fn(),
274279
});
@@ -338,6 +343,7 @@ describe('tabsContainer', () => {
338343
id: 'testcomponentid',
339344
onDidAddPanel: jest.fn(),
340345
onDidRemovePanel: jest.fn(),
346+
onDidActivePanelChange: jest.fn(),
341347
options: {},
342348
onDidOptionsChange: jest.fn(),
343349
});
@@ -403,6 +409,7 @@ describe('tabsContainer', () => {
403409
id: 'testcomponentid',
404410
onDidAddPanel: jest.fn(),
405411
onDidRemovePanel: jest.fn(),
412+
onDidActivePanelChange: jest.fn(),
406413
options: {},
407414
onDidOptionsChange: jest.fn(),
408415
});
@@ -468,6 +475,7 @@ describe('tabsContainer', () => {
468475
options: {},
469476
onDidAddPanel: jest.fn(),
470477
onDidRemovePanel: jest.fn(),
478+
onDidActivePanelChange: jest.fn(),
471479
element: document.createElement('div'),
472480
addFloatingGroup: jest.fn(),
473481
doSetGroupActive: jest.fn(),
@@ -525,6 +533,7 @@ describe('tabsContainer', () => {
525533
options: {},
526534
onDidAddPanel: jest.fn(),
527535
onDidRemovePanel: jest.fn(),
536+
onDidActivePanelChange: jest.fn(),
528537
element: document.createElement('div'),
529538
addFloatingGroup: jest.fn(),
530539
doSetGroupActive: jest.fn(),
@@ -577,6 +586,7 @@ describe('tabsContainer', () => {
577586
options: {},
578587
onDidAddPanel: jest.fn(),
579588
onDidRemovePanel: jest.fn(),
589+
onDidActivePanelChange: jest.fn(),
580590
element: document.createElement('div'),
581591
addFloatingGroup: jest.fn(),
582592
getGroupPanel: jest.fn(),
@@ -634,6 +644,7 @@ describe('tabsContainer', () => {
634644
options: {},
635645
onDidAddPanel: jest.fn(),
636646
onDidRemovePanel: jest.fn(),
647+
onDidActivePanelChange: jest.fn(),
637648
element: document.createElement('div'),
638649
addFloatingGroup: jest.fn(),
639650
getGroupPanel: jest.fn(),
@@ -702,6 +713,7 @@ describe('tabsContainer', () => {
702713
options: {},
703714
onDidAddPanel: jest.fn(),
704715
onDidRemovePanel: jest.fn(),
716+
onDidActivePanelChange: jest.fn(),
705717
element: document.createElement('div'),
706718
addFloatingGroup: jest.fn(),
707719
getGroupPanel: jest.fn(),
@@ -770,6 +782,7 @@ describe('tabsContainer', () => {
770782
options: {},
771783
onDidAddPanel: jest.fn(),
772784
onDidRemovePanel: jest.fn(),
785+
onDidActivePanelChange: jest.fn(),
773786
element: document.createElement('div'),
774787
addFloatingGroup: jest.fn(),
775788
getGroupPanel: jest.fn(),

packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6917,4 +6917,91 @@ describe('dockviewComponent', () => {
69176917
dockview.layout(1000, 1000);
69186918
});
69196919
});
6920+
6921+
test('that arrow keys should activate appropriate tabs', () => {
6922+
dockview.layout(500, 1000);
6923+
6924+
dockview.addPanel({
6925+
id: 'panel1',
6926+
component: 'default',
6927+
});
6928+
6929+
dockview.addPanel({
6930+
id: 'panel2',
6931+
component: 'default',
6932+
position: { referencePanel: 'panel1', direction: 'within' },
6933+
});
6934+
6935+
dockview.addPanel({
6936+
id: 'panel3',
6937+
component: 'default',
6938+
});
6939+
6940+
dockview.addPanel({
6941+
id: 'panel4',
6942+
component: 'default',
6943+
position: { referencePanel: 'panel3', direction: 'below' },
6944+
});
6945+
6946+
const panel1 = dockview.getGroupPanel('panel1')!;
6947+
const panel2 = dockview.getGroupPanel('panel2')!;
6948+
const panel3 = dockview.getGroupPanel('panel3')!;
6949+
const panel4 = dockview.getGroupPanel('panel4')!;
6950+
6951+
panel1.api.setActive();
6952+
6953+
expect(panel1.api.isActive).toBeTruthy();
6954+
expect(panel2.api.isActive).toBeFalsy();
6955+
expect(panel3.api.isActive).toBeFalsy();
6956+
expect(panel4.api.isActive).toBeFalsy();
6957+
6958+
const tabsContainer = (panel: IDockviewPanel) =>
6959+
panel.api.group.element.querySelector('.tabs-container')!;
6960+
6961+
const event = new KeyboardEvent('keydown', { key: 'ArrowRight' });
6962+
6963+
fireEvent(tabsContainer(panel1), event);
6964+
expect(panel1.api.isActive).toBeFalsy();
6965+
expect(panel2.api.isActive).toBeTruthy();
6966+
expect(panel3.api.isActive).toBeFalsy();
6967+
expect(panel4.api.isActive).toBeFalsy();
6968+
6969+
fireEvent(tabsContainer(panel1), event);
6970+
expect(panel1.api.isActive).toBeFalsy();
6971+
expect(panel2.api.isActive).toBeFalsy();
6972+
expect(panel3.api.isActive).toBeTruthy();
6973+
expect(panel4.api.isActive).toBeFalsy();
6974+
6975+
const event2 = new KeyboardEvent('keydown', { key: 'ArrowLeft' });
6976+
6977+
fireEvent(tabsContainer(panel1), event2);
6978+
expect(panel1.api.isActive).toBeFalsy();
6979+
expect(panel2.api.isActive).toBeTruthy();
6980+
expect(panel3.api.isActive).toBeFalsy();
6981+
expect(panel4.api.isActive).toBeFalsy();
6982+
6983+
fireEvent(tabsContainer(panel1), event2);
6984+
expect(panel1.api.isActive).toBeTruthy();
6985+
expect(panel2.api.isActive).toBeFalsy();
6986+
expect(panel3.api.isActive).toBeFalsy();
6987+
expect(panel4.api.isActive).toBeFalsy();
6988+
6989+
panel4.api.setActive();
6990+
expect(panel1.api.isActive).toBeFalsy();
6991+
expect(panel2.api.isActive).toBeFalsy();
6992+
expect(panel3.api.isActive).toBeFalsy();
6993+
expect(panel4.api.isActive).toBeTruthy();
6994+
6995+
fireEvent(tabsContainer(panel4), event2);
6996+
expect(panel1.api.isActive).toBeFalsy();
6997+
expect(panel2.api.isActive).toBeFalsy();
6998+
expect(panel3.api.isActive).toBeFalsy();
6999+
expect(panel4.api.isActive).toBeTruthy();
7000+
7001+
fireEvent(tabsContainer(panel4), event);
7002+
expect(panel1.api.isActive).toBeFalsy();
7003+
expect(panel2.api.isActive).toBeFalsy();
7004+
expect(panel3.api.isActive).toBeFalsy();
7005+
expect(panel4.api.isActive).toBeTruthy();
7006+
});
69207007
});

packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ export class TestPanel implements IDockviewPanel {
174174
private _group: DockviewGroupPanel | undefined;
175175
private _params: IGroupPanelInitParameters | undefined;
176176
readonly view: IDockviewPanelModel;
177+
readonly componentElId: string;
178+
readonly tabComponentElId: string;
177179

178180
get title() {
179181
return '';
@@ -189,6 +191,8 @@ export class TestPanel implements IDockviewPanel {
189191

190192
constructor(public readonly id: string, public api: DockviewPanelApi) {
191193
this.view = new TestModel(id);
194+
this.tabComponentElId = `tab-${id}`;
195+
this.componentElId = `tab-panel-${id}`;
192196
this.init({
193197
title: `${id}`,
194198
params: {},
@@ -266,6 +270,7 @@ describe('dockviewGroupPanelModel', () => {
266270
removeGroup: removeGroupMock,
267271
onDidAddPanel: () => ({ dispose: jest.fn() }),
268272
onDidRemovePanel: () => ({ dispose: jest.fn() }),
273+
onDidActivePanelChange: () => ({ dispose: jest.fn() }),
269274
overlayRenderContainer: new OverlayRenderContainer(
270275
document.createElement('div'),
271276
fromPartial<DockviewComponent>({})
@@ -653,6 +658,7 @@ describe('dockviewGroupPanelModel', () => {
653658
onDidAddPanel: jest.fn(),
654659
onDidRemovePanel: jest.fn(),
655660
onDidOptionsChange: jest.fn(),
661+
onDidActivePanelChange: jest.fn(),
656662
});
657663

658664
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@@ -716,6 +722,7 @@ describe('dockviewGroupPanelModel', () => {
716722
onDidAddPanel: jest.fn(),
717723
onDidRemovePanel: jest.fn(),
718724
onDidOptionsChange: jest.fn(),
725+
onDidActivePanelChange: jest.fn(),
719726
});
720727

721728
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@@ -808,6 +815,7 @@ describe('dockviewGroupPanelModel', () => {
808815
doSetGroupActive: jest.fn(),
809816
onDidAddPanel: jest.fn(),
810817
onDidRemovePanel: jest.fn(),
818+
onDidActivePanelChange: jest.fn(),
811819
overlayRenderContainer: new OverlayRenderContainer(
812820
document.createElement('div'),
813821
fromPartial<DockviewComponent>({})
@@ -875,6 +883,7 @@ describe('dockviewGroupPanelModel', () => {
875883
doSetGroupActive: jest.fn(),
876884
onDidAddPanel: jest.fn(),
877885
onDidRemovePanel: jest.fn(),
886+
onDidActivePanelChange: jest.fn(),
878887
overlayRenderContainer: new OverlayRenderContainer(
879888
document.createElement('div'),
880889
fromPartial<DockviewComponent>({})
@@ -949,6 +958,7 @@ describe('dockviewGroupPanelModel', () => {
949958
doSetGroupActive: jest.fn(),
950959
onDidAddPanel: jest.fn(),
951960
onDidRemovePanel: jest.fn(),
961+
onDidActivePanelChange: jest.fn(),
952962
overlayRenderContainer: new OverlayRenderContainer(
953963
document.createElement('div'),
954964
fromPartial<DockviewComponent>({})
@@ -1030,6 +1040,7 @@ describe('dockviewGroupPanelModel', () => {
10301040
return {
10311041
id: 'testgroupid',
10321042
model: groupView,
1043+
dispose: jest.fn()
10331044
};
10341045
});
10351046

packages/dockview-core/src/__tests__/gridview/gridviewPanel.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe('gridviewPanel', () => {
77
return {
88
onDidAddPanel: jest.fn(),
99
onDidRemovePanel: jest.fn(),
10+
onDidActivePanelChange: jest.fn(),
1011
options: {},
1112
onDidOptionsChange: jest.fn(),
1213
} as any;

packages/dockview-core/src/dockview/components/tab/defaultTab.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createCloseButton } from '../../../svg';
66
export class DefaultTab extends CompositeDisposable implements ITabRenderer {
77
private readonly _element: HTMLElement;
88
private readonly _content: HTMLElement;
9-
private readonly action: HTMLElement;
9+
private readonly action: HTMLButtonElement;
1010
private _title: string | undefined;
1111

1212
get element(): HTMLElement {
@@ -22,22 +22,38 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
2222
this._content = document.createElement('div');
2323
this._content.className = 'dv-default-tab-content';
2424

25-
this.action = document.createElement('div');
25+
this.action = document.createElement('button');
26+
this.action.type = 'button';
2627
this.action.className = 'dv-default-tab-action';
28+
// originally hide this, so only when it is focused is it read out.
29+
// so the SR when focused on the tab, doesn't read "<Tab Content> Close Button"
30+
this.action.ariaHidden = 'true';
31+
2732
this.action.appendChild(createCloseButton());
2833

2934
this._element.appendChild(this._content);
3035
this._element.appendChild(this.action);
3136

37+
this.addDisposables(
38+
addDisposableListener(this.action, 'focus', (event) => {
39+
this.action.ariaHidden = 'false';
40+
}),
41+
addDisposableListener(this.action, 'blur', (event) => {
42+
this.action.ariaHidden = 'true';
43+
})
44+
);
45+
3246
this.render();
3347
}
3448

3549
init(params: GroupPanelPartInitParameters): void {
3650
this._title = params.title;
37-
51+
this.action.ariaLabel = `Close "${this._title}" tab`;
52+
3853
this.addDisposables(
3954
params.api.onDidTitleChange((event) => {
4055
this._title = event.title;
56+
this.action.ariaLabel = `Close "${event.title}" tab`;
4157
this.render();
4258
}),
4359
addDisposableListener(this.action, 'pointerdown', (ev) => {
@@ -50,6 +66,18 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
5066

5167
ev.preventDefault();
5268
params.api.close();
69+
}),
70+
addDisposableListener(this.action, 'keydown', (ev) => {
71+
if (ev.defaultPrevented) {
72+
return;
73+
}
74+
75+
switch (ev.key) {
76+
case 'Enter':
77+
case 'Space':
78+
params.api.close();
79+
break;
80+
}
5381
})
5482
);
5583

0 commit comments

Comments
 (0)