Skip to content

Commit dcf34a7

Browse files
committed
fix(ContextMenu/DropdownMenu): wrap groups in a viewport
Resolves #3315
1 parent 2ba94db commit dcf34a7

8 files changed

+2210
-2060
lines changed

src/runtime/components/ContextMenuContent.vue

Lines changed: 58 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -109,68 +109,70 @@ const groups = computed<ContextMenuItem[][]>(() =>
109109
<component :is="sub ? ContextMenu.SubContent : ContextMenu.Content" :class="props.class" v-bind="contentProps">
110110
<slot name="content-top" />
111111

112-
<ContextMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
113-
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
114-
<ContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
115-
<ReuseItemTemplate :item="item" :index="index" />
116-
</ContextMenu.Label>
117-
<ContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
118-
<ContextMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
119-
<ContextMenu.SubTrigger
120-
as="button"
121-
type="button"
112+
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
113+
<ContextMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
114+
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
115+
<ContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
116+
<ReuseItemTemplate :item="item" :index="index" />
117+
</ContextMenu.Label>
118+
<ContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
119+
<ContextMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
120+
<ContextMenu.SubTrigger
121+
as="button"
122+
type="button"
123+
:disabled="item.disabled"
124+
:text-value="get(item, props.labelKey as string)"
125+
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
126+
>
127+
<ReuseItemTemplate :item="item" :index="index" />
128+
</ContextMenu.SubTrigger>
129+
130+
<UContextMenuContent
131+
sub
132+
:class="props.class"
133+
:ui="ui"
134+
:ui-override="uiOverride"
135+
:portal="portal"
136+
:items="(item.children as T)"
137+
:align-offset="-4"
138+
:label-key="labelKey"
139+
:checked-icon="checkedIcon"
140+
:loading-icon="loadingIcon"
141+
:external-icon="externalIcon"
142+
v-bind="item.content"
143+
>
144+
<template v-for="(_, name) in proxySlots" #[name]="slotData">
145+
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
146+
</template>
147+
</UContextMenuContent>
148+
</ContextMenu.Sub>
149+
<ContextMenu.CheckboxItem
150+
v-else-if="item.type === 'checkbox'"
151+
:model-value="item.checked"
122152
:disabled="item.disabled"
123153
:text-value="get(item, props.labelKey as string)"
124154
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
155+
@update:model-value="item.onUpdateChecked"
156+
@select="item.onSelect"
125157
>
126158
<ReuseItemTemplate :item="item" :index="index" />
127-
</ContextMenu.SubTrigger>
128-
129-
<UContextMenuContent
130-
sub
131-
:class="props.class"
132-
:ui="ui"
133-
:ui-override="uiOverride"
134-
:portal="portal"
135-
:items="(item.children as T)"
136-
:align-offset="-4"
137-
:label-key="labelKey"
138-
:checked-icon="checkedIcon"
139-
:loading-icon="loadingIcon"
140-
:external-icon="externalIcon"
141-
v-bind="item.content"
159+
</ContextMenu.CheckboxItem>
160+
<ContextMenu.Item
161+
v-else
162+
as-child
163+
:disabled="item.disabled"
164+
:text-value="get(item, props.labelKey as string)"
165+
@select="item.onSelect"
142166
>
143-
<template v-for="(_, name) in proxySlots" #[name]="slotData">
144-
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
145-
</template>
146-
</UContextMenuContent>
147-
</ContextMenu.Sub>
148-
<ContextMenu.CheckboxItem
149-
v-else-if="item.type === 'checkbox'"
150-
:model-value="item.checked"
151-
:disabled="item.disabled"
152-
:text-value="get(item, props.labelKey as string)"
153-
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
154-
@update:model-value="item.onUpdateChecked"
155-
@select="item.onSelect"
156-
>
157-
<ReuseItemTemplate :item="item" :index="index" />
158-
</ContextMenu.CheckboxItem>
159-
<ContextMenu.Item
160-
v-else
161-
as-child
162-
:disabled="item.disabled"
163-
:text-value="get(item, props.labelKey as string)"
164-
@select="item.onSelect"
165-
>
166-
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" custom>
167-
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], active, color: item?.color })">
168-
<ReuseItemTemplate :item="item" :active="active" :index="index" />
169-
</ULinkBase>
170-
</ULink>
171-
</ContextMenu.Item>
172-
</template>
173-
</ContextMenu.Group>
167+
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" custom>
168+
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], active, color: item?.color })">
169+
<ReuseItemTemplate :item="item" :active="active" :index="index" />
170+
</ULinkBase>
171+
</ULink>
172+
</ContextMenu.Item>
173+
</template>
174+
</ContextMenu.Group>
175+
</div>
174176

175177
<slot />
176178

src/runtime/components/DropdownMenuContent.vue

