Skip to content

Commit a93cfa7

Browse files
authored
fix(core): keep a group's defaultChildId reachable regardless of visibility, respecting when (#472)
* fix(core): keep a group's defaultChildId reachable when its target is visibility-hidden DockGroupButton resolved defaultChildId against the visibility-filtered member list, so a target with visibility: 'false' was never found and the click fell through to the (empty) popover instead of jumping to it directly. Resolve against the unfiltered member list instead, matching switchEntry's own group-to-member resolution and the documented render-only visibility contract. DockEntries' empty-group check is updated the same way so the group button doesn't disappear when its only member is a hidden default child. * fix(core): honor when, ignore visibility, when resolving a group's defaultChildId resolveGroupDefaultChild (new, in dock-settings.ts) centralizes the group->member resolution used by DockGroupButton's click handler, DockEntries' empty-group check, and switchEntry's own group activation: - Ignores the target's render-only `visibility` clause entirely, since visibility never affects reachability (docs/kit/when-clauses.md). - Still checks the target's `when` clause, so a conditionally-unavailable member (e.g. when: 'clientType == embedded' evaluating false) is never treated as a valid defaultChildId target. DockGroupButton.vue and DockEntries.vue now call the shared helper instead of re-deriving membership themselves. switchEntry's group resolution (state/context.ts) needed settings/getWhenContext hoisted above its declaration to evaluate `when`; those are now established immediately before switchEntry instead of much later in the file. Replaces the getGroupMembers-based defaultChildId resolution landed in the previous commit, which ignored when clauses along with visibility.
1 parent c14406b commit a93cfa7

5 files changed

Lines changed: 154 additions & 37 deletions

File tree

packages/core/src/client/webcomponents/components/dock/DockEntries.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { DevToolsDockEntry } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import { evaluateWhen } from 'devframe/utils/when'
55
import { toRefs } from 'vue'
6-
import { getGroupMembers } from '../../state/dock-settings'
6+
import { getGroupMembers, resolveGroupDefaultChild } from '../../state/dock-settings'
77
import { sharedStateToRef } from '../../state/docks'
88
import DockEntry from './DockEntry.vue'
99
import DockGroupButton from './DockGroupButton.vue'
@@ -25,9 +25,15 @@ const settings = sharedStateToRef(props.context.docks.settings)
2525
2626
function isDockVisible(dock: DevToolsDockEntry): boolean {
2727
// Hide empty groups — a group button with no members has nothing to reveal.
28+
// A `defaultChildId` still counts as "something to reveal" even when its
29+
// target is render-only hidden via `visibility`: clicking the group button
30+
// jumps straight to that member instead of opening the (visibly empty)
31+
// popover, so the button must stay reachable. Only treat the group as empty
32+
// when no member is visible AND no reachable `defaultChildId` target exists
33+
// (a target hidden by its own `when` clause doesn't count as reachable).
2834
if (dock.type === 'group') {
2935
const members = getGroupMembers(props.context.docks.entries, dock.id, settings.value, { whenContext: props.context.when.context })
30-
if (members.length === 0)
36+
if (members.length === 0 && !resolveGroupDefaultChild(props.context.docks.entries, dock.id, dock.defaultChildId, props.context.when.context))
3137
return false
3238
}
3339
if (dock.when && !evaluateWhen(dock.when, props.context.when.context))

packages/core/src/client/webcomponents/components/dock/DockGroupButton.vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { DevToolsDockEntry, DevToolsViewGroup } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import { watchDebounced } from '@vueuse/core'
55
import { computed, h, ref, useTemplateRef } from 'vue'
6-
import { getGroupMembers, getGroupMembersGrouped } from '../../state/dock-settings'
6+
import { getGroupMembers, getGroupMembersGrouped, resolveGroupDefaultChild } from '../../state/dock-settings'
77
import { sharedStateToRef } from '../../state/docks'
88
import { setDocksGroupPanel, useDocksGroupPanel } from '../../state/floating-tooltip'
99
import DockEntry from './DockEntry.vue'
@@ -98,8 +98,14 @@ function onClick() {
9898
return
9999
}
100100
// `defaultChildId` opens its member directly; otherwise reveal the popover.
101-
const fallback = props.group.defaultChildId
102-
&& members.value.find(m => m.id === props.group.defaultChildId)
101+
// Resolved regardless of the target's render-only `visibility` (a hidden
102+
// button must still fire), but honoring its `when` clause.
103+
const fallback = resolveGroupDefaultChild(
104+
props.context.docks.entries,
105+
props.group.id,
106+
props.group.defaultChildId,
107+
props.context.when.context,
108+
)
103109
if (fallback) {
104110
hidePanel()
105111
emit('select', fallback)

packages/core/src/client/webcomponents/state/__tests__/dock-groups.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
PINNED_CATEGORY,
1313
PINNED_CATEGORY_ORDER,
1414
resolveCommandIcon,
15+
resolveGroupDefaultChild,
1516
} from '../dock-settings'
1617

1718
function iframe(id: string, extra: Partial<DevToolsDockEntry> = {}): DevToolsDockEntry {
@@ -139,6 +140,69 @@ describe('render-only `visibility` (subTabs anchor use case)', () => {
139140
})
140141
})
141142

