diff --git a/README.md b/README.md index af10bf3..6a3de02 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,26 @@ following features: containing block. To work around this, the polyfill strips any non-`auto` inset from the target (setting `inset: auto`) and re-applies it as padding on the wrapper, so the wrapper continues to drive positioning. + - Moving the target into the wrapper disconnects and reconnects it. If the + target is a custom element, its `connectedCallback` therefore runs more + than once, and any setup that can only happen once must be guarded — for + example, calling `attachShadow()` a second time throws. This applies to + any custom element the polyfill positions with `position-area`, including + a host positioned by a `position-area` in its own `:host` rule: + + ```js + class MyElement extends HTMLElement { + connectedCallback() { + if (this.shadowRoot) return; + this.attachShadow({ mode: 'open' }); + // ... + } + } + ``` + + Setting [`positionAreaContainingBlock`](#positionareacontainingblock) to + `false` (or `'auto'`, for targets that don't need the wrapper) avoids the + wrapper, and with it the reconnection. - When the wrapper is not added, styles that resolve against the containing block — percentage sizes, `auto` or percentage margins, percentage padding, or `stretch`/`anchor-center` self-alignment — will not match native diff --git a/shadow-dom.html b/shadow-dom.html index b835fb9..8ad0cb0 100644 --- a/shadow-dom.html +++ b/shadow-dom.html @@ -97,6 +97,39 @@ } } customElements.define('position-anchor-on-host', PositionAnchorOnHost); + + // `position-area` in a `:host` rule positions the shadow *host*, which + // lives in the outer tree rather than in the shadow root the rule came + // from. The styles the polyfill generates to map the computed insets + // onto the target have to be inserted into that outer tree to match it. + const positionAreaOnHostSheet = new CSSStyleSheet(); + positionAreaOnHostSheet.replaceSync(` + :host { + --element-color: var(--target, var(--outer-anchored)); + background: var(--element-color); + border: thin solid var(--border); + border-radius: var(--radius-1); + color: white; + font-weight: bold; + padding: 0.5em; + white-space: nowrap; + position: absolute; + position-area: top; + } + `); + + class PositionAreaOnHost extends HTMLElement { + connectedCallback() { + // Moving the host into the `position-area` wrapper disconnects and + // reconnects it, so this runs more than once. + if (this.shadowRoot) return; + + this.attachShadow({ mode: 'open' }); + this.shadowRoot.adoptedStyleSheets = [positionAreaOnHostSheet]; + this.shadowRoot.innerHTML = ''; + } + } + customElements.define('position-area-on-host', PositionAreaOnHost); } const btn = document.getElementById('apply-polyfill'); @@ -478,6 +511,70 @@

} customElements.define("position-anchor-on-host", PositionAnchorOnHost); </script> + + +
+

+ + Works when a custom element host has position-area +

+
+
+ Anchor +
+ Target +
+
+

With polyfill applied: Target sits directly above the Anchor.

+

+ The position-area is declared in a + :host rule, so the element it positions is the host + (<position-area-on-host>), which lives in the outer + tree rather than in the shadow root the rule came from. The styles the + polyfill generates to map the computed insets onto the target are + inserted into the host's own tree; a <style> inside + the shadow root would never match the host. +

+
+ +
<div class="anchor" style="anchor-name: --position-area-on-host">Anchor</div>
+<position-area-on-host style="position-anchor: --position-area-on-host">Target</position-area-on-host>
+<script>
+<!-- Load the shadow entrypoint before defining custom elements,
+so the replaceSync and adoptedStyleSheets patches are installed
+before any connectedCallback runs. -->
+import { patchAndPolyfillConstructedStylesheets } from '@oddbird/css-anchor-positioning/fn';
+patchAndPolyfillConstructedStylesheets();
+
+class PositionAreaOnHost extends HTMLElement {
+  connectedCallback() {
+    // Moving the host into the position-area wrapper reconnects it.
+    if (this.shadowRoot) return;
+
+    this.attachShadow({ mode: "open" });
+
+    const sheet = new CSSStyleSheet();
+    sheet.replaceSync(`
+      :host {
+        position: absolute;
+        position-area: top;
+      }
+    `);
+    this.shadowRoot.adoptedStyleSheets = [sheet];
+    this.shadowRoot.innerHTML = "<slot></slot>";
+  }
+}
+customElements.define("position-area-on-host", PositionAreaOnHost);
+</script>