Skip to content

Commit cd36fc7

Browse files
antfuclaude
andauthored
feat: move popup to context menu (#207)
Co-authored-by: Claude Haiku 4.5 <[email protected]>
1 parent bfd8da3 commit cd36fc7

File tree

10 files changed

+34
-87
lines changed

10 files changed

+34
-87
lines changed

packages/core/src/client/webcomponents/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/core/src/client/webcomponents/components/Dock.vue

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useEventListener, useScreenSafeArea } from '@vueuse/core'
44
import { computed, onMounted, reactive, ref, useTemplateRef, watchEffect } from 'vue'
55
import { BUILTIN_ENTRY_CLIENT_AUTH_NOTICE } from '../constants'
66
import { docksSplitGroupsWithCapacity } from '../state/dock-settings'
7-
import { filterPopupDockEntry, isDockPopupEntryVisible } from '../state/popup'
87
import DockEntriesWithCategories from './DockEntriesWithCategories.vue'
98
import DockOverflowButton from './DockOverflowButton.vue'
109
import BracketLeft from './icons/BracketLeft.vue'
@@ -75,12 +74,7 @@ context.rpc.events.on('rpc:is-trusted:updated', (isTrusted) => {
7574
context.docks.switchEntry(null)
7675
})
7776
78-
const isPopupEntryVisible = computed(() => isDockPopupEntryVisible(context.clientType))
79-
const groupedEntries = computed(() => {
80-
if (isPopupEntryVisible.value)
81-
return context.docks.groupedEntries
82-
return filterPopupDockEntry(context.docks.groupedEntries)
83-
})
77+
const groupedEntries = computed(() => context.docks.groupedEntries)
8478
8579
const splitEntries = computed(() => {
8680
return docksSplitGroupsWithCapacity(groupedEntries.value, 5)

packages/core/src/client/webcomponents/components/DockContextMenu.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { DevToolsDockEntry } from '@vitejs/devtools-kit'
22
import type { DocksContext } from '@vitejs/devtools-kit/client'
33
import { h } from 'vue'
44
import { setDockContextMenu } from '../state/floating-tooltip'
5+
import { isDockPopupSupported, requestDockPopupOpen, useIsDockPopupOpen } from '../state/popup'
56

67
// @unocss-include
78

@@ -77,6 +78,15 @@ export function openDockContextMenu(options: {
7778
action: () => refreshDock(context, entry),
7879
visible: canRefresh(entry),
7980
},
81+
{
82+
label: 'Popup',
83+
icon: 'i-ph-arrow-square-out-duotone',
84+
action: () => {
85+
setDockContextMenu(null)
86+
requestDockPopupOpen(context)
87+
},
88+
visible: isDockPopupSupported() && !useIsDockPopupOpen().value && context.clientType === 'embedded',
89+
},
8090
].filter(item => item.visible)
8191

8292
if (items.length === 0)

packages/core/src/client/webcomponents/components/DockStandalone.vue

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script setup lang="ts">
22
import type { DocksContext } from '@vitejs/devtools-kit/client'
33
import { computed, markRaw, ref, useTemplateRef, watch } from 'vue'
4-
import { filterPopupDockEntry, isDockPopupEntryVisible } from '../state/popup'
54
import { PersistedDomViewsManager } from '../utils/PersistedDomViewsManager'
65
import DockEntriesWithCategories from './DockEntriesWithCategories.vue'
76
import FloatingElements from './FloatingElements.vue'
@@ -31,12 +30,7 @@ watch(
3130
{ immediate: true },
3231
)
3332
34-
const groupedEntries = computed(() => {
35-
if (isDockPopupEntryVisible('standalone'))
36-
return context.docks.groupedEntries
37-
38-
return filterPopupDockEntry(context.docks.groupedEntries)
39-
})
33+
const groupedEntries = computed(() => context.docks.groupedEntries)
4034
4135
function switchEntry(id: string | undefined) {
4236
if (id) {

packages/core/src/client/webcomponents/components/FloatingElements.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ useEventListener(window, 'keydown', (e: KeyboardEvent) => {
1616
</script>
1717

1818
<template>
19-
<FloatingPopover :item="dockContextMenu" @dismiss="() => setDockContextMenu(null)" />
2019
<FloatingPopover :item="docksOverflowPanel" @dismiss="() => setDocksOverflowPanel(null)" />
20+
<FloatingPopover :item="dockContextMenu" @dismiss="() => setDockContextMenu(null)" />
2121
<FloatingPopover :item="tooltip" />
2222
</template>

packages/core/src/client/webcomponents/state/__tests__/popup.test.ts

Lines changed: 12 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { DocksContext } from '@vitejs/devtools-kit/client'
22
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
33
import { setDocksOverflowPanel, useDocksOverflowPanel } from '../floating-tooltip'
4-
import { closeDockPopup, filterPopupDockEntry, isDockPopupEntryVisible, isDockPopupSupported, openDockPopup, setDockStandaloneLoaderForTest, useDockPopupWindow, useIsDockPopupOpen } from '../popup'
4+
import { closeDockPopup, isDockPopupSupported, openDockPopup, setDockStandaloneLoaderForTest, useDockPopupWindow, useIsDockPopupOpen } from '../popup'
55

66
const {
77
DockStandaloneElementMock,
@@ -49,6 +49,9 @@ function createMockContext(
4949
inactiveTimeout: 3_000,
5050
},
5151
},
52+
docks: {
53+
switchEntry: vi.fn(),
54+
},
5255
} as unknown as DocksContext
5356
}
5457

@@ -145,49 +148,11 @@ describe('dock popup state', () => {
145148

146149
it('returns null when the API is unavailable', async () => {
147150
expect(isDockPopupSupported()).toBe(false)
148-
expect(isDockPopupEntryVisible('embedded')).toBe(false)
149151
const popup = await openDockPopup(createMockContext())
150152
expect(popup).toBeNull()
151153
expect(useIsDockPopupOpen().value).toBe(false)
152154
})
153155

154-
it('hides popup entry when popup is already open', async () => {
155-
const popup = createMockPopupWindow()
156-
const requestWindow = vi.fn().mockResolvedValue(popup)
157-
;(window as Window & { documentPictureInPicture?: unknown }).documentPictureInPicture = { requestWindow }
158-
159-
expect(isDockPopupEntryVisible('embedded')).toBe(true)
160-
await openDockPopup(createMockContext())
161-
expect(isDockPopupEntryVisible('embedded')).toBe(false)
162-
})
163-
164-
it('hides popup entry when running inside popup window', () => {
165-
;(window as Window & { documentPictureInPicture?: unknown }).documentPictureInPicture = { requestWindow: vi.fn() }
166-
167-
expect(isDockPopupEntryVisible('standalone')).toBe(false)
168-
})
169-
170-
it('filters popup entry from grouped entries', () => {
171-
const groups = [
172-
['~builtin', [
173-
{ type: '~builtin', id: '~popup', title: 'Popup', icon: 'test' },
174-
{ type: '~builtin', id: '~settings', title: 'Settings', icon: 'test' },
175-
]],
176-
['default', [
177-
{ type: 'iframe', id: 'app', title: 'App', icon: 'test', url: 'http://example.com' },
178-
]],
179-
] as const as Parameters<typeof filterPopupDockEntry>[0]
180-
181-
expect(filterPopupDockEntry(groups)).toEqual([
182-
['~builtin', [
183-
{ type: '~builtin', id: '~settings', title: 'Settings', icon: 'test' },
184-
]],
185-
['default', [
186-
{ type: 'iframe', id: 'app', title: 'App', icon: 'test', url: 'http://example.com' },
187-
]],
188-
])
189-
})
190-
191156
it('opens popup window and mounts standalone frame', async () => {
192157
const popup = createMockPopupWindow()
193158
const requestWindow = vi.fn().mockResolvedValue(popup)
@@ -240,32 +205,36 @@ describe('dock popup state', () => {
240205
expect(popup.focus).toHaveBeenCalledTimes(1)
241206
})
242207

243-
it('clears popup state when popup is closed', async () => {
208+
it('clears popup state and closes panel when popup is closed', async () => {
244209
const popup = createMockPopupWindow()
245210
const requestWindow = vi.fn().mockResolvedValue(popup)
246211
;(window as Window & { documentPictureInPicture?: unknown }).documentPictureInPicture = { requestWindow }
247212

248-
await openDockPopup(createMockContext())
213+
const context = createMockContext()
214+
await openDockPopup(context)
249215
expect(useIsDockPopupOpen().value).toBe(true)
250216

251217
popup.dispatchEvent(new Event('pagehide'))
252218

253219
expect(dockElementRemoveMock).toHaveBeenCalledTimes(1)
254220
expect(useIsDockPopupOpen().value).toBe(false)
255221
expect(useDockPopupWindow().value).toBeNull()
222+
expect(context.docks.switchEntry).toHaveBeenCalledWith(null)
256223
})
257224

258-
it('closes popup window via closeDockPopup', async () => {
225+
it('closes popup window and panel via closeDockPopup', async () => {
259226
const popup = createMockPopupWindow()
260227
const requestWindow = vi.fn().mockResolvedValue(popup)
261228
;(window as Window & { documentPictureInPicture?: unknown }).documentPictureInPicture = { requestWindow }
262229

263-
await openDockPopup(createMockContext())
230+
const context = createMockContext()
231+
await openDockPopup(context)
264232
closeDockPopup()
265233

266234
expect(dockElementRemoveMock).toHaveBeenCalledTimes(1)
267235
expect(popup.close).toHaveBeenCalledTimes(1)
268236
expect(useIsDockPopupOpen().value).toBe(false)
269237
expect(useDockPopupWindow().value).toBeNull()
238+
expect(context.docks.switchEntry).toHaveBeenCalledWith(null)
270239
})
271240
})

packages/core/src/client/webcomponents/state/context.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { BUILTIN_ENTRIES } from '../constants'
88
import { docksGroupByCategories } from './dock-settings'
99
import { createDockEntryState, DEFAULT_DOCK_PANEL_STORE, sharedStateToRef, useDocksEntries } from './docks'
1010
import { createClientLogsClient } from './logs-client'
11-
import { registerMainFrameDockActionHandler, requestDockPopupOpen, triggerMainFrameDockAction } from './popup'
11+
import { registerMainFrameDockActionHandler, triggerMainFrameDockAction } from './popup'
1212
import { executeSetupScript } from './setup-script'
1313

1414
const docksContextByRpc = new WeakMap<DevToolsRpcClient, DocksContext>()
@@ -56,10 +56,6 @@ export async function createDocksContext(
5656
panelStore.value.open = true
5757
return true
5858
}
59-
if (id === '~popup') {
60-
requestDockPopupOpen(docksContext)
61-
return true
62-
}
6359
const entry = dockEntries.value.find(e => e.id === id)
6460
if (!entry)
6561
return false

packages/core/src/client/webcomponents/state/popup.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { DevToolsDockEntriesGrouped, DevToolsDockEntry } from '@vitejs/devtools-kit'
21
import type { DocksContext } from '@vitejs/devtools-kit/client'
32
import { createEventEmitter } from '@vitejs/devtools-kit/utils/events'
43
import { shallowRef } from 'vue'
@@ -18,7 +17,6 @@ const PANEL_MIN_SIZE = 20
1817
const PANEL_MAX_SIZE = 100
1918
const POPUP_MIN_WIDTH = 320
2019
const POPUP_MIN_HEIGHT = 240
21-
const POPUP_DOCK_ID = '~popup'
2220
const MAIN_FRAME_ACTION_HANDLER_KEY = '__VITE_DEVTOOLS_TRIGGER_DOCK_ACTION__'
2321

2422
const popupWindow = shallowRef<Window | null>(null)
@@ -27,6 +25,7 @@ const popupEvents = createEventEmitter<DockPopupEvents>()
2725
let detachPopupListeners: (() => void) | undefined
2826
let detachColorModeSync: (() => void) | undefined
2927
let popupDockElement: (HTMLElement & { remove: () => void }) | undefined
28+
let popupContext: DocksContext | undefined
3029
let loadDockStandalone: () => Promise<new (props: { context: DocksContext }) => HTMLElement> = async () => {
3130
return await import('../components/DockStandalone').then(m => m.DockStandalone)
3231
}
@@ -136,6 +135,9 @@ function clearPopupState() {
136135
unmountPopupElement()
137136
popupWindow.value = null
138137
isPopupOpen.value = false
138+
const ctx = popupContext
139+
popupContext = undefined
140+
ctx?.docks.switchEntry(null)
139141
}
140142

141143
function clamp(value: number, min: number, max: number) {
@@ -226,18 +228,6 @@ export async function triggerMainFrameDockAction(
226228
}
227229
}
228230

229-
export function isDockPopupEntryVisible(clientType: 'embedded' | 'standalone'): boolean {
230-
return isDockPopupSupported() && !isPopupOpen.value && clientType !== 'standalone'
231-
}
232-
233-
export function filterPopupDockEntry(
234-
groups: DevToolsDockEntriesGrouped,
235-
): DevToolsDockEntriesGrouped {
236-
return groups
237-
.map(([category, entries]) => [category, entries.filter(entry => entry.id !== POPUP_DOCK_ID)] as [string, DevToolsDockEntry[]])
238-
.filter(([, entries]) => entries.length > 0)
239-
}
240-
241231
export function useDockPopupWindow() {
242232
return popupWindow as Readonly<typeof popupWindow>
243233
}
@@ -304,6 +294,7 @@ export async function openDockPopup(context: DocksContext): Promise<Window | nul
304294
popup.removeEventListener('pagehide', onPageHide)
305295
}
306296

297+
popupContext = context
307298
popupWindow.value = popup
308299
isPopupOpen.value = true
309300
return popup

packages/core/src/node/__tests__/host-docks.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ describe('devToolsDockHost', () => {
66
const mockContext = {} as DevToolsNodeContext
77

88
describe('builtin entries', () => {
9-
it('includes popup in builtin docks', () => {
9+
it('does not include popup in builtin docks', () => {
1010
const host = new DevToolsDockHost(mockContext)
1111
const builtinEntries = host.values().filter(entry => entry.type === '~builtin')
1212
const builtinIds = builtinEntries.map(entry => entry.id)
1313

14-
expect(builtinIds).toContain('~popup')
14+
expect(builtinIds).not.toContain('~popup')
1515
})
1616
})
1717

packages/core/src/node/host-docks.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,6 @@ export class DevToolsDockHost implements DevToolsDockHostType {
5353
return size > 0 ? String(size) : undefined
5454
},
5555
},
56-
{
57-
type: '~builtin',
58-
id: '~popup',
59-
title: 'Popup',
60-
category: '~builtin',
61-
icon: 'ph:arrow-square-out-duotone',
62-
},
6356
{
6457
type: '~builtin',
6558
id: '~settings',

0 commit comments

Comments
 (0)