143+
describe('resolveGroupDefaultChild (group→member resolution ignores `visibility`, respects `when`)', () => {
144+
it('resolves a plain, fully-visible default child', () => {
145+
const entries: DevToolsDockEntry[] = [
146+
group('g', { defaultChildId: 'g:a' } as any),
147+
iframe('g:a', { groupId: 'g' }),
148+
iframe('g:b', { groupId: 'g' }),
149+
]
150+
expect(resolveGroupDefaultChild(entries, 'g', 'g:a')?.id).toBe('g:a')
151+
})
152+
153+
it('resolves the target even when it is render-only hidden via `visibility`', () => {
154+
// The core bug fixed here: a defaultChildId target with `visibility: 'false'`
155+
// must still fire — visibility only hides the target's own dock-bar button.
156+
const entries: DevToolsDockEntry[] = [
157+
group('g', { defaultChildId: 'g:hidden' } as any),
158+
iframe('g:hidden', { groupId: 'g', visibility: 'false' } as any),
159+
]
160+
expect(resolveGroupDefaultChild(entries, 'g', 'g:hidden')?.id).toBe('g:hidden')
161+
})
162+
163+
it('does NOT resolve the target when its own `when` clause evaluates false', () => {
164+
const entries: DevToolsDockEntry[] = [
165+
group('g', { defaultChildId: 'g:embedded-only' } as any),
166+
iframe('g:embedded-only', { groupId: 'g', when: 'clientType == embedded' } as any),
167+
]
168+
expect(resolveGroupDefaultChild(entries, 'g', 'g:embedded-only', { clientType: 'standalone' } as any)).toBeUndefined()
169+
expect(resolveGroupDefaultChild(entries, 'g', 'g:embedded-only', { clientType: 'embedded' } as any)?.id).toBe('g:embedded-only')
170+
})
171+
172+
it('does NOT resolve an unconditionally `when: false` target with no whenContext', () => {
173+
const entries: DevToolsDockEntry[] = [
174+
group('g', { defaultChildId: 'g:off' } as any),
175+
iframe('g:off', { groupId: 'g', when: 'false' } as any),
176+
]
177+
expect(resolveGroupDefaultChild(entries, 'g', 'g:off')).toBeUndefined()
178+
})
179+
180+
it('resolves the target even when `when`-hidden AND `visibility`-hidden together, as long as `when` passes', () => {
181+
const entries: DevToolsDockEntry[] = [
182+
group('g', { defaultChildId: 'g:both' } as any),
183+
iframe('g:both', { groupId: 'g', when: 'clientType == embedded', visibility: 'false' } as any),
184+
]
185+
expect(resolveGroupDefaultChild(entries, 'g', 'g:both', { clientType: 'embedded' } as any)?.id).toBe('g:both')
186+
})
187+
188+
it('returns undefined when there is no defaultChildId', () => {
189+
const entries: DevToolsDockEntry[] = [
190+
group('g'),
191+
iframe('g:a', { groupId: 'g' }),
192+
]
193+
expect(resolveGroupDefaultChild(entries, 'g', undefined)).toBeUndefined()
194+
})
195+
196+
it('returns undefined when defaultChildId does not reference a member of this group', () => {
197+
const entries: DevToolsDockEntry[] = [
198+
group('g', { defaultChildId: 'other:x' } as any),
199+
iframe('g:a', { groupId: 'g' }),
200+
iframe('other:x', { groupId: 'other-group' }),
201+
]
202+
expect(resolveGroupDefaultChild(entries, 'g', 'other:x')).toBeUndefined()
203+
})
204+
})
205+
142206
describe('in-group sub-categories (dual role of `category`)', () => {
143207
// The group carries category 'framework' (the OUTER bucket for the whole
144208
// group); its members carry their own categories, which act as IN-GROUP

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

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { computed, markRaw, reactive, ref, toRefs, watch, watchEffect } from 'vu
1010
import { DEVTOOLS_HIDE_EVENT, DEVTOOLS_MODE_FILENAME } from '../../../constants'
1111
import { BUILTIN_ENTRIES } from '../constants'
1212
import { createCommandsContext } from './commands'
13-
import { docksGroupByCategories, getCategoryLabel, getGroupMembers, getGroupMembersGrouped, getRegisteredGroupIds, resolveCommandIcon } from './dock-settings'
13+
import { docksGroupByCategories, getCategoryLabel, getGroupMembers, getGroupMembersGrouped, getRegisteredGroupIds, resolveCommandIcon, resolveGroupDefaultChild } from './dock-settings'
1414
import { createDockEntryState, DEFAULT_DOCK_PANEL_STORE, sharedStateToRef, useDocksEntries } from './docks'
1515
import { createClientMessagesClient } from './messages-client'
1616
import { registerMainFrameDockActionHandler, triggerMainFrameDockAction } from './popup'
@@ -113,6 +113,31 @@ export async function createDocksContext(
113113
panelStore ||= ref(DEFAULT_DOCK_PANEL_STORE())
114114
let docksContext: DocksContext
115115

116+
let _settingsStorePromise: Promise<SharedState<DevToolsDocksUserSettings>> | undefined
117+
const getSettingsStore = async () => {
118+
if (!_settingsStorePromise) {
119+
_settingsStorePromise = rpc.sharedState.get(
120+
'devframe:user-settings',
121+
{ initialValue: DEFAULT_STATE_USER_SETTINGS() },
122+
)
123+
}
124+
return _settingsStorePromise
125+
}
126+
127+
// Get settings store ahead of `switchEntry` — its group→member resolution
128+
// needs `getWhenContext` to honor a `defaultChildId` target's `when` clause.
129+
const settingsStore = markRaw(await getSettingsStore())
130+
const settings = sharedStateToRef(settingsStore)
131+
132+
// Shared when-context provider — used by both commands and docks
133+
let commandsContext: CommandsContext
134+
const getWhenContext = (): WhenContext => ({
135+
clientType,
136+
dockOpen: panelStore.value.open,
137+
paletteOpen: commandsContext?.paletteOpen ?? false,
138+
dockSelectedId: selectedId.value ?? '',
139+
})
140+
116141
const switchEntry = async (id: string | null = null) => {
117142
if (id == null) {
118143
selectedId.value = null
@@ -129,14 +154,14 @@ export async function createDocksContext(
129154
return false
130155

131156
// A group has no view of its own — resolve to the member it represents.
132-
// Prefer the author's `defaultChildId`, otherwise the first visible member.
133-
// With neither, the group is popover-only and selecting it is a no-op here
134-
// (the dock-bar group button opens the member popover instead).
157+
// Prefer the author's `defaultChildId` (honoring its `when` clause but
158+
// ignoring its render-only `visibility` — see `resolveGroupDefaultChild`),
159+
// otherwise the first member. With neither, the group is popover-only and
160+
// selecting it is a no-op here (the dock-bar group button opens the
161+
// member popover instead).
135162
if (entry.type === 'group') {
136-
const members = getGroupMembers(entries.value, entry.id)
137-
const target = (entry.defaultChildId && members.some(m => m.id === entry.defaultChildId))
138-
? entry.defaultChildId
139-
: members[0]?.id
163+
const target = resolveGroupDefaultChild(entries.value, entry.id, entry.defaultChildId, getWhenContext())?.id
164+
?? getGroupMembers(entries.value, entry.id)[0]?.id
140165
if (!target)
141166
return false
142167
return switchEntry(target)
@@ -258,30 +283,8 @@ export async function createDocksContext(
258283
},
259284
})
260285

261-
let _settingsStorePromise: Promise<SharedState<DevToolsDocksUserSettings>> | undefined
262-
const getSettingsStore = async () => {
263-
if (!_settingsStorePromise) {
264-
_settingsStorePromise = rpc.sharedState.get(
265-
'devframe:user-settings',
266-
{ initialValue: DEFAULT_STATE_USER_SETTINGS() },
267-
)
268-
}
269-
return _settingsStorePromise
270-
}
271-
272-
// Get settings store and create computed grouped entries
273-
const settingsStore = markRaw(await getSettingsStore())
274-
const settings = sharedStateToRef(settingsStore)
275-
276-
// Shared when-context provider — used by both commands and docks
277-
let commandsContext: CommandsContext
278-
const getWhenContext = (): WhenContext => ({
279-
clientType,
280-
dockOpen: panelStore.value.open,
281-
paletteOpen: commandsContext?.paletteOpen ?? false,
282-
dockSelectedId: selectedId.value ?? '',
283-
})
284-
286+
// Settings store, `settings`, and `getWhenContext` are established earlier
287+
// (right before `switchEntry`) — its group→member resolution needs them.
285288
const groupedEntries = computed(() => {
286289
return docksGroupByCategories(entries.value, settings.value, { whenContext: getWhenContext(), collapseGroups: true })
287290
})

packages/core/src/client/webcomponents/state/dock-settings.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,44 @@ export function getGroupMembers(
171171
return getGroupMembersGrouped(entries, groupId, settings, options).flatMap(([, items]) => items)
172172
}
173173

174+
/**
175+
* Resolve a group's `defaultChildId` to its target member, for the "clicking
176+
* the group button jumps straight to this member" behavior (the dock-bar
177+
* button and `switchEntry`'s own group→member resolution both need this).
178+
*
179+
* Respects the member's own `when` clause — a conditionally-unavailable
180+
* target (e.g. `when: 'clientType == embedded'` evaluating false) is not a
181+
* valid default and this returns `undefined` so the caller falls back to its
182+
* own behavior (open the popover, or pick another member). Deliberately
183+
* ignores the render-only `visibility` clause: `visibility` never affects
184+
* reachability (see {@link docksGroupByCategories}'s `visibility` check), and
185+
* jumping straight to a `defaultChildId` target is exactly the kind of
186+
* id-based activation the render-only contract says stays unaffected — only
187+
* the target's own dock-bar button (if it has one) should disappear.
188+
*/
189+
export function resolveGroupDefaultChild(
190+
entries: DevToolsDockEntry[],
191+
groupId: string,
192+
defaultChildId: string | undefined,
193+
whenContext?: WhenContext,
194+
): DevToolsDockEntry | undefined {
195+
if (!defaultChildId)
196+
return undefined
197+
const member = entries.find(e => e.type !== 'group' && e.groupId === groupId && e.id === defaultChildId)
198+
if (!member)
199+
return undefined
200+
if (member.when) {
201+
if (whenContext) {
202+
if (!evaluateWhen(member.when, whenContext))
203+
return undefined
204+
}
205+
else if (member.when === 'false') {
206+
return undefined
207+
}
208+
}
209+
return member
210+
}
211+
174212
/**
175213
* Group and sort dock entries based on user settings.
176214
* Filters out hidden entries and categories, then sorts by custom order and

0 commit comments

Comments
 (0)