Skip to content

Commit 6ebed89

Browse files
committed
feat(CDropdown, CPopover, CTooltip): allow to "teleport" a component into a DOM node that exists outside the DOM hierarchy of that component
1 parent 122c017 commit 6ebed89

File tree

12 files changed

+289
-131
lines changed

12 files changed

+289
-131
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { defineComponent, h, PropType, ref, Teleport, watch } from 'vue'
2+
3+
const getContainer = (
4+
container?: HTMLElement | (() => HTMLElement) | string,
5+
): HTMLElement | string => {
6+
if (container) {
7+
return typeof container === 'function' ? container() : container
8+
}
9+
10+
return 'body'
11+
}
12+
13+
const CConditionalTeleport = defineComponent({
14+
name: 'CConditionalTeleport',
15+
props: {
16+
/**
17+
* An HTML element or function that returns a single element, with `document.body` as the default.
18+
*
19+
* @since v5.0.0-beta.0
20+
*/
21+
container: {
22+
type: [HTMLElement, () => HTMLElement, String] as PropType<
23+
HTMLElement | (() => HTMLElement) | string
24+
>,
25+
default: 'body',
26+
},
27+
/**
28+
* Render some children into a different part of the DOM
29+
*/
30+
teleport: {
31+
type: [Boolean],
32+
default: true,
33+
},
34+
},
35+
setup(props, { slots }) {
36+
const container = ref<HTMLElement | string>(getContainer(props.container))
37+
38+
watch(
39+
() => [props.container, props.teleport],
40+
() => {
41+
if (props.teleport) {
42+
container.value = getContainer(props.container)
43+
}
44+
},
45+
)
46+
47+
return () =>
48+
h(
49+
Teleport,
50+
{
51+
disabled: props.teleport === false,
52+
to: container.value,
53+
},
54+
{
55+
default: () => slots.default && slots.default(),
56+
},
57+
)
58+
},
59+
})
60+
export { CConditionalTeleport }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { App } from 'vue'
2+
import { CConditionalTeleport } from './CConditionalTeleport'
3+
4+
const CConditionalTeleportPlugin = {
5+
install: (app: App): void => {
6+
app.component(CConditionalTeleport.name, CConditionalTeleport)
7+
},
8+
}
9+
10+
export { CConditionalTeleport, CConditionalTeleportPlugin }

