From 692588c887d4b0407d91d697c30d874a6e8b8575 Mon Sep 17 00:00:00 2001 From: Ademola Adedeji Date: Wed, 28 Aug 2024 11:09:04 -0300 Subject: [PATCH 1/9] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20access:=20add=20accesi?= =?UTF-8?q?bility=20attributes=20to=20elements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/dockview/components/tab/tab.ts | 10 ++++- .../components/titlebar/tabsContainer.ts | 43 +++++++++++++++++++ .../src/dockview/dockviewPanel.ts | 17 +++++++- .../dockview-core/src/dockview/options.ts | 12 ++++++ .../dockview-core/src/gridview/gridview.ts | 4 +- .../src/overlay/overlayRenderContainer.ts | 4 +- 6 files changed, 85 insertions(+), 5 deletions(-) diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index c97f1f110..ec3de0c29 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -72,10 +72,15 @@ export class Tab extends CompositeDisposable { super(); this._element = document.createElement('div'); + this._element.id = this.panel.tabComponentElId; this._element.className = 'tab'; - this._element.tabIndex = 0; this._element.draggable = true; + this._element.role = 'tab'; + this._element.tabIndex = -1; + this._element.ariaSelected = 'false'; + this._element.setAttribute('aria-controls', this.panel.componentElId); + toggleClass(this.element, 'inactive-tab', true); const dragHandler = new TabDragHandler( @@ -139,6 +144,9 @@ export class Tab extends CompositeDisposable { } public setActive(isActive: boolean): void { + this.element.tabIndex = isActive ? 0 : -1; + this.element.ariaSelected = isActive.toString(); + toggleClass(this.element, 'active-tab', isActive); toggleClass(this.element, 'inactive-tab', !isActive); } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 7557f3fde..326b269cf 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -195,6 +195,7 @@ export class TabsContainer this.tabContainer = document.createElement('div'); this.tabContainer.className = 'tabs-container'; + this.tabContainer.role = 'tablist'; this.voidContainer = new VoidContainer(this.accessor, this.group); @@ -223,6 +224,13 @@ export class TabsContainer ); } }), + this.accessor.onDidActivePanelChange((e) => { + if (e?.api.group === this.group) { + this.selectedIndex = this.indexOf(e.id); + } else { + this.selectedIndex = -1; + } + }), this._onWillShowOverlay, this._onDrop, this._onTabDragStart, @@ -288,6 +296,40 @@ export class TabsContainer if (isLeftClick) { this.accessor.doSetGroupActive(this.group); } + }), + addDisposableListener(this.tabContainer, 'keydown', (event) => { + if (event.defaultPrevented) { + return; + } + + let tab: IValueDisposable | undefined = undefined; + + switch (event.key) { + case 'ArrowLeft': { + if (this.selectedIndex > 0) { + tab = this.tabs[this.selectedIndex - 1]; + } + break; + } + case 'ArrowRight': { + if (this.selectedIndex + 1 < this.size) { + tab = this.tabs[this.selectedIndex + 1]; + } + break; + } + case 'Home': + tab = this.tabs[0]; + break; + case 'End': + tab = this.tabs[this.size - 1]; + break; + } + + if (tab == null) { + return; + } + + this.group.model.openPanel(tab.value.panel); }) ); } @@ -336,6 +378,7 @@ export class TabsContainer this.tabs.forEach((tab) => { const isActivePanel = panel.id === tab.value.panel.id; tab.value.setActive(isActivePanel); + tab.value.panel.runEvents(); }); } diff --git a/packages/dockview-core/src/dockview/dockviewPanel.ts b/packages/dockview-core/src/dockview/dockviewPanel.ts index 984c3fcdb..6d93777cf 100644 --- a/packages/dockview-core/src/dockview/dockviewPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanel.ts @@ -18,6 +18,8 @@ export interface IDockviewPanel extends IDisposable, IPanel { readonly api: DockviewPanelApi; readonly title: string | undefined; readonly params: Parameters | undefined; + readonly componentElId: string; + readonly tabComponentElId: string; updateParentGroup( group: DockviewGroupPanel, options?: { skipSetActive?: boolean } @@ -34,6 +36,8 @@ export class DockviewPanel implements IDockviewPanel { readonly api: DockviewPanelApiImpl; + readonly componentElId: string; + readonly tabComponentElId: string; private _group: DockviewGroupPanel; private _params?: Parameters; @@ -64,12 +68,23 @@ export class DockviewPanel private readonly containerApi: DockviewApi, group: DockviewGroupPanel, readonly view: IDockviewPanelModel, - options: { renderer?: DockviewPanelRenderer } + options: { + renderer?: DockviewPanelRenderer; + componentElId?: string; + tabComponentElId?: string; + } ) { super(); this._renderer = options.renderer; this._group = group; + const randomId = Math.random().toString(36).slice(2); + + this.tabComponentElId = + options.tabComponentElId ?? `tab-${id}-${randomId}`; + this.componentElId = + options.componentElId ?? `tab-panel-${id}-${randomId}`; + this.api = new DockviewPanelApiImpl( this, this._group, diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 5fce93ebf..936e94dfe 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -230,6 +230,18 @@ export type AddPanelOptions

= { * Defaults to `false` which forces newly added panels to become active. */ inactive?: boolean; + /** + * The unique DOM id for the rendered panel element + * + * Used for accessibility attributes + */ + componentElId?: string; + /** + * The unique DOM id for the rendered tab element + * + * Used for accessibility attributes + */ + tabComponentElId?: string; } & Partial; type AddGroupOptionsWithPanel = { diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index f2b4806cf..daf99f1a6 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -575,7 +575,7 @@ export class Gridview implements IDisposable { } this._root = root; - this.element.appendChild(this._root.element); + this.element.prepend(this._root.element); this.disposable.value = this._root.onDidChange((e) => { this._onDidChange.fire(e); }); @@ -631,7 +631,7 @@ export class Gridview implements IDisposable { 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._onDidChange.fire(e); diff --git a/packages/dockview-core/src/overlay/overlayRenderContainer.ts b/packages/dockview-core/src/overlay/overlayRenderContainer.ts index 1d68042e9..25a24048b 100644 --- a/packages/dockview-core/src/overlay/overlayRenderContainer.ts +++ b/packages/dockview-core/src/overlay/overlayRenderContainer.ts @@ -74,12 +74,14 @@ export class OverlayRenderContainer extends CompositeDisposable { if (!this.map[panel.api.id]) { const element = createFocusableElement(); element.className = 'dv-render-overlay'; + element.role = 'tabpanel'; + element.tabIndex = 0; + element.setAttribute('aria-labelledby', panel.tabComponentElId); this.map[panel.api.id] = { panel, disposable: Disposable.NONE, destroy: Disposable.NONE, - element, }; } From 23a7bbeb05ce47e2685253d7901e38164d8a2697 Mon Sep 17 00:00:00 2001 From: Ademola Adedeji Date: Wed, 28 Aug 2024 11:09:24 -0300 Subject: [PATCH 2/9] =?UTF-8?q?=E2=9C=85=20test:=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/api/dockviewPanelApi.spec.ts | 2 + .../components/titlebar/tabsContainer.spec.ts | 13 +++ .../dockview/dockviewComponent.spec.ts | 89 ++++++++++++++++++- .../dockview/dockviewGroupPanelModel.spec.ts | 11 +++ .../__tests__/gridview/gridviewPanel.spec.ts | 1 + 5 files changed, 115 insertions(+), 1 deletion(-) diff --git a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts index 0bc24eaa5..720823242 100644 --- a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts +++ b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts @@ -49,6 +49,7 @@ describe('groupPanelApi', () => { const accessor = fromPartial({ onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, }); @@ -81,6 +82,7 @@ describe('groupPanelApi', () => { const accessor = fromPartial({ onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, }); diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index bdcc370ee..d751a26e0 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -16,6 +16,7 @@ describe('tabsContainer', () => { const accessor = fromPartial({ onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, }); @@ -69,6 +70,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, }); @@ -135,6 +137,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, }); @@ -198,6 +201,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, }); @@ -261,6 +265,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, }); @@ -329,6 +334,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, }); @@ -393,6 +399,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, }); @@ -457,6 +464,7 @@ describe('tabsContainer', () => { options: {}, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), element: document.createElement('div'), addFloatingGroup: jest.fn(), }); @@ -511,6 +519,7 @@ describe('tabsContainer', () => { options: {}, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), element: document.createElement('div'), addFloatingGroup: jest.fn(), }); @@ -560,6 +569,7 @@ describe('tabsContainer', () => { options: {}, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), element: document.createElement('div'), addFloatingGroup: jest.fn(), getGroupPanel: jest.fn(), @@ -616,6 +626,7 @@ describe('tabsContainer', () => { options: {}, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), element: document.createElement('div'), addFloatingGroup: jest.fn(), getGroupPanel: jest.fn(), @@ -683,6 +694,7 @@ describe('tabsContainer', () => { options: {}, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), element: document.createElement('div'), addFloatingGroup: jest.fn(), getGroupPanel: jest.fn(), @@ -750,6 +762,7 @@ describe('tabsContainer', () => { options: {}, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), element: document.createElement('div'), addFloatingGroup: jest.fn(), getGroupPanel: jest.fn(), diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index ae45adf8f..e3580a705 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -8,7 +8,7 @@ import { PanelUpdateEvent } from '../../panel/types'; import { Orientation } from '../../splitview/splitview'; import { CompositeDisposable } from '../../lifecycle'; import { Emitter } from '../../events'; -import { IDockviewPanel } from '../../dockview/dockviewPanel'; +import { IDockviewPanel } from '../../dockview/dockviewPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { fireEvent, queryByTestId } from '@testing-library/dom'; import { getPanelData } from '../../dnd/dataTransfer'; @@ -5637,4 +5637,91 @@ describe('dockviewComponent', () => { expect(dockview.gap).toBe(15); }); }); + + 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(); + }); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 01d0c84d1..a93659dd7 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -188,6 +188,8 @@ export class TestPanel implements IDockviewPanel { private _group: DockviewGroupPanel | undefined; private _params: IGroupPanelInitParameters | undefined; readonly view: IDockviewPanelModel; + readonly componentElId: string; + readonly tabComponentElId: string; get title() { return ''; @@ -203,6 +205,8 @@ export class TestPanel implements IDockviewPanel { constructor(public readonly id: string, public api: DockviewPanelApi) { this.view = new TestModel(id); + this.tabComponentElId = `tab-${id}`; + this.componentElId = `tab-panel-${id}`; this.init({ title: `${id}`, params: {}, @@ -280,6 +284,7 @@ describe('dockviewGroupPanelModel', () => { removeGroup: removeGroupMock, onDidAddPanel: () => ({ dispose: jest.fn() }), onDidRemovePanel: () => ({ dispose: jest.fn() }), + onDidActivePanelChange: () => ({ dispose: jest.fn() }), overlayRenderContainer: new OverlayRenderContainer( document.createElement('div'), fromPartial({}) @@ -665,6 +670,7 @@ describe('dockviewGroupPanelModel', () => { getPanel: jest.fn(), onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), }); const groupviewMock = jest.fn, []>( @@ -727,6 +733,7 @@ describe('dockviewGroupPanelModel', () => { getPanel: jest.fn(), onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), }); const groupviewMock = jest.fn, []>( @@ -819,6 +826,7 @@ describe('dockviewGroupPanelModel', () => { doSetGroupActive: jest.fn(), onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), overlayRenderContainer: new OverlayRenderContainer( document.createElement('div'), fromPartial({}) @@ -891,6 +899,7 @@ describe('dockviewGroupPanelModel', () => { doSetGroupActive: jest.fn(), onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), overlayRenderContainer: new OverlayRenderContainer( document.createElement('div'), fromPartial({}) @@ -964,6 +973,7 @@ describe('dockviewGroupPanelModel', () => { doSetGroupActive: jest.fn(), onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), overlayRenderContainer: new OverlayRenderContainer( document.createElement('div'), fromPartial({}) @@ -1044,6 +1054,7 @@ describe('dockviewGroupPanelModel', () => { return { id: 'testgroupid', model: groupView, + dispose: jest.fn() }; }); diff --git a/packages/dockview-core/src/__tests__/gridview/gridviewPanel.spec.ts b/packages/dockview-core/src/__tests__/gridview/gridviewPanel.spec.ts index cfae5ea6b..88672e774 100644 --- a/packages/dockview-core/src/__tests__/gridview/gridviewPanel.spec.ts +++ b/packages/dockview-core/src/__tests__/gridview/gridviewPanel.spec.ts @@ -7,6 +7,7 @@ describe('gridviewPanel', () => { return { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidActivePanelChange: jest.fn(), options: {}, } as any; }); From 9d9a9281c26e0ebbbce8cb12dee1a7c98de454cb Mon Sep 17 00:00:00 2001 From: Ademola Adedeji Date: Wed, 2 Apr 2025 11:56:16 -0300 Subject: [PATCH 3/9] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20access:=20remove=20id?= =?UTF-8?q?=20options,=20focus=20tab=20on=20select,=20close=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/dockview/components/tab/defaultTab.ts | 12 ++++++++++ .../src/dockview/components/tab/tab.ts | 4 +--- .../src/dockview/components/titlebar/tabs.ts | 5 +++-- .../src/dockview/dockviewPanel.ts | 22 +++++++++++-------- .../dockview-core/src/dockview/options.ts | 12 ---------- 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/dockview-core/src/dockview/components/tab/defaultTab.ts b/packages/dockview-core/src/dockview/components/tab/defaultTab.ts index 205e4e562..2d595242d 100644 --- a/packages/dockview-core/src/dockview/components/tab/defaultTab.ts +++ b/packages/dockview-core/src/dockview/components/tab/defaultTab.ts @@ -24,16 +24,28 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer { this.action = document.createElement('div'); this.action.className = 'dv-default-tab-action'; + this.action.tabIndex = 0; + this.action.ariaHidden = 'true'; this.action.appendChild(createCloseButton()); this._element.appendChild(this._content); 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(); } init(params: GroupPanelPartInitParameters): void { this._title = params.title; + this.action.ariaLabel = `Close ${this._title}`; this.addDisposables( params.api.onDidTitleChange((event) => { diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index c40eff92f..5933b844c 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -75,11 +75,9 @@ export class Tab extends CompositeDisposable { this._element = document.createElement('div'); this._element.id = this.panel.tabComponentElId; this._element.className = 'dv-tab'; - this._element.tabIndex = 0; - this._element.draggable = true; - this._element.role = 'tab'; this._element.tabIndex = -1; + this._element.draggable = true; this._element.ariaSelected = 'false'; this._element.setAttribute('aria-controls', this.panel.componentElId); diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts index 5549b6e23..d4f4b489d 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts @@ -109,7 +109,7 @@ export class Tabs extends CompositeDisposable { this.addDisposables(scrollbar); } - this.element.role = "tablist" + this.element.role = 'tablist'; this.addDisposables( this._onOverflowTabsChange, @@ -167,6 +167,7 @@ export class Tabs extends CompositeDisposable { return; } + tab.value.element.focus(); this.group.model.openPanel(tab.value.panel); }), Disposable.from(() => { @@ -197,7 +198,7 @@ export class Tabs extends CompositeDisposable { for (const tab of this._tabs) { const isActivePanel = panel.id === tab.value.panel.id; tab.value.setActive(isActivePanel); - tab.value.panel.runEvents() + tab.value.panel.runEvents(); if (isActivePanel) { const element = tab.value.element; diff --git a/packages/dockview-core/src/dockview/dockviewPanel.ts b/packages/dockview-core/src/dockview/dockviewPanel.ts index 0c26f6eb8..cf904ff16 100644 --- a/packages/dockview-core/src/dockview/dockviewPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanel.ts @@ -41,7 +41,17 @@ export class DockviewPanel implements IDockviewPanel { 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; @@ -94,11 +104,7 @@ export class DockviewPanel private readonly containerApi: DockviewApi, group: DockviewGroupPanel, readonly view: IDockviewPanelModel, - options: { - renderer?: DockviewPanelRenderer; - componentElId?: string; - tabComponentElId?: string; - } & Partial + options: { renderer?: DockviewPanelRenderer } & Partial ) { super(); this._renderer = options.renderer; @@ -110,10 +116,8 @@ export class DockviewPanel const randomId = Math.random().toString(36).slice(2); - this.tabComponentElId = - options.tabComponentElId ?? `tab-${id}-${randomId}`; - this.componentElId = - options.componentElId ?? `tab-panel-${id}-${randomId}`; + this.tabComponentElId = `tab-${id}-${randomId}`; + this.componentElId = `tab-panel-${id}-${randomId}`; this.api = new DockviewPanelApiImpl( this, diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 762003461..39e66d2c3 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -258,18 +258,6 @@ export type AddPanelOptions

= { * Defaults to `false` which forces newly added panels to become active. */ inactive?: boolean; - /** - * The unique DOM id for the rendered panel element - * - * Used for accessibility attributes - */ - componentElId?: string; - /** - * The unique DOM id for the rendered tab element - * - * Used for accessibility attributes - */ - tabComponentElId?: string; initialWidth?: number; initialHeight?: number; } & Partial & From cef78168f7f766617bc3c013224087a8dc2512e8 Mon Sep 17 00:00:00 2001 From: Ademola Adedeji Date: Fri, 4 Apr 2025 09:18:47 -0300 Subject: [PATCH 4/9] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20access:=20allow=20Ente?= =?UTF-8?q?r/Space=20to=20trigger=20tab=20close?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/dockview/components/tab/defaultTab.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/dockview-core/src/dockview/components/tab/defaultTab.ts b/packages/dockview-core/src/dockview/components/tab/defaultTab.ts index 2d595242d..7105f2024 100644 --- a/packages/dockview-core/src/dockview/components/tab/defaultTab.ts +++ b/packages/dockview-core/src/dockview/components/tab/defaultTab.ts @@ -6,7 +6,7 @@ import { createCloseButton } from '../../../svg'; export class DefaultTab extends CompositeDisposable implements ITabRenderer { private readonly _element: HTMLElement; private readonly _content: HTMLElement; - private readonly action: HTMLElement; + private readonly action: HTMLButtonElement; private _title: string | undefined; get element(): HTMLElement { @@ -22,10 +22,13 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer { this._content = document.createElement('div'); 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.tabIndex = 0; + // originally hide this, so only when it is focused is it read out. + // so the SR when focused on the tab, doesn't read " Close Button" this.action.ariaHidden = 'true'; + this.action.appendChild(createCloseButton()); this._element.appendChild(this._content); @@ -45,11 +48,12 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer { init(params: GroupPanelPartInitParameters): void { this._title = params.title; - this.action.ariaLabel = `Close ${this._title}`; - + this.action.ariaLabel = `Close "${this._title}" tab`; + this.addDisposables( params.api.onDidTitleChange((event) => { this._title = event.title; + this.action.ariaLabel = `Close "${event.title}" tab`; this.render(); }), addDisposableListener(this.action, 'pointerdown', (ev) => { @@ -62,6 +66,18 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer { ev.preventDefault(); params.api.close(); + }), + addDisposableListener(this.action, 'keydown', (ev) => { + if (ev.defaultPrevented) { + return; + } + + switch (ev.key) { + case 'Enter': + case 'Space': + params.api.close(); + break; + } }) ); From 000fa474ba8085e3c988308ec864e42a17be919a Mon Sep 17 00:00:00 2001 From: Ademola Adedeji Date: Fri, 4 Apr 2025 09:45:58 -0300 Subject: [PATCH 5/9] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20access:=20add=20outlin?= =?UTF-8?q?e=20to=20tab=20on=20focus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/dockview-core/src/dockview/dockviewComponent.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/dockview-core/src/dockview/dockviewComponent.scss b/packages/dockview-core/src/dockview/dockviewComponent.scss index 440f6cc5e..df182caab 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.scss +++ b/packages/dockview-core/src/dockview/dockviewComponent.scss @@ -20,6 +20,9 @@ &.dv-active-group { > .dv-tabs-and-actions-container { .dv-tabs-container > .dv-tab { + &:focus { + outline: 1px solid var(--dv-paneview-active-outline-color); + } &.dv-active-tab { background-color: var( --dv-activegroup-visiblepanel-tab-background-color @@ -38,6 +41,9 @@ &.dv-inactive-group { > .dv-tabs-and-actions-container { .dv-tabs-container > .dv-tab { + &:focus { + outline: 1px solid var(--dv-paneview-active-outline-color); + } &.dv-active-tab { background-color: var( --dv-inactivegroup-visiblepanel-tab-background-color From 5c78f2bd67626bdcc6b4ace2dd762c6ae5b664ab Mon Sep 17 00:00:00 2001 From: Ademola Adedeji Date: Fri, 4 Apr 2025 10:59:49 -0300 Subject: [PATCH 6/9] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20access:=20allow=20Ente?= =?UTF-8?q?r/Space=20on=20tab=20to=20activate=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dockview-core/src/dockview/components/tab/tab.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index 5933b844c..159de4f96 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -143,6 +143,18 @@ export class Tab extends CompositeDisposable { addDisposableListener(this._element, 'pointerdown', (event) => { this._onPointDown.fire(event); }), + addDisposableListener(this.element, 'keydown', (event) => { + if (event.defaultPrevented) { + return; + } + + switch (event.key) { + case 'Enter': + case 'Space': + this.group.model.openPanel(this.panel); + break; + } + }), this.dropTarget.onDrop((event) => { this._onDropped.fire(event); }), From 8606692e3884a8185888e0b03bbeff06f8fede08 Mon Sep 17 00:00:00 2001 From: Ademola Adedeji Date: Fri, 4 Apr 2025 11:15:05 -0300 Subject: [PATCH 7/9] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20access:=20add=20instru?= =?UTF-8?q?ctions=20to=20tab-list,=20and=20orientation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/dockview/components/titlebar/tabs.ts | 3 +++ packages/dockview-core/src/splitview/splitview.ts | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts index d4f4b489d..08fc329de 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts @@ -98,6 +98,7 @@ export class Tabs extends CompositeDisposable { this._tabsList = document.createElement('div'); this._tabsList.className = 'dv-tabs-container dv-horizontal'; + this._tabsList.ariaOrientation = 'horizontal'; this.showTabsOverflowControl = options.showTabsOverflowControl; @@ -110,6 +111,8 @@ export class Tabs extends CompositeDisposable { } this.element.role = 'tablist'; + this.element.ariaLabel = + 'Use 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._onOverflowTabsChange, diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index c511d54f6..a7e6b2d0e 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -165,6 +165,10 @@ export class Splitview { ? 'dv-horizontal' : 'dv-vertical' ); + this.element.ariaOrientation = + this.orientation == Orientation.HORIZONTAL + ? 'horizontal' + : 'vertical'; } get minimumSize(): number { @@ -1148,6 +1152,10 @@ export class Splitview { ? 'dv-horizontal' : 'dv-vertical'; element.className = `dv-split-view-container ${orientationClassname}`; + element.ariaOrientation = + this._orientation == Orientation.HORIZONTAL + ? 'horizontal' + : 'vertical'; return element; } From 57a1624e754216e573c8ff706711b3c8ee45f949 Mon Sep 17 00:00:00 2001 From: Ademola Adedeji Date: Fri, 4 Apr 2025 15:51:22 -0300 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=92=AC=20text:=20verbiage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/dockview-core/src/dockview/components/titlebar/tabs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts index 08fc329de..1e60d28b7 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts @@ -112,7 +112,7 @@ export class Tabs extends CompositeDisposable { this.element.role = 'tablist'; this.element.ariaLabel = - 'Use 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.'; + '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._onOverflowTabsChange, From b5d28ce042d71e5a05c9125808531b67f51a1dfb Mon Sep 17 00:00:00 2001 From: Ademola Adedeji Date: Mon, 14 Apr 2025 13:03:54 -0300 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=90=9B=20fix:=20use=20focused=20tab?= =?UTF-8?q?=20as=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/dockview/components/tab/tab.ts | 16 ++--- .../src/dockview/components/titlebar/tabs.ts | 69 +++++++++---------- 2 files changed, 39 insertions(+), 46 deletions(-) diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index 159de4f96..de09c714b 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -52,6 +52,9 @@ export class Tab extends CompositeDisposable { private readonly _onPointDown = new Emitter(); readonly onPointerDown: Event = this._onPointDown.event; + + private readonly _onKeyDown = new Emitter(); + readonly onKeyDown: Event = this._onKeyDown.event; private readonly _onDropped = new Emitter(); readonly onDrop: Event = this._onDropped.event; @@ -143,17 +146,8 @@ export class Tab extends CompositeDisposable { addDisposableListener(this._element, 'pointerdown', (event) => { this._onPointDown.fire(event); }), - addDisposableListener(this.element, 'keydown', (event) => { - if (event.defaultPrevented) { - return; - } - - switch (event.key) { - case 'Enter': - case 'Space': - this.group.model.openPanel(this.panel); - break; - } + addDisposableListener(this._element, 'keydown', (event) => { + this._onKeyDown.fire(event); }), this.dropTarget.onDrop((event) => { this._onDropped.fire(event); diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts index 1e60d28b7..317c11b18 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts @@ -138,41 +138,6 @@ export class Tabs extends CompositeDisposable { this.accessor.doSetGroupActive(this.group); } }), - addDisposableListener(this.element, 'keydown', (event) => { - if (event.defaultPrevented) { - return; - } - - let tab: IValueDisposable | undefined = undefined; - - switch (event.key) { - case 'ArrowLeft': { - if (this.selectedIndex > 0) { - tab = this._tabs[this.selectedIndex - 1]; - } - break; - } - case 'ArrowRight': { - if (this.selectedIndex + 1 < this.size) { - tab = this._tabs[this.selectedIndex + 1]; - } - break; - } - case 'Home': - tab = this._tabs[0]; - break; - case 'End': - tab = this._tabs[this.size - 1]; - break; - } - - if (tab == null) { - return; - } - - tab.value.element.focus(); - this.group.model.openPanel(tab.value.panel); - }), Disposable.from(() => { for (const { value, disposable } of this._tabs) { disposable.dispose(); @@ -272,6 +237,40 @@ export class Tabs extends CompositeDisposable { 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) => { this._onDrop.fire({ event: event.nativeEvent,