Skip to content

Commit d5f2e78

Browse files
authored
fix(ssr): fix ssr for overlay type components (#85)
1 parent 5253c6a commit d5f2e78

File tree

9 files changed

+163
-89
lines changed

9 files changed

+163
-89
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"cSpell.words": [
8282
"composables",
8383
"Vite",
84-
"vitepress"
84+
"vitepress",
85+
"Vuetify's"
8586
],
8687
// Extension: UnoCSS
8788
"unocss.root": "packages/documentation",

netlify.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# a base has not been set. This sample publishes the directory
1313
# located at the absolute path "root/project/build-output"
1414

15-
publish = "packages/documentation/docs/.vitepress/dist"
15+
publish = "docs/.vitepress/dist"
1616
command = "npx pnpm i --store=node_modules/.pnpm-store && npm run docs:build"
1717

1818
# Default build command.

packages/anu-vue/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
},
6767
"peerDependencies": {
6868
"@unocss/reset": "^0.41.1",
69-
"@vueuse/core": "^8.7.5",
69+
"@vueuse/core": "^9.6.0",
7070
"vue-router": "4"
7171
}
7272
}

packages/anu-vue/src/components/dialog/ADialog.tsx

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { onClickOutside } from '@vueuse/core'
1+
import { onClickOutside, useMounted } from '@vueuse/core'
22
import { Teleport, Transition, defineComponent, ref, toRef } from 'vue'
33
import { ACard, useCardProps } from '@/components/card'
44
import { useDOMScrollLock } from '@/composables/useDOMScrollLock'
5+
import { useTeleport } from '@/composables/useTeleport'
56

