/
portal.ts
129 lines (110 loc) · 3.14 KB
/
portal.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import {
Teleport,
defineComponent,
h,
inject,
onUnmounted,
provide,
reactive,
ref,
watchEffect,
// Types
InjectionKey,
PropType,
computed,
} from 'vue'
import { render } from '../../utils/render'
import { usePortalRoot } from '../../internal/portal-force-root'
import { getOwnerDocument } from '../../utils/owner'
// ---
function getPortalRoot(contextElement?: Element | null) {
let ownerDocument = getOwnerDocument(contextElement)
if (!ownerDocument) {
if (contextElement === null) {
return null
}
throw new Error(
`[Headless UI]: Cannot find ownerDocument for contextElement: ${contextElement}`
)
}
let existingRoot = ownerDocument.getElementById('headlessui-portal-root')
if (existingRoot) return existingRoot
let root = ownerDocument.createElement('div')
root.setAttribute('id', 'headlessui-portal-root')
return ownerDocument.body.appendChild(root)
}
export let Portal = defineComponent({
name: 'Portal',
props: {
as: { type: [Object, String], default: 'div' },
},
setup(props, { slots, attrs }) {
let element = ref<HTMLElement | null>(null)
let ownerDocument = computed(() => getOwnerDocument(element))
let forcePortalRoot = usePortalRoot()
let groupContext = inject(PortalGroupContext, null)
let myTarget = ref(
forcePortalRoot === true
? getPortalRoot(element.value)
: groupContext == null
? getPortalRoot(element.value)
: groupContext.resolveTarget()
)
watchEffect(() => {
if (forcePortalRoot) return
if (groupContext == null) return
myTarget.value = groupContext.resolveTarget()
})
onUnmounted(() => {
let root = ownerDocument.value?.getElementById('headlessui-portal-root')
if (!root) return
if (myTarget.value !== root) return
if (myTarget.value.children.length <= 0) {
myTarget.value.parentElement?.removeChild(myTarget.value)
}
})
return () => {
if (myTarget.value === null) return null
let ourProps = {
ref: element,
'data-headlessui-portal': '',
}
return h(
// @ts-expect-error Children can be an object, but TypeScript is not happy
// with it. Once this is fixed upstream we can remove this assertion.
Teleport,
{ to: myTarget.value },
render({
props: { ...props, ...ourProps },
slot: {},
attrs,
slots,
name: 'Portal',
})
)
}
},
})
// ---
let PortalGroupContext = Symbol('PortalGroupContext') as InjectionKey<{
resolveTarget(): HTMLElement | null
}>
export let PortalGroup = defineComponent({
name: 'PortalGroup',
props: {
as: { type: [Object, String], default: 'template' },
target: { type: Object as PropType<HTMLElement | null>, default: null },
},
setup(props, { attrs, slots }) {
let api = reactive({
resolveTarget() {
return props.target
},
})
provide(PortalGroupContext, api)
return () => {
let { target: _, ...incomingProps } = props
return render({ props: incomingProps, slot: {}, attrs, slots, name: 'PortalGroup' })
}
},
})