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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user