67
export const ADialog = defineComponent({
78
name: 'ADialog',
@@ -27,6 +28,9 @@ export const ADialog = defineComponent({
2728
},
2829
emits: ['update:modelValue'],
2930
setup(props, { slots, emit, attrs }) {
31+
const { teleportTarget } = useTeleport()
32+
const isMounted = useMounted()
33+
3034
const refCard = ref()
3135
if (!props.persistent) {
3236
onClickOutside(refCard, () => {
@@ -41,26 +45,28 @@ export const ADialog = defineComponent({
4145
// Lock DOM scroll when modelValue is `true`
4246
useDOMScrollLock(toRef(props, 'modelValue'))
4347

44-
return () => <Teleport to="body">
45-
<Transition name="bg">
46-
<div
47-
class={['a-dialog-wrapper grid uno-layer-base-place-items-center fixed uno-layer-base-inset-0 bg-[hsla(var(--a-overlay-color),var(--a-overlay-opacity))]']}
48-
v-show={props.modelValue}
49-
>
50-
<Transition name="scale">
51-
<ACard
52-
{...attrs}
53-
class="a-dialog backface-hidden transform translate-z-0 max-w-[calc(100vw-2rem)]"
54-
ref={refCard}
55-
v-show={props.modelValue}
56-
{...props}
57-
>
58-
{{ ...slots }}
59-
</ACard>
60-
</Transition>
61-
</div>
62-
</Transition>
63-
</Teleport>
48+
return () => isMounted.value
49+
? <Teleport to={teleportTarget.value}>
50+
<Transition name="bg">
51+
<div
52+
class={['a-dialog-wrapper grid uno-layer-base-place-items-center fixed uno-layer-base-inset-0 bg-[hsla(var(--a-overlay-color),var(--a-overlay-opacity))]']}
53+
v-show={props.modelValue}
54+
>
55+
<Transition name="scale">
56+
<ACard
57+
{...attrs}
58+
class="a-dialog backface-hidden transform translate-z-0 max-w-[calc(100vw-2rem)]"
59+
ref={refCard}
60+
v-show={props.modelValue}
61+
{...props}
62+
>
63+
{{ ...slots }}
64+
</ACard>
65+
</Transition>
66+
</div>
67+
</Transition>
68+
</Teleport>
69+
: null
6470
},
6571
})
6672

packages/anu-vue/src/components/drawer/ADrawer.tsx

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { onClickOutside } from '@vueuse/core'
1+
import { onClickOutside, useMounted } from '@vueuse/core'
22
import type { PropType } from 'vue'
33
import { Teleport, Transition, defineComponent, ref, toRef } from 'vue'
4-
import { useDOMScrollLock } from '@/composables/useDOMScrollLock'
54
import { ACard, useCardProps } from '@/components/card'
5+
import { useDOMScrollLock } from '@/composables/useDOMScrollLock'
6+
import { useTeleport } from '@/composables/useTeleport'
67

78
export const ADrawer = defineComponent({
89
name: 'ADrawer',
@@ -35,6 +36,9 @@ export const ADrawer = defineComponent({
3536
},
3637
emits: ['update:modelValue'],
3738
setup(props, { slots, emit, attrs }) {
39+
const { teleportTarget } = useTeleport()
40+
const isMounted = useMounted()
41+
3842
const refCard = ref()
3943
if (!props.persistent) {
4044
onClickOutside(refCard, () => {
@@ -49,38 +53,40 @@ export const ADrawer = defineComponent({
4953
// Lock DOM scroll when modelValue is `true`
5054
useDOMScrollLock(toRef(props, 'modelValue'))
5155

52-
return () => <Teleport to="body">
53-
<Transition name="bg">
54-
<div
55-
class={[
56-
'a-drawer-wrapper flex fixed uno-layer-base-inset-0 bg-[hsla(var(--a-overlay-color),var(--a-overlay-opacity))]',
56+
return () => isMounted.value
57+
? <Teleport to={teleportTarget.value}>
58+
<Transition name="bg">
59+
<div
60+
class={[
61+
'a-drawer-wrapper flex fixed uno-layer-base-inset-0 bg-[hsla(var(--a-overlay-color),var(--a-overlay-opacity))]',
5762
`a-drawer-anchor-${props.anchor}`,
5863

5964
// `flex-col` set full width for top & bottom anchored drawer
6065
['top', 'bottom'].includes(props.anchor) && 'flex-col',
6166

6267
// set drawer to end of flex container of anchor is right or bottom
6368
['right', 'bottom'].includes(props.anchor) && 'justify-end',
64-
]}
65-
v-show={props.modelValue}
66-
>
67-
<Transition name={`slide-${props.anchor === 'bottom' ? 'up' : props.anchor === 'top' ? 'down' : props.anchor}`}>
68-
<ACard
69-
{...attrs}
70-
{...props}
71-
class={[
72-
'a-drawer backface-hidden transform translate-z-0',
73-
props.anchor === 'bottom' && '[--a-transition-slide-up-transform:100%]',
74-
]}
75-
ref={refCard}
76-
v-show={props.modelValue}
77-
>
78-
{{ ...slots }}
79-
</ACard>
80-
</Transition>
81-
</div>
82-
</Transition>
83-
</Teleport>
69+
]}
70+
v-show={props.modelValue}
71+
>
72+
<Transition name={`slide-${props.anchor === 'bottom' ? 'up' : props.anchor === 'top' ? 'down' : props.anchor}`}>
73+
<ACard
74+
{...attrs}
75+
{...props}
76+
class={[
77+
'a-drawer backface-hidden transform translate-z-0',
78+
props.anchor === 'bottom' && '[--a-transition-slide-up-transform:100%]',
79+
]}
80+
ref={refCard}
81+
v-show={props.modelValue}
82+
>
83+
{{ ...slots }}
84+
</ACard>
85+
</Transition>
86+
</div>
87+
</Transition>
88+
</Teleport>
89+
: null
8490
},
8591
})
8692

packages/anu-vue/src/components/menu/AMenu.tsx

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { Middleware, Placement, Strategy } from '@floating-ui/dom'
22
import { autoUpdate, computePosition, flip, shift } from '@floating-ui/dom'
3-
import { onClickOutside, useEventListener } from '@vueuse/core'
3+
import { onClickOutside, useEventListener, useMounted } from '@vueuse/core'
44
import type { PropType } from 'vue'
5-
import { Teleport, Transition, defineComponent, getCurrentInstance, onBeforeUnmount, onMounted, ref, watch } from 'vue'
5+
import { Teleport, Transition, defineComponent, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
66
import { sameWidth as sameWidthMiddleware } from './middlewares'
7+
import { useTeleport } from '@/composables/useTeleport'
78
import { ACard } from '@/components'
89

910
export const AMenu = defineComponent({
@@ -71,6 +72,9 @@ export const AMenu = defineComponent({
7172
},
7273
},
7374
setup(props, { slots }) {
75+
const { teleportTarget } = useTeleport()
76+
const isMounted = useMounted()
77+
7478
const isMenuVisible = ref(props.modelValue ?? false)
7579

7680
// Template refs
@@ -110,15 +114,22 @@ export const AMenu = defineComponent({
110114
}
111115

112116
let floatingUiCleanup: Function
117+
113118
onMounted(() => {
114119
const vm = getCurrentInstance()
115120
refReference.value = vm?.proxy?.$el?.parentNode
116-
117-
// Recalculate position if placement changes at runtime
118-
watch(() => props.placement, () => {
119-
floatingUiCleanup = autoUpdate(refReference.value, refFloating.value.$el, calculateFloatingPosition)
120-
}, { immediate: true })
121121
})
122+
123+
// Recalculate position if placement changes at runtime
124+
watch(
125+
[isMounted, () => props.placement],
126+
() => {
127+
console.log('watch triggered')
128+
nextTick(() => {
129+
floatingUiCleanup = autoUpdate(refReference.value, refFloating.value.$el, calculateFloatingPosition)
130+
})
131+
},
132+
)
122133
onBeforeUnmount(() => floatingUiCleanup())
123134

124135
// 👉 Event listeners
@@ -155,18 +166,20 @@ export const AMenu = defineComponent({
155166
}
156167
}
157168

158-
return () => <Teleport to="body">
159-
{/* ℹ️ Transition component don't accept null as value of name prop so we need `props.transition || undefined` */}
160-
<Transition name={props.transition || undefined}>
161-
<ACard
162-
class={['a-menu', props.strategy === 'fixed' ? 'fixed' : 'absolute']}
163-
ref={refFloating}
164-
v-show={props.modelValue ?? isMenuVisible.value}
165-
>
166-
{slots.default?.()}
167-
</ACard>
168-
</Transition>
169-
</Teleport>
169+
return () => isMounted.value
170+
? <Teleport to={teleportTarget.value}>
171+
{/* ℹ️ Transition component don't accept null as value of name prop so we need `props.transition || undefined` */}
172+
<Transition name={props.transition || undefined}>
173+
<ACard
174+
class={['a-menu', props.strategy === 'fixed' ? 'fixed' : 'absolute']}
175+
ref={refFloating}
176+
v-show={props.modelValue ?? isMenuVisible.value}
177+
>
178+
{slots.default?.()}
179+
</ACard>
180+
</Transition>
181+
</Teleport>
182+
: null
170183
},
171184
})
172185

packages/anu-vue/src/components/select/ASelect.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
2-
import { onClickOutside } from '@vueuse/core'
2+
import { onClickOutside, useMounted } from '@vueuse/core'
33
import type { PropType } from 'vue'
4-
import { Teleport, computed, defineComponent, onBeforeUnmount, onMounted, ref } from 'vue'
4+
import { Teleport, computed, defineComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
55
import { isObject } from '@/utils/helpers'
6+
import { useTeleport } from '@/composables/useTeleport'
67
import { ABaseInput, useBaseInputProp } from '@/components/base-input'
78

89
interface ObjectOption { label: string; value: string | number }
@@ -51,6 +52,9 @@ export const ASelect = defineComponent({
5152
},
5253
emits: ['input', 'update:modelValue'],
5354
setup(props, { slots, emit, attrs }) {
55+
const { teleportTarget } = useTeleport()
56+
const isMounted = useMounted()
57+
5458
// SECTION Floating
5559
// Template refs
5660
const refReference = ref()
@@ -87,7 +91,9 @@ export const ASelect = defineComponent({
8791

8892
let floatingUiCleanup: Function = () => { }
8993
onMounted(() => {
90-
floatingUiCleanup = autoUpdate(refReference.value.refInputContainer, refFloating.value, calculateFloatingPosition)
94+
nextTick(() => {
95+
floatingUiCleanup = autoUpdate(refReference.value.refInputContainer, refFloating.value, calculateFloatingPosition)
96+
})
9197
})
9298
onBeforeUnmount(() => floatingUiCleanup())
9399

@@ -157,19 +163,21 @@ export const ASelect = defineComponent({
157163
/>,
158164
}}
159165
</ABaseInput>
160-
<Teleport to="body">
161-
<ul
162-
class={[
163-
'a-select-options-container absolute bg-[hsl(var(--a-layer))]',
164-
props.optionsWrapperClasses,
165-
]}
166-
onClick={closeOptions}
167-
ref={refFloating}
168-
v-show={isOptionsVisible.value}
169-
>
170-
{
166+
{
167+
isMounted.value
168+
? <Teleport to={teleportTarget.value}>
169+
<ul
170+
class={[
171+
'a-select-options-container absolute bg-[hsl(var(--a-layer))]',
172+
props.optionsWrapperClasses,
173+
]}
174+
onClick={closeOptions}
175+
ref={refFloating}
176+
v-show={isOptionsVisible.value}
177+
>
178+
{
171179
slots.default
172-
? slots.default?.({
180+
? slots.default({
173181
attrs: {
174182
class: optionClasses,
175183
},
@@ -183,8 +191,10 @@ export const ASelect = defineComponent({
183191
</li>
184192
))
185193
}
186-
</ul>
187-
</Teleport>
194+
</ul>
195+
</Teleport>
196+
: null
197+
}
188198
</>
189199
},
190200
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// ℹ️ Inspired from Vuetify's useTeleport composable
2+
3+
import type { Ref } from 'vue'
4+
import { computed, unref } from 'vue'
5+
6+
export function useTeleport(target?: Ref<string | Element>) {
7+
const teleportTarget = computed(() => {
8+
const _target = unref(target)
9+
10+
if (typeof window === 'undefined')
11+
return undefined
12+
13+
const targetElement
14+
= _target === undefined
15+
? document.body
16+
: typeof _target === 'string'
17+
? document.querySelector(_target)
18+
: _target
19+
20+
if (targetElement == null) {
21+
console.warn(`Unable to locate target ${_target}`)
22+
23+
return undefined
24+
}
25+
26+
if (!useTeleport.cache.has(targetElement)) {
27+
const el = document.createElement('div')
28+
el.id = 'a-teleport-target'
29+
targetElement.appendChild(el)
30+
useTeleport.cache.set(targetElement, el)
31+
}
32+
33+
return useTeleport.cache.get(targetElement)
34+
})
35+
36+
return { teleportTarget }
37+
}
38+
useTeleport.cache = new WeakMap<Element, Element>()

0 commit comments

Comments
 (0)