packages/coreui-vue/src/components/dropdown/CDropdown.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ const CDropdown = defineComponent({
5959
return typeof value === 'boolean' || ['inside', 'outside'].includes(value)
6060
},
6161
},
62+
/**
63+
* Appends the vue dropdown menu to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`.
64+
*
65+
* @since v5.0.0-beta.0
66+
*/
67+
container: {
68+
type: [HTMLElement, () => HTMLElement, String] as PropType<
69+
HTMLElement | (() => HTMLElement) | string
70+
>,
71+
default: 'body',
72+
},
6273
/**
6374
* Sets a darker color scheme to match a dark navbar.
6475
*/
@@ -103,6 +114,15 @@ const CDropdown = defineComponent({
103114
type: Boolean,
104115
default: true,
105116
},
117+
/**
118+
* Generates dropdown menu using Teleport.
119+
*
120+
* @since v5.0.0-beta.0
121+
*/
122+
teleport: {
123+
type: Boolean,
124+
default: false,
125+
},
106126
/**
107127
* Sets which event handlers you’d like provided to your toggle prop. You can specify one trigger or an array of them.
108128
*/
@@ -191,8 +211,10 @@ const CDropdown = defineComponent({
191211

192212
provide('config', {
193213
alignment: props.alignment,
214+
container: props.container,
194215
dark: props.dark,
195216
popper: props.popper,
217+
teleport: props.teleport,
196218
})
197219

198220
provide('variant', props.variant)

packages/coreui-vue/src/components/dropdown/CDropdownMenu.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { defineComponent, h, inject, Ref } from 'vue'
22

3+
import { CConditionalTeleport } from '../conditional-teleport'
34
import { getAlignmentClassNames } from './utils'
45

56
const CDropdownMenu = defineComponent({
@@ -20,22 +21,36 @@ const CDropdownMenu = defineComponent({
2021
const config = inject('config') as any // eslint-disable-line @typescript-eslint/no-explicit-any
2122
const visible = inject('visible') as Ref<boolean>
2223

23-
const { alignment, dark, popper } = config
24+
const { alignment, container, dark, popper, teleport } = config
2425

2526
return () =>
2627
h(
27-
props.component,
28+
CConditionalTeleport,
2829
{
29-
class: ['dropdown-menu', { show: visible.value }, getAlignmentClassNames(alignment)],
30-
...((typeof alignment === 'object' || !popper) && {
31-
'data-coreui-popper': 'static',
32-
}),
33-
...(dark && { 'data-coreui-theme': 'dark' }),
34-
ref: dropdownMenuRef,
30+
container: container,
31+
teleport: teleport,
32+
},
33+
{
34+
default: () =>
35+
h(
36+
props.component,
37+
{
38+
class: [
39+
'dropdown-menu',
40+
{ show: visible.value },
41+
getAlignmentClassNames(alignment),
42+
],
43+
...((typeof alignment === 'object' || !popper) && {
44+
'data-coreui-popper': 'static',
45+
}),
46+
...(dark && { 'data-coreui-theme': 'dark' }),
47+
ref: dropdownMenuRef,
48+
},
49+
props.component === 'ul'
50+
? slots.default && slots.default().map((vnode) => h('li', {}, vnode))
51+
: slots.default && slots.default(),
52+
),
3553
},
36-
props.component === 'ul'
37-
? slots.default && slots.default().map((vnode) => h('li', {}, vnode))
38-
: slots.default && slots.default(),
3954
)
4055
},
4156
})

packages/coreui-vue/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './card'
1111
export * from './carousel'
1212
export * from './close-button'
1313
export * from './collapse'
14+
export * from './conditional-teleport'
1415
export * from './dropdown'
1516
export * from './footer'
1617
export * from './form'

packages/coreui-vue/src/components/popover/CPopover.ts

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { defineComponent, h, PropType, ref, RendererElement, Teleport, Transition } from 'vue'
1+
import { defineComponent, h, PropType, ref, RendererElement, Transition } from 'vue'
22
import type { Placement } from '@popperjs/core'
33

4+
import { CConditionalTeleport } from '../conditional-teleport'
45
import { usePopper } from '../../composables'
56
import type { Placements, Triggers } from '../../types'
67
import { executeAfterTransition } from '../../utils/transition'
@@ -18,6 +19,17 @@ const CPopover = defineComponent({
1819
type: Boolean,
1920
default: true,
2021
},
22+
/**
23+
* Appends the vue popover to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`.
24+
*
25+
* @since v5.0.0-beta.0
26+
*/
27+
container: {
28+
type: [HTMLElement, () => HTMLElement, String] as PropType<
29+
HTMLElement | (() => HTMLElement) | string
30+
>,
31+
default: 'body',
32+
},
2133
/**
2234
* Content for your component. If you want to pass non-string value please use dedicated slot `<template #content>...</template>`
2335
*/
@@ -168,53 +180,57 @@ const CPopover = defineComponent({
168180

169181
return () => [
170182
h(
171-
Teleport,
183+
CConditionalTeleport,
172184
{
173-
to: 'body',
185+
container: props.container,
186+
teleport: true,
174187
},
175-
h(
176-
Transition,
177-
{
178-
onEnter: (el, done) => handleEnter(el, done),
179-
onLeave: (el, done) => handleLeave(el, done),
180-
},
181-
() =>
182-
visible.value &&
188+
{
189+
default: () =>
183190
h(
184-
'div',
191+
Transition,
185192
{
186-
class: [
187-
'popover',
188-
'bs-popover-auto',
193+
onEnter: (el, done) => handleEnter(el, done),
194+
onLeave: (el, done) => handleLeave(el, done),
195+
},
196+
() =>
197+
visible.value &&
198+
h(
199+
'div',
189200
{
190-
fade: props.animation,
201+
class: [
202+
'popover',
203+
'bs-popover-auto',
204+
{
205+
fade: props.animation,
206+
},
207+
],
208+
ref: popoverRef,
209+
role: 'tooltip',
210+
...attrs,
191211
},
192-
],
193-
ref: popoverRef,
194-
role: 'tooltip',
195-
...attrs,
196-
},
197-
[
198-
h('div', { class: 'popover-arrow' }),
199-
(props.title || slots.title) &&
200-
h(
201-
'div',
202-
{ class: 'popover-header' },
203-
{
204-
default: () => (slots.title && slots.title()) || props.title,
205-
},
206-
),
207-
(props.content || slots.content) &&
208-
h(
209-
'div',
210-
{ class: 'popover-body' },
211-
{
212-
default: () => (slots.content && slots.content()) || props.content,
213-
},
214-
),
215-
],
212+
[
213+
h('div', { class: 'popover-arrow' }),
214+
(props.title || slots.title) &&
215+
h(
216+
'div',
217+
{ class: 'popover-header' },
218+
{
219+
default: () => (slots.title && slots.title()) || props.title,
220+
},
221+
),
222+
(props.content || slots.content) &&
223+
h(
224+
'div',
225+
{ class: 'popover-body' },
226+
{
227+
default: () => (slots.content && slots.content()) || props.content,
228+
},
229+
),
230+
],
231+
),
216232
),
217-
),
233+
},
218234
),
219235
slots.toggler &&
220236
slots.toggler({

0 commit comments

Comments
 (0)