feat: push modal to shadow root in isolated shell mode

This commit is contained in:
Shivam Mishra
2025-12-15 15:39:23 +05:30
parent 9ea746df9a
commit 73bd05a941
2 changed files with 51 additions and 7 deletions
+18 -7
View File
@@ -15,6 +15,7 @@ const { modalType, closeOnBackdropClick, onClose } = defineProps({
const emit = defineEmits(['close']);
const show = defineModel('show', { type: Boolean, default: false });
const teleportTarget = ref(null);
const modalClassName = computed(() => {
const modalClassNameMap = {
@@ -25,13 +26,23 @@ const modalClassName = computed(() => {
return `modal-mask skip-context-menu ${modalClassNameMap[modalType] || ''}`;
});
const teleportTarget = computed(() => {
const resolveTeleportTarget = () => {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) {
console.log(document.getElementById('cw-modal-root'));
return '#cw-modal-root';
// Look for the specific chatwoot-message-list custom element
const messageListElement = document.querySelector('chatwoot-message-list');
if (messageListElement?.shadowRoot) {
const modalRoot =
messageListElement.shadowRoot.querySelector('#cw-modal-root');
if (modalRoot) {
teleportTarget.value = modalRoot;
return;
}
}
}
return 'body';
});
teleportTarget.value = document.body;
};
// [TODO] Revisit this logic to use outside click directive
const mousedDownOnBackdrop = ref(false);
@@ -66,7 +77,7 @@ useEventListener(document.body, 'mouseup', onMouseUp);
useEventListener(document, 'keydown', onKeydown);
onMounted(() => {
console.log('TESTING 15:10');
resolveTeleportTarget();
if (import.meta.env.DEV && onClose && typeof onClose === 'function') {
// eslint-disable-next-line no-console
console.warn(
@@ -77,7 +88,7 @@ onMounted(() => {
</script>
<template>
<Teleport :to="teleportTarget">
<Teleport v-if="teleportTarget" :to="teleportTarget">
<transition name="modal-fade">
<div
v-if="show"
+33
View File
@@ -0,0 +1,33 @@
# Modal teleport target escapes shadow DOM in embedded mode
## Context
- Vue components are exported as custom elements for React, rendered inside a shadow DOM.
- The modal component teleports its content; when it teleports to `document.body`, it leaves the shadow DOM and loses styles.
## Relevant files
- `app/javascript/react-components/src/vue-components/ChatwootMessageListWebComponent.vue`
- CE wrapper rendered inside the shadow root.
- Currently includes both `#cw-app-root` (MessageList) and `#cw-modal-root` (intended modal host) inside the shadow DOM.
- `app/javascript/dashboard/components/Modal.vue`
- Modal component using `<Teleport>`.
- Has logic to resolve the teleport target using `getRootNode` and a fallback to `document.body`.
- Entry/UI setup: `app/javascript/entrypoints/ui.js` mounts the Vue app in non-CE mode (body teleport is fine there).
## Current behavior
- In embedded/isolated mode (`window.__WOOT_ISOLATED_SHELL__` is true), the modal still teleports to `document.body` instead of the shadow DOM.
- Because the modal node ends up in `body`, CE-injected styles do not apply; the modal appears unstyled.
## Desired behavior
- When running inside the custom element (shadow DOM), the modal should teleport to `#cw-modal-root` inside that shadow root (the element already exists in the CE wrapper).
- Only fall back to `document.body` when not in embedded mode or not in a shadow root.
## Likely fixes
- In `Modal.vue`, resolve the teleport target on mount:
- Use `getRootNode()` on the component root to detect a `ShadowRoot`.
- If in a `ShadowRoot`, select `#cw-modal-root` there and use that element.
- If not found or not in a shadow root, fall back to `document.body`.
- Remove extra complexity (e.g., hidden anchors or element creation); the modal host is already present in the CE template.
## Acceptance criteria
- In embedded mode, the modal stays inside the shadow DOM and is styled (teleport target is the shadows `#cw-modal-root`).
- In normal app mode, behavior remains unchanged (teleport to `body`).