From 3361ce2b609b70b473e3d6cf811866ef1d5d5840 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 3 Sep 2026 17:30:16 +0200 Subject: [PATCH] Keep what the component draws inside the container it was given MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening settings in an embedded call put the dialog in the middle of the host's window, spilling outside the container, and with two calls on a page the second drew over the first's dialog. The container established a stacking context but not a containing block, which are two different things and only the first had been done. `position: fixed` resolves against the viewport unless an ancestor makes itself the containing block, so the modal scrim and dialog in Overlay.module.css — `fixed`, `inset: 0`, centred, because in the standalone app they are meant to cover the page — were positioned and sized against the window. Layout and paint containment makes the container the containing block for those descendants and clips what we paint to our own box. The second-call-on-top symptom goes with it, since the dialog now stays inside the first call's box and there is nothing to overlap. Two sibling components still cannot draw over one another by construction, neither being able to leave its own stacking context, but that only shows if a host overlaps them. Containment clips a box measured in viewport units but cannot resize it, so anything positioned that way is simply put somewhere outside the container and disappears. That applied to the reactions overlay, a `100vw` by `100vh` box, and to the reaction picker, which sits at `82vh` so as to appear near the footer it belongs to. Both are positioned against whatever Element Call treats as its root — `[data-overlay-container]` in the app, which is the size of the page, and the host's container when embedded — so percentages mean the same thing there and the right thing here. The earpiece overlay is `inset: 0` with no viewport units, so containment is enough for it. Three viewport-relative sizes remain, all of which need more than a change of unit: the lobby's video preview is `50vh` on a flex item with an aspect ratio, `--content-inset-*` ramps up to a desktop inset from the window width, and the picker's `max-width` cap is the window's. The clipping cuts both ways: a menu near the edge of a small container is trimmed rather than overflowing into the host. That is the trade an embedded component makes. --- component/ElementCall.module.css | 17 ++++++++++++++--- src/button/ReactionToggleButton.module.css | 7 ++++++- src/room/ReactionsOverlay.module.css | 15 +++++++++------ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/component/ElementCall.module.css b/component/ElementCall.module.css index 461ad9b0c..f99dccb16 100644 --- a/component/ElementCall.module.css +++ b/component/ElementCall.module.css @@ -6,15 +6,26 @@ Please see LICENSE in the repository root for full details. */ /* The container a host mounts us into. It fills whatever space the host gives -it, and establishes a stacking context of its own so that our overlays and -modals cannot escape it — which is the whole reason for embedding rather than -using an iframe. */ +it, and nothing we draw may leave it. + +That takes two separate things, which are easy to mistake for one. `isolation` +gives us a stacking context, so nothing inside can be layered above the host's +own interface. Containment makes us the containing block for `position: fixed` +descendants, and clips what we paint to our own box: without it, the modal +scrim and dialog — which are positioned `fixed` and centred, since in the app +they are meant to cover the page — resolve against the viewport and appear in +the middle of the host's window rather than in the middle of the call. + +The clipping cuts both ways: a menu near the edge of a small container is +trimmed rather than overflowing into the host. That is the trade being an +embedded component makes. */ .root { display: flex; flex-direction: column; inline-size: 100%; block-size: 100%; isolation: isolate; + contain: layout paint; position: relative; background-color: var(--cpd-color-bg-canvas-default); color: var(--cpd-color-text-primary); diff --git a/src/button/ReactionToggleButton.module.css b/src/button/ReactionToggleButton.module.css index 90c6af021..705d4d9ed 100644 --- a/src/button/ReactionToggleButton.module.css +++ b/src/button/ReactionToggleButton.module.css @@ -19,7 +19,12 @@ } div.reactionPopupMenuRoot.reactionPopupMenuModal { - --overlay-top: 82vh; + /* Down near the footer it belongs to, rather than centred like other modals. + A percentage, not a viewport unit: the overlay is positioned `fixed`, so this + resolves against the page in the standalone app and against the container + when a host embeds us — where 82vh would put it below the container + entirely. */ + --overlay-top: 82%; width: fit-content; } diff --git a/src/room/ReactionsOverlay.module.css b/src/room/ReactionsOverlay.module.css index 3738dc09e..618adbf38 100644 --- a/src/room/ReactionsOverlay.module.css +++ b/src/room/ReactionsOverlay.module.css @@ -3,8 +3,11 @@ display: inline; z-index: 2; pointer-events: none; - width: 100vw; - height: 100vh; + /* Percentages, not viewport units: the containing block is the element + Element Call treats as its root, which is the page in the standalone app but + the container a host gave us when embedded. */ + width: 100%; + height: 100%; left: 0; top: 0; } @@ -16,7 +19,7 @@ animation-name: reaction-up; width: fit-content; position: relative; - top: 80vh; + top: 80%; } @keyframes reaction-up { @@ -24,7 +27,7 @@ opacity: 1; translate: 0 0; scale: 200%; - top: 80vh; + top: 80%; } to { @@ -48,7 +51,7 @@ .reaction { font-size: 48pt; animation-name: reaction-up-reduced; - top: calc(-50vh + (48pt / 2)); - left: calc(50vw - (48pt / 2)) !important; + top: calc(-50% + (48pt / 2)); + left: calc(50% - (48pt / 2)) !important; } }