feat(Popover): add anchor slot (#4119)

Co-authored-by: Jakub <jakub.michalek@freelo.io>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
J-Michalek
2025-05-22 17:04:17 +02:00
committed by GitHub
parent fe4e1f859d
commit 473513c246
7 changed files with 97 additions and 1 deletions

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
const open = ref(false)
</script>
<template>
<UPopover
v-model:open="open"
:dismissible="false"
:ui="{ content: 'w-(--reka-popper-anchor-width) p-4' }"
>
<template #anchor>
<UInput placeholder="Focus to open" @focus="open = true" @blur="open = false" />
</template>
<template #content>
<Placeholder class="w-full aspect-square" />
</template>
</UPopover>
</template>

View File

@@ -202,6 +202,21 @@ name: 'popover-command-palette-example'
---
::
### With anchor slot
You can use the `#anchor` slot to position the Popover against a custom element.
::warning
This slot only works when `mode` is `click`.
::
::component-example
---
collapse: true
name: 'popover-anchor-slot-example'
---
::
## API
### Props

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
const open = ref(false)
const openCustomAnchor = ref(false)
const loading = ref(false)
function send() {
@@ -51,6 +52,21 @@ function send() {
</div>
</template>
</UPopover>
<div class="mt-8 relative">
<UPopover
v-model:open="openCustomAnchor"
:dismissible="false"
>
<template #anchor>
<UInput placeholder="Search" class="w-56" @focus="openCustomAnchor = true" />
</template>
<template #content>
<Placeholder class="size-48 m-4 inline-flex" />
</template>
</UPopover>
</div>
</div>
<div class="mt-24">

View File

@@ -43,6 +43,7 @@ export interface PopoverEmits extends PopoverRootEmits {
export interface PopoverSlots {
default(props: { open: boolean }): any
content(props?: {}): any
anchor(props?: {}): any
}
</script>
@@ -103,6 +104,10 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
<slot :open="open" />
</Component.Trigger>
<Component.Anchor v-if="'Anchor' in Component && !!slots.anchor" as-child>
<slot name="anchor" />
</Component.Anchor>
<Component.Portal v-bind="portalProps">
<Component.Content v-bind="contentProps" :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-on="contentEvents">
<slot name="content" />

View File

@@ -13,7 +13,8 @@ describe('Popover', () => {
['with ui', { props: { ...props, ui: { content: 'shadow-xl' } } }],
// Slots
['with default slot', { props, slots: { default: () => 'Default slot' } }],
['with content slot', { props, slots: { content: () => 'Content slot' } }]
['with content slot', { props, slots: { content: () => 'Content slot' } }],
['with anchor slot', { props, slots: { anchor: () => 'Anchor slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: PopoverProps, slots?: Partial<PopoverSlots> }) => {
const html = await ComponentRender(nameOrHtml, options, Popover)
expect(html).toMatchSnapshot()

View File

@@ -1,7 +1,22 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Popover > renders with anchor slot correctly 1`] = `
"<!--v-if-->
Anchor slot
<!--teleport start-->
<div data-reka-popper-content-wrapper="" style="position: fixed; left: 0px; top: 0px; transform: translate(0, -200%); min-width: max-content;">
<div id="reka-popover-content-v-0" data-state="open" aria-labelledby="" style="--reka-popover-content-transform-origin: var(--reka-popper-transform-origin); --reka-popover-content-available-width: var(--reka-popper-available-width); --reka-popover-content-available-height: var(--reka-popper-available-height); --reka-popover-trigger-width: var(--reka-popper-anchor-width); --reka-popover-trigger-height: var(--reka-popper-anchor-height); animation: none;" role="dialog" data-dismissable-layer="" tabindex="-1" class="bg-default shadow-lg rounded-md ring ring-default data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-popover-content-transform-origin) focus:outline-none pointer-events-auto" data-side="bottom" data-align="center"></div>
</div>
<!--teleport end-->"
`;
exports[`Popover > renders with arrow correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->
@@ -15,6 +30,7 @@ exports[`Popover > renders with arrow correctly 1`] = `
exports[`Popover > renders with class correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->
@@ -28,6 +44,7 @@ exports[`Popover > renders with class correctly 1`] = `
exports[`Popover > renders with content slot correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->
@@ -43,6 +60,7 @@ exports[`Popover > renders with content slot correctly 1`] = `
exports[`Popover > renders with default slot correctly 1`] = `
"Default slot
<!--v-if-->
<!--teleport start-->
@@ -56,6 +74,7 @@ exports[`Popover > renders with default slot correctly 1`] = `
exports[`Popover > renders with open correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->
@@ -69,6 +88,7 @@ exports[`Popover > renders with open correctly 1`] = `
exports[`Popover > renders with ui correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->

View File

@@ -1,7 +1,22 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Popover > renders with anchor slot correctly 1`] = `
"<!--v-if-->
Anchor slot
<!--teleport start-->
<div data-reka-popper-content-wrapper="" style="position: fixed; left: 0px; top: 0px; transform: translate(0, -200%); min-width: max-content;">
<div id="reka-popover-content-v-0-0-0" data-state="open" aria-labelledby="" style="--reka-popover-content-transform-origin: var(--reka-popper-transform-origin); --reka-popover-content-available-width: var(--reka-popper-available-width); --reka-popover-content-available-height: var(--reka-popper-available-height); --reka-popover-trigger-width: var(--reka-popper-anchor-width); --reka-popover-trigger-height: var(--reka-popper-anchor-height); animation: none;" role="dialog" data-dismissable-layer="" tabindex="-1" class="bg-default shadow-lg rounded-md ring ring-default data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-popover-content-transform-origin) focus:outline-none pointer-events-auto" data-side="bottom" data-align="center"></div>
</div>
<!--teleport end-->"
`;
exports[`Popover > renders with arrow correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->
@@ -15,6 +30,7 @@ exports[`Popover > renders with arrow correctly 1`] = `
exports[`Popover > renders with class correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->
@@ -28,6 +44,7 @@ exports[`Popover > renders with class correctly 1`] = `
exports[`Popover > renders with content slot correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->
@@ -43,6 +60,7 @@ exports[`Popover > renders with content slot correctly 1`] = `
exports[`Popover > renders with default slot correctly 1`] = `
"Default slot
<!--v-if-->
<!--teleport start-->
@@ -56,6 +74,7 @@ exports[`Popover > renders with default slot correctly 1`] = `
exports[`Popover > renders with open correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->
@@ -69,6 +88,7 @@ exports[`Popover > renders with open correctly 1`] = `
exports[`Popover > renders with ui correctly 1`] = `
"<!--v-if-->
<!--v-if-->
<!--teleport start-->