diff --git a/packages/dockview-core/src/__tests__/dockview/components/tab/defaultTab.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/tab/defaultTab.spec.ts index 2189282dd..5536bb641 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/tab/defaultTab.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/tab/defaultTab.spec.ts @@ -60,4 +60,81 @@ describe('defaultTab', () => { fireEvent.click(el!); expect(api.close).toHaveBeenCalledTimes(1); }); + + test('that close button prevents default behavior', () => { + const cut = new DefaultTab(); + + const api = fromPartial({ + onDidTitleChange: jest.fn(), + close: jest.fn(), + }); + const containerApi = fromPartial({}); + + cut.init({ + api, + containerApi, + params: {}, + title: 'title_abc', + }); + + let el = cut.element.querySelector('.dv-default-tab-action'); + + // Create a custom event to verify preventDefault is called + const clickEvent = new Event('click', { cancelable: true }); + const preventDefaultSpy = jest.spyOn(clickEvent, 'preventDefault'); + + el!.dispatchEvent(clickEvent); + + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + expect(api.close).toHaveBeenCalledTimes(1); + }); + + test('that close button respects already prevented events', () => { + const cut = new DefaultTab(); + + const api = fromPartial({ + onDidTitleChange: jest.fn(), + close: jest.fn(), + }); + const containerApi = fromPartial({}); + + cut.init({ + api, + containerApi, + params: {}, + title: 'title_abc', + }); + + let el = cut.element.querySelector('.dv-default-tab-action'); + + // Create a custom event and prevent it before dispatching + const clickEvent = new Event('click', { cancelable: true }); + clickEvent.preventDefault(); + + el!.dispatchEvent(clickEvent); + + // Close should not be called if event was already prevented + expect(api.close).not.toHaveBeenCalled(); + }); + + test('that close button is visible by default', () => { + const cut = new DefaultTab(); + + const api = fromPartial({ + onDidTitleChange: jest.fn(), + close: jest.fn(), + }); + const containerApi = fromPartial({}); + + cut.init({ + api, + containerApi, + params: {}, + title: 'title_abc', + }); + + let el = cut.element.querySelector('.dv-default-tab-action') as HTMLElement; + expect(el).toBeTruthy(); + expect(el.style.display).not.toBe('none'); + }); }); 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 2cd466626..4b4545cfd 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 @@ -894,4 +894,363 @@ describe('tabsContainer', () => { expect(mockVoidContainer.updateDragAndDropState).toHaveBeenCalledTimes(1); }); }); + + describe('tab overflow dropdown with close buttons', () => { + test('close button should be visible and clickable in dropdown tabs', () => { + const mockPopupService = { + openPopover: jest.fn(), + close: jest.fn(), + }; + + const accessor = fromPartial({ + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + options: {}, + onDidOptionsChange: jest.fn(), + popupService: mockPopupService, + }); + + const mockClose = jest.fn(); + const mockSetActive = jest.fn(); + const mockScrollIntoView = jest.fn(); + + const mockPanel = fromPartial({ + id: 'test-panel', + api: { + isActive: false, + close: mockClose, + setActive: mockSetActive, + }, + view: { + createTabRenderer: jest.fn().mockReturnValue({ + element: (() => { + const tabElement = document.createElement('div'); + tabElement.className = 'dv-default-tab'; + + const content = document.createElement('div'); + content.className = 'dv-default-tab-content'; + content.textContent = 'Test Tab'; + + const action = document.createElement('div'); + action.className = 'dv-default-tab-action'; + const closeButton = document.createElement('div'); + action.appendChild(closeButton); + + // Simulate close button functionality + action.addEventListener('click', (e) => { + e.preventDefault(); + mockClose(); + }); + + tabElement.appendChild(content); + tabElement.appendChild(action); + + return tabElement; + })(), + }), + }, + }); + + const mockTab = { + panel: mockPanel, + element: { + scrollIntoView: mockScrollIntoView, + }, + }; + + const mockTabs = { + tabs: [mockTab], + onDrop: jest.fn(), + onTabDragStart: jest.fn(), + onWillShowOverlay: jest.fn(), + onOverflowTabsChange: jest.fn(), + size: 1, + panels: ['test-panel'], + isActive: jest.fn(), + indexOf: jest.fn(), + delete: jest.fn(), + setActivePanel: jest.fn(), + openPanel: jest.fn(), + showTabsOverflowControl: true, + updateDragAndDropState: jest.fn(), + element: document.createElement('div'), + dispose: jest.fn(), + }; + + const groupPanel = fromPartial({ + id: 'testgroupid', + panels: [mockPanel], + model: fromPartial({}), + }); + + const cut = new TabsContainer(accessor, groupPanel); + (cut as any).tabs = mockTabs; + + // Simulate overflow tabs + (cut as any).toggleDropdown({ tabs: ['test-panel'], reset: false }); + + // Find the dropdown trigger and click it + const dropdownTrigger = cut.element.querySelector('.dv-tabs-overflow-dropdown-root'); + expect(dropdownTrigger).toBeTruthy(); + + // Simulate clicking the dropdown trigger + fireEvent.click(dropdownTrigger!); + + // Verify popup was opened + expect(mockPopupService.openPopover).toHaveBeenCalled(); + + // Get the popover content + const popoverContent = mockPopupService.openPopover.mock.calls[0][0]; + expect(popoverContent).toBeTruthy(); + + // Find the tab wrapper in the popover + const tabWrapper = popoverContent.querySelector('.dv-tab'); + expect(tabWrapper).toBeTruthy(); + + // Verify the close button is visible in dropdown + const closeButton = tabWrapper!.querySelector('.dv-default-tab-action') as HTMLElement; + expect(closeButton).toBeTruthy(); + expect(closeButton.style.display).not.toBe('none'); + + // Simulate clicking the close button + fireEvent.click(closeButton!); + + // Verify that the close method was called + expect(mockClose).toHaveBeenCalledTimes(1); + + // Verify that tab activation methods were NOT called when clicking close button + expect(mockScrollIntoView).not.toHaveBeenCalled(); + expect(mockSetActive).not.toHaveBeenCalled(); + }); + + test('clicking tab content (not close button) should activate tab', () => { + const mockPopupService = { + openPopover: jest.fn(), + close: jest.fn(), + }; + + const accessor = fromPartial({ + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + options: {}, + onDidOptionsChange: jest.fn(), + popupService: mockPopupService, + }); + + const mockClose = jest.fn(); + const mockSetActive = jest.fn(); + const mockScrollIntoView = jest.fn(); + + const mockPanel = fromPartial({ + id: 'test-panel', + api: { + isActive: false, + close: mockClose, + setActive: mockSetActive, + }, + view: { + createTabRenderer: jest.fn().mockReturnValue({ + element: (() => { + const tabElement = document.createElement('div'); + tabElement.className = 'dv-default-tab'; + + const content = document.createElement('div'); + content.className = 'dv-default-tab-content'; + content.textContent = 'Test Tab'; + + const action = document.createElement('div'); + action.className = 'dv-default-tab-action'; + const closeButton = document.createElement('div'); + action.appendChild(closeButton); + + // Simulate close button functionality + action.addEventListener('click', (e) => { + e.preventDefault(); + mockClose(); + }); + + tabElement.appendChild(content); + tabElement.appendChild(action); + + return tabElement; + })(), + }), + }, + }); + + const mockTab = { + panel: mockPanel, + element: { + scrollIntoView: mockScrollIntoView, + }, + }; + + const mockTabs = { + tabs: [mockTab], + onDrop: jest.fn(), + onTabDragStart: jest.fn(), + onWillShowOverlay: jest.fn(), + onOverflowTabsChange: jest.fn(), + size: 1, + panels: ['test-panel'], + isActive: jest.fn(), + indexOf: jest.fn(), + delete: jest.fn(), + setActivePanel: jest.fn(), + openPanel: jest.fn(), + showTabsOverflowControl: true, + updateDragAndDropState: jest.fn(), + element: document.createElement('div'), + dispose: jest.fn(), + }; + + const groupPanel = fromPartial({ + id: 'testgroupid', + panels: [mockPanel], + model: fromPartial({}), + }); + + const cut = new TabsContainer(accessor, groupPanel); + (cut as any).tabs = mockTabs; + + // Simulate overflow tabs + (cut as any).toggleDropdown({ tabs: ['test-panel'], reset: false }); + + // Find the dropdown trigger and click it + const dropdownTrigger = cut.element.querySelector('.dv-tabs-overflow-dropdown-root'); + expect(dropdownTrigger).toBeTruthy(); + + // Simulate clicking the dropdown trigger + fireEvent.click(dropdownTrigger!); + + // Get the popover content + const popoverContent = mockPopupService.openPopover.mock.calls[0][0]; + const tabWrapper = popoverContent.querySelector('.dv-tab'); + + // Simulate clicking the tab content (not the close button) + const tabContent = tabWrapper!.querySelector('.dv-default-tab-content'); + fireEvent.click(tabContent!); + + // Verify that tab activation methods were called + expect(mockPopupService.close).toHaveBeenCalled(); + expect(mockScrollIntoView).toHaveBeenCalled(); + expect(mockSetActive).toHaveBeenCalled(); + + // Verify that close was NOT called when clicking content + expect(mockClose).not.toHaveBeenCalled(); + }); + + test('click event should respect preventDefault in dropdown wrapper', () => { + const mockPopupService = { + openPopover: jest.fn(), + close: jest.fn(), + }; + + const accessor = fromPartial({ + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + options: {}, + onDidOptionsChange: jest.fn(), + popupService: mockPopupService, + }); + + const mockClose = jest.fn(); + const mockSetActive = jest.fn(); + const mockScrollIntoView = jest.fn(); + + const mockPanel = fromPartial({ + id: 'test-panel', + api: { + isActive: false, + close: mockClose, + setActive: mockSetActive, + }, + view: { + createTabRenderer: jest.fn().mockReturnValue({ + element: (() => { + const tabElement = document.createElement('div'); + tabElement.className = 'dv-default-tab'; + + const content = document.createElement('div'); + content.className = 'dv-default-tab-content'; + content.textContent = 'Test Tab'; + + const action = document.createElement('div'); + action.className = 'dv-default-tab-action'; + const closeButton = document.createElement('div'); + action.appendChild(closeButton); + + // Simulate close button functionality that prevents default + action.addEventListener('click', (e) => { + e.preventDefault(); + mockClose(); + }); + + tabElement.appendChild(content); + tabElement.appendChild(action); + + return tabElement; + })(), + }), + }, + }); + + const mockTab = { + panel: mockPanel, + element: { + scrollIntoView: mockScrollIntoView, + }, + }; + + const mockTabs = { + tabs: [mockTab], + onDrop: jest.fn(), + onTabDragStart: jest.fn(), + onWillShowOverlay: jest.fn(), + onOverflowTabsChange: jest.fn(), + size: 1, + panels: ['test-panel'], + isActive: jest.fn(), + indexOf: jest.fn(), + delete: jest.fn(), + setActivePanel: jest.fn(), + openPanel: jest.fn(), + showTabsOverflowControl: true, + updateDragAndDropState: jest.fn(), + element: document.createElement('div'), + dispose: jest.fn(), + }; + + const groupPanel = fromPartial({ + id: 'testgroupid', + panels: [mockPanel], + model: fromPartial({}), + }); + + const cut = new TabsContainer(accessor, groupPanel); + (cut as any).tabs = mockTabs; + + // Simulate overflow tabs + (cut as any).toggleDropdown({ tabs: ['test-panel'], reset: false }); + + // Find the dropdown trigger and click it + const dropdownTrigger = cut.element.querySelector('.dv-tabs-overflow-dropdown-root'); + fireEvent.click(dropdownTrigger!); + + // Get the popover content + const popoverContent = mockPopupService.openPopover.mock.calls[0][0]; + const tabWrapper = popoverContent.querySelector('.dv-tab'); + const closeButton = tabWrapper!.querySelector('.dv-default-tab-action'); + + // Simulate clicking the close button (which calls preventDefault) + fireEvent.click(closeButton!); + + // Verify close was called + expect(mockClose).toHaveBeenCalledTimes(1); + + // Verify that tab activation methods were NOT called due to preventDefault + expect(mockScrollIntoView).not.toHaveBeenCalled(); + expect(mockSetActive).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index b96b13dd8..2291141fc 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -379,8 +379,13 @@ export class TabsContainer !panelObject.api.isActive ); - wrapper.addEventListener('pointerdown', () => { + wrapper.addEventListener('click', (event) => { this.accessor.popupService.close(); + + if (event.defaultPrevented) { + return; + } + tab.element.scrollIntoView(); tab.panel.api.setActive(); }); diff --git a/packages/dockview/src/dockview/defaultTab.tsx b/packages/dockview/src/dockview/defaultTab.tsx index c2abed5e7..94a163cd5 100644 --- a/packages/dockview/src/dockview/defaultTab.tsx +++ b/packages/dockview/src/dockview/defaultTab.tsx @@ -97,7 +97,7 @@ export const DockviewDefaultTab: React.FunctionComponent< className="dv-default-tab" > {title} - {!hideClose && tabLocation !== 'headerOverflow' && ( + {!hideClose && (