Lines changed: 60 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -115,70 +115,72 @@ const groups = computed<DropdownMenuItem[][]>(() =>
115115
<component :is="sub ? DropdownMenu.SubContent : DropdownMenu.Content" :class="props.class" v-bind="contentProps">
116116
<slot name="content-top" />
117117

118-
<DropdownMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
119-
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
120-
<DropdownMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
121-
<ReuseItemTemplate :item="item" :index="index" />
122-
</DropdownMenu.Label>
123-
<DropdownMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
124-
<DropdownMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
125-
<DropdownMenu.SubTrigger
126-
as="button"
127-
type="button"
118+
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
119+
<DropdownMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
120+
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
121+
<DropdownMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
122+
<ReuseItemTemplate :item="item" :index="index" />
123+
</DropdownMenu.Label>
124+
<DropdownMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
125+
<DropdownMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
126+
<DropdownMenu.SubTrigger
127+
as="button"
128+
type="button"
129+
:disabled="item.disabled"
130+
:text-value="get(item, props.labelKey as string)"
131+
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
132+
>
133+
<ReuseItemTemplate :item="item" :index="index" />
134+
</DropdownMenu.SubTrigger>
135+
136+
<UDropdownMenuContent
137+
sub
138+
:class="props.class"
139+
:ui="ui"
140+
:ui-override="uiOverride"
141+
:portal="portal"
142+
:items="(item.children as T)"
143+
align="start"
144+
:align-offset="-4"
145+
:side-offset="3"
146+
:label-key="labelKey"
147+
:checked-icon="checkedIcon"
148+
:loading-icon="loadingIcon"
149+
:external-icon="externalIcon"
150+
v-bind="item.content"
151+
>
152+
<template v-for="(_, name) in proxySlots" #[name]="slotData">
153+
<slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotData" />
154+
</template>
155+
</UDropdownMenuContent>
156+
</DropdownMenu.Sub>
157+
<DropdownMenu.CheckboxItem
158+
v-else-if="item.type === 'checkbox'"
159+
:model-value="item.checked"
128160
:disabled="item.disabled"
129161
:text-value="get(item, props.labelKey as string)"
130162
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
163+
@update:model-value="item.onUpdateChecked"
164+
@select="item.onSelect"
131165
>
132166
<ReuseItemTemplate :item="item" :index="index" />
133-
</DropdownMenu.SubTrigger>
134-
135-
<UDropdownMenuContent
136-
sub
137-
:class="props.class"
138-
:ui="ui"
139-
:ui-override="uiOverride"
140-
:portal="portal"
141-
:items="(item.children as T)"
142-
align="start"
143-
:align-offset="-4"
144-
:side-offset="3"
145-
:label-key="labelKey"
146-
:checked-icon="checkedIcon"
147-
:loading-icon="loadingIcon"
148-
:external-icon="externalIcon"
149-
v-bind="item.content"
167+
</DropdownMenu.CheckboxItem>
168+
<DropdownMenu.Item
169+
v-else
170+
as-child
171+
:disabled="item.disabled"
172+
:text-value="get(item, props.labelKey as string)"
173+
@select="item.onSelect"
150174
>
151-
<template v-for="(_, name) in proxySlots" #[name]="slotData">
152-
<slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotData" />
153-
</template>
154-
</UDropdownMenuContent>
155-
</DropdownMenu.Sub>
156-
<DropdownMenu.CheckboxItem
157-
v-else-if="item.type === 'checkbox'"
158-
:model-value="item.checked"
159-
:disabled="item.disabled"
160-
:text-value="get(item, props.labelKey as string)"
161-
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
162-
@update:model-value="item.onUpdateChecked"
163-
@select="item.onSelect"
164-
>
165-
<ReuseItemTemplate :item="item" :index="index" />
166-
</DropdownMenu.CheckboxItem>
167-
<DropdownMenu.Item
168-
v-else
169-
as-child
170-
:disabled="item.disabled"
171-
:text-value="get(item, props.labelKey as string)"
172-
@select="item.onSelect"
173-
>
174-
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
175-
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color, active })">
176-
<ReuseItemTemplate :item="item" :active="active" :index="index" />
177-
</ULinkBase>
178-
</ULink>
179-
</DropdownMenu.Item>
180-
</template>
181-
</DropdownMenu.Group>
175+
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
176+
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color, active })">
177+
<ReuseItemTemplate :item="item" :active="active" :index="index" />
178+
</ULinkBase>
179+
</ULink>
180+
</DropdownMenu.Item>
181+
</template>
182+
</DropdownMenu.Group>
183+
</div>
182184

183185
<slot />
184186

src/theme/context-menu.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { ModuleOptions } from '../module'
22

33
export default (options: Required<ModuleOptions>) => ({
44
slots: {
5-
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default divide-y divide-default overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-context-menu-content-transform-origin)',
5+
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-context-menu-content-transform-origin) flex flex-col',
6+
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
67
group: 'p-1 isolate',
78
label: 'w-full flex items-center font-semibold text-highlighted',
89
separator: '-mx-1 my-1 h-px bg-border',

src/theme/dropdown-menu.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { ModuleOptions } from '../module'
22

33
export default (options: Required<ModuleOptions>) => ({
44
slots: {
5-
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default divide-y divide-default overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin)',
5+
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
6+
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
67
arrow: 'fill-default',
78
group: 'p-1 isolate',
89
label: 'w-full flex items-center font-semibold text-highlighted',

0 commit comments

Comments
 (0)