fix(messaging-ui): portal compose modal to <body> so it renders above host chrome

A host wrapper like .view{position:relative;z-index:1} creates a stacking context
that traps the modal's fixed overlay below a sibling sticky header, no matter its
z-index. Render the modal through a ModalPortal (createPortal to document.body) so
it escapes the host stacking context + overflow entirely; the portal root copies
the SDK theme tokens from the live surface (synchronously, no unstyled paint) and
carries dark defaults as a fallback. Adds react-dom peer dep. 67 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 18:19:25 +05:30
parent f16d7986c7
commit 9882177c59
6 changed files with 60 additions and 5 deletions
+3 -1
View File
@@ -37,8 +37,9 @@
"test": "vitest run"
},
"peerDependencies": {
"@insignia/iios-kernel-client": "*",
"react": ">=18",
"@insignia/iios-kernel-client": "*"
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"@insignia/iios-kernel-client": {
@@ -50,6 +51,7 @@
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.1.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"jsdom": "^26.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -0,0 +1,35 @@
import { useState, type ReactNode, type CSSProperties } from 'react';
import { createPortal } from 'react-dom';
// The SDK theme tokens. A body-portaled node is outside the `.miu-messenger`/`.miu-inbox`
// subtree that defines these, so we copy their resolved values onto the portal root.
const THEME_VARS = [
'--miu-bg', '--miu-panel', '--miu-panel-2', '--miu-border',
'--miu-text', '--miu-muted', '--miu-accent', '--miu-accent-text', '--miu-radius',
] as const;
function copyThemeVars(): Record<string, string> {
if (typeof document === 'undefined') return {};
const src = document.querySelector('.miu-messenger, .miu-inbox');
if (!src) return {};
const cs = getComputedStyle(src);
const out: Record<string, string> = {};
for (const v of THEME_VARS) {
const val = cs.getPropertyValue(v).trim();
if (val) out[v] = val;
}
return out;
}
/**
* Render an overlay into document.body so it escapes the host's stacking context and overflow.
* A host wrapper like `.view { position: relative; z-index: 1 }` traps a `position: fixed` overlay
* BELOW a sibling sticky header no matter how high its z-index — the only robust fix is to leave
* that stacking context entirely. Theme tokens are copied from the live surface (captured once on
* mount, synchronously, so there is no unstyled first paint) and re-applied on the portal root.
*/
export function ModalPortal({ children }: { children: ReactNode }) {
const [vars] = useState<Record<string, string>>(copyThemeVars);
if (typeof document === 'undefined') return null;
return createPortal(<div className="miu-portal" style={vars as CSSProperties}>{children}</div>, document.body);
}
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState, type FormEvent } from 'react';
import { ModalPortal } from '../components/modal-portal';
import { useCompose, useInbox, useMailThread } from './hooks';
import type { InboxItem, InboxState, MailAttachment, MailPerson } from './types';
@@ -254,7 +255,8 @@ function ComposeModal({ onClose, onSent }: { onClose: () => void; onSent: () =>
}
return (
<div className="miu-modal-overlay" onMouseDown={onClose}>
<ModalPortal>
<div className="miu-modal-overlay" onMouseDown={onClose}>
<div className="miu-modal" role="dialog" aria-modal="true" onMouseDown={(e) => e.stopPropagation()}>
<div className="miu-modal-head">
<span>New message</span>
@@ -320,6 +322,7 @@ function ComposeModal({ onClose, onSent }: { onClose: () => void; onSent: () =>
<button type="button" className="miu-send" onClick={() => void send()} disabled={!canSend}>{busy ? 'Sending…' : 'Send'}</button>
</div>
</div>
</div>
</div>
</ModalPortal>
);
}
+14 -2
View File
@@ -802,11 +802,23 @@
word-break: break-word;
}
/* compose modal */
/* compose modal — portaled to <body>, so carry SDK theme-token defaults here (the host's
own token mapping + a synchronous copy on the portal root override these). */
.miu-portal {
--miu-bg: #0e0e13;
--miu-panel: #16161d;
--miu-panel-2: #1d1d26;
--miu-border: #262631;
--miu-text: #e9e9ef;
--miu-muted: #9a9aa7;
--miu-accent: #fda913;
--miu-accent-text: #1a1206;
--miu-radius: 12px;
}
.miu-modal-overlay {
position: fixed;
inset: 0;
z-index: 100;
z-index: 2147483000;
background: rgba(2, 2, 6, 0.6);
display: grid;
place-items: center;
+3
View File
@@ -340,6 +340,9 @@ importers:
'@types/react':
specifier: ^19.0.0
version: 19.2.17
'@types/react-dom':
specifier: ^19.0.0
version: 19.2.3(@types/react@19.2.17)
jsdom:
specifier: ^26.0.0
version: 26.1.0