Browse Source

feat(@vben-core/popup-ui): add extra header slot to modal (#8310)

Allow custom content to be rendered in the modal header, positioned to the
left of the fullscreen toggle button, via a new `extra` slot. Mirrors the
existing `extra` slot in the drawer component.

Co-authored-by: EvoX <evox@evomap.ai>
pull/8317/head
俞可 · YU KE 1 month ago
committed by GitHub
parent
commit
0b8750eafb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/gold-donkeys-modal-extra.md
  2. 37
      packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal.test.ts
  3. 20
      packages/@core/ui-kit/popup-ui/src/modal/modal.vue

5
.changeset/gold-donkeys-modal-extra.md

@ -0,0 +1,5 @@
---
'@vben-core/popup-ui': minor
---
feat(@vben-core/popup-ui): support custom content in modal header via extra slot

37
packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal.test.ts

@ -106,6 +106,43 @@ describe('vben modal', () => {
document.dispatchEvent(new MouseEvent('mouseup'));
});
it('renders custom content in the extra header slot', async () => {
const mainContent = document.createElement('main');
mainContent.id = ELEMENT_ID_MAIN_CONTENT;
mainContent.innerHTML = '<div><div></div></div>';
document.body.append(mainContent);
const Consumer = defineComponent(() => {
const [Modal, modalApi] = useVbenModal({
appendToMain: true,
fullscreenButton: true,
title: 'Slot modal',
});
onMounted(() => {
modalApi.open();
});
return () =>
h(Modal, null, {
extra: () => h('span', { class: 'extra-slot-marker' }, 'Custom'),
});
});
const host = document.createElement('div');
document.body.append(host);
activeApp = createApp(() => h(Consumer));
activeApp.mount(host);
await nextTick();
await nextTick();
const marker = document.querySelector('.extra-slot-marker');
expect(marker).toBeInstanceOf(HTMLElement);
if (!(marker instanceof HTMLElement)) return;
expect(marker.textContent?.trim()).toBe('Custom');
// extra slot 应渲染在 fullscreen 按钮容器内,与全屏按钮同排
const container = marker.closest('.absolute');
expect(container).toBeInstanceOf(HTMLElement);
expect(mainContent.contains(container)).toBe(true);
});
it('fires onClosed via the fallback when no animation event arrives', async () => {
vi.useFakeTimers({
toFake: [

20
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@ -319,14 +319,20 @@ function handleClosed() {
<slot></slot>
</div>
<VbenLoading v-if="showLoading || submitting" spinning />
<VbenIconButton
v-if="fullscreenButton"
class="absolute top-3 right-10 flex-center size-6 rounded-full px-1 text-lg text-foreground/80 opacity-70 transition-opacity hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-hidden disabled:pointer-events-none"
@click="handleFullscreen"
<div
v-if="fullscreenButton || $slots.extra"
class="absolute top-3 right-10 flex items-center gap-1"
>
<Shrink v-if="fullscreen" class="size-3.5" />
<Expand v-else class="size-3.5" />
</VbenIconButton>
<slot name="extra"></slot>
<VbenIconButton
v-if="fullscreenButton"
class="flex-center size-6 rounded-full px-1 text-lg text-foreground/80 opacity-70 transition-opacity hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-hidden disabled:pointer-events-none"
@click="handleFullscreen"
>
<Shrink v-if="fullscreen" class="size-3.5" />
<Expand v-else class="size-3.5" />
</VbenIconButton>
</div>
<DialogFooter
ref="footerRef"

Loading…
Cancel
Save