Skip to content

Commit 209d2d1

Browse files
authored
Merge pull request #7941 from nextcloud-libraries/fix/NcModal--teleport-scoped-styles
fix(NcModal): scoped styles with teleport
2 parents ddf0ee8 + 131d167 commit 209d2d1

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

src/components/NcModal/NcModal.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import NcActions from '../NcActions/NcActions.vue'
1616
import NcButton from '../NcButton/NcButton.vue'
1717
import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue'
1818
import { useHotKey } from '../../composables/index.ts'
19+
import { useScopeIdAttrs } from '../../composables/useScopeIdAttrs.ts'
1920
import { t } from '../../l10n.ts'
2021
import { createElementId } from '../../utils/createElementId.ts'
2122
import { getTrapStack } from '../../utils/focusTrap.ts'
@@ -188,6 +189,8 @@ defineSlots<{
188189
default?: Slot
189190
}>()
190191
192+
const scopeIdAttrs = useScopeIdAttrs()
193+
191194
const modalId = createElementId()
192195
const maskElement = useTemplateRef('mask')
193196
@@ -401,7 +404,7 @@ function clearFocusTrap() {
401404
@before-leave="clearFocusTrap">
402405
<div
403406
v-show="showModal"
404-
v-bind="$attrs"
407+
v-bind="{ ...$attrs, ...scopeIdAttrs }"
405408
ref="mask"
406409
class="modal-mask"
407410
:class="{

src/composables/useScopeIdAttrs.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { ComponentInternalInstance } from 'vue'
7+
8+
import { getCurrentInstance, warn } from 'vue'
9+
10+
/**
11+
* Get the parent instance of the instance only if the instance is the root node of the parent
12+
* - Parent > Child - same root node
13+
* - Parent > div > Child - different root node
14+
* - Parent > WrapperWithSlot > Child - different root node
15+
*
16+
* @param instance - Current instance (internal)
17+
*/
18+
function getSameNodeParent(instance: ComponentInternalInstance): ComponentInternalInstance | null {
19+
if (!instance.parent) {
20+
return null
21+
}
22+
23+
if ('vapor' in instance || 'vapor' in instance.parent) {
24+
warn('Vapor instances are not supported in useScopeIdAttrs :(')
25+
return null
26+
}
27+
28+
if (instance.parent.subTree !== instance.vnode) {
29+
return null
30+
}
31+
32+
return instance.parent
33+
}
34+
35+
/**
36+
* Get all ancestor instances of the instance that are on the same root node
37+
*
38+
* @param instance - Current instance (internal)
39+
*/
40+
function getSameNodeAncestors(instance: ComponentInternalInstance): ComponentInternalInstance[] {
41+
const ancestors: ComponentInternalInstance[] = [instance]
42+
let parent = getSameNodeParent(instance)
43+
while (parent) {
44+
ancestors.push(parent)
45+
parent = getSameNodeParent(parent)
46+
}
47+
return ancestors
48+
}
49+
50+
/**
51+
* Get a binding object for all data-v-scopeid attributes that are supposed to be on the root node.
52+
* It allows to have scoped styles from parents in edge cases not covered by Vue:
53+
* - Teleport on the root
54+
* - Fragments
55+
*
56+
* @todo Do we need to support slotScopeIds for `:slotted()`?
57+
*
58+
* @example
59+
* ```ts
60+
* <script setup>
61+
* import { useScopeIdAttrs } from './useScopeIdAttrs.ts'
62+
* const scopeIdAttrs = useScopeIdAttrs()
63+
* </script>
64+
* <template>
65+
* <teleport to="body">
66+
* <div v-bind="scopeIdAttrs" />
67+
* </teleport>
68+
* </template>
69+
* ```
70+
*/
71+
export function useScopeIdAttrs() {
72+
const instance = getCurrentInstance()
73+
74+
if (!instance) {
75+
throw new Error('useScopeId must be called within a setup context')
76+
}
77+
78+
const sameNodeAncestors = getSameNodeAncestors(instance)
79+
const scopeIds = sameNodeAncestors.map((instance) => instance.vnode.scopeId).filter(Boolean)
80+
const scopeIdAttrs = Object.fromEntries(scopeIds.map((scopeId) => [scopeId, '']))
81+
return scopeIdAttrs
82+
}

0 commit comments

Comments
 (0)