Skip to content

Commit db16313

Browse files
authored
Work with anchor and target inside same shadow root (#353)
* Work with anchor and target inside the same shadow root * Refactor to use a root option instead of binding * Support multiple roots, use to fetch CSS * Rename to shadow-dom in preparation for other related tests * Roots plural, type normalized options separately to avoid `!` * Add a safety in case of an empty array * Document the new option, update limitations in readme
1 parent b18b8ed commit db16313

File tree

14 files changed

+463
-54
lines changed

14 files changed

+463
-54
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ value of `window.ANCHOR_POSITIONING_POLYFILL_OPTIONS`.
7171
window.ANCHOR_POSITIONING_POLYFILL_OPTIONS = {
7272
elements: undefined,
7373
excludeInlineStyles: false,
74+
roots: [document],
7475
useAnimationFrame: false,
7576
};
7677
import("https://unpkg.com/@oddbird/css-anchor-positioning");
@@ -89,6 +90,7 @@ an argument.
8990
polyfill({
9091
elements: undefined,
9192
excludeInlineStyles: false,
93+
roots: [document],
9294
useAnimationFrame: false,
9395
});
9496
}
@@ -115,6 +117,15 @@ defined. When set to `true`, elements with eligible inline styles listed in the
115117
`elements` option will still be polyfilled, but no other elements in the
116118
document will be implicitly polyfilled.
117119

120+
### roots
121+
122+
type: `(Document | HTMLElement)[]`, default: `[document]`
123+
124+
By default the polyfill applies to `document`, but you can configure one or more
125+
shadow roots the polyfill should apply to using this option. See the
126+
[shadow DOM examples](https://anchor-positioning.oddbird.net/shadow-dom) to learn
127+
more.
128+
118129
### useAnimationFrame
119130

120131
type: `boolean`, default: `false`
@@ -150,7 +161,7 @@ following features:
150161
`align-items` properties
151162
- `position-visibility` property
152163
- dynamically added/removed anchors or targets
153-
- anchors or targets in the shadow-dom
164+
- anchors and targets in separate shadow roots (see https://github.com/oddbird/css-anchor-positioning/issues/191)
154165
- anchors or targets in constructed stylesheets
155166
(https://github.com/oddbird/css-anchor-positioning/issues/228)
156167
- anchor functions assigned to `inset-*` properties or `inset` shorthand

index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,6 +1379,16 @@ <h2>
13791379
color: var(--brand-orange);
13801380
}</code></pre>
13811381
</section>
1382+
<section id="shadow-dom">
1383+
<h2>
1384+
<a href="#shadow-com" aria-hidden="true">🔗</a>
1385+
Positioning within the shadow DOM
1386+
</h2>
1387+
<p class="note">
1388+
The polyfill can position targets and anchors inside a shadow root. See
1389+
<a href="shadow-dom.html">shadow DOM examples</a>.
1390+
</p>
1391+
</section>
13821392
<section id="sponsor">
13831393
<h2>Sponsor OddBird’s OSS Work</h2>
13841394
<p>

public/anchor-shadow-root.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#shadow-anchor-positioning {
2+
anchor-name: --shadow-anchor-positioning;
3+
}
4+
5+
#shadow-target-positioning {
6+
position: absolute;
7+
right: anchor(--shadow-anchor-positioning right, 50px);
8+
top: anchor(--shadow-anchor-positioning bottom);
9+
}

shadow-dom.html

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>CSS Anchor Positioning Polyfill</title>
8+
<link
9+
rel="stylesheet"
10+
href="https://unpkg.com/[email protected]/themes/prism.css"
11+
/>
12+
<script
13+
src="https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js"
14+
crossorigin="anonymous"
15+
defer
16+
></script>
17+
<!-- TypeKit Fonts -->
18+
<script src="https://use.typekit.net/slx1xnq.js"></script>
19+
<script>
20+
try {
21+
Typekit.load({ async: true });
22+
} catch (e) {}
23+
</script>
24+
<link rel="stylesheet" href="/demo.css" />
25+
<style>
26+
anchor-web-component {
27+
width: 100%;
28+
}
29+
</style>
30+
<script type="module">
31+
import polyfill from '/src/index-fn.ts';
32+
33+
const SUPPORTS_ANCHOR_POSITIONING =
34+
'anchorName' in document.documentElement.style;
35+
36+
const btn = document.getElementById('apply-polyfill');
37+
38+
if (!SUPPORTS_ANCHOR_POSITIONING) {
39+
btn.addEventListener('click', () => {
40+
document
41+
.querySelector('anchor-web-component')
42+
.applyPolyfill()
43+
.then(() => {
44+
btn.innerText = 'Polyfill Applied';
45+
btn.setAttribute('disabled', '');
46+
});
47+
});
48+
} else {
49+
btn.innerText = 'No Polyfill Needed';
50+
btn.setAttribute('disabled', '');
51+
console.log(
52+
'anchor-positioning is supported in this browser; polyfill skipped.',
53+
);
54+
}
55+
56+
class AnchorWebComponent extends HTMLElement {
57+
/* This would typically be run in connectedCallback() */
58+
applyPolyfill() {
59+
return polyfill({ roots: [this.shadowRoot] });
60+
}
61+
}
62+
customElements.define('anchor-web-component', AnchorWebComponent);
63+
</script>
64+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
65+
<script src="https://unpkg.com/[email protected]/components/prism-core.min.js"></script>
66+
<script src="https://unpkg.com/[email protected]/plugins/autoloader/prism-autoloader.min.js"></script>
67+
<script
68+
defer
69+
data-domain="anchor-positioning.oddbird.net"
70+
src="https://plausible.io/js/script.hash.outbound-links.js"
71+
></script>
72+
</head>
73+
<body>
74+
<header>
75+
<h1><a href="/">CSS Anchor Positioning Polyfill</a></h1>
76+
<nav>
77+
<span> See also: </span>
78+
<a
79+
href="https://github.com/web-platform-tests/wpt/tree/master/css/css-anchor-position"
80+
target="_blank"
81+
rel="noopener noreferrer"
82+
>WPT examples</a
83+
>
84+
<!-- <a
85+
href="https://anchor-position-wpt.netlify.app/"
86+
target="_blank"
87+
rel="noopener noreferrer"
88+
>WPT results</a
89+
> -->
90+
<a
91+
href="https://drafts.csswg.org/css-anchor-position/"
92+
target="_blank"
93+
rel="noopener noreferrer"
94+
>Draft Spec</a
95+
>
96+
</nav>
97+
<button id="apply-polyfill" data-button="apply-polyfill" type="button">
98+
Apply Polyfill
99+
</button>
100+
</header>
101+
<section>
102+
<h2>Anchoring Elements in the shadow DOM</h2>
103+
<p>
104+
<strong>Note: </strong>We strive to keep the polyfill up-to-date with
105+
ongoing changes to the spec, and we welcome
106+
<a
107+
href="https://github.com/oddbird/css-anchor-positioning"
108+
target="_blank"
109+
rel="noopener noreferrer"
110+
>code contributions</a
111+
>
112+
and <a href="#sponsor">financial support</a> to make that happen.
113+
</p>
114+
</section>
115+
<section id="shadow-root" class="demo-item">
116+
<h2>
117+
<a href="#shadow-root" aria-hidden="true">🔗</a>
118+
Works if anchor and target are both inside the same shadow root
119+
</h2>
120+
<div class="demo-elements">
121+
<anchor-web-component>
122+
<template shadowrootmode="open">
123+
<link rel="stylesheet" href="/demo.css" />
124+
<link rel="stylesheet" href="/anchor-shadow-root.css" />
125+
<div style="position: relative">
126+
<div id="shadow-target-positioning" class="target">Target</div>
127+
<div id="shadow-anchor-positioning" class="anchor">Anchor</div>
128+
</div>
129+
</template>
130+
</anchor-web-component>
131+
</div>
132+
<div class="note">
133+
<p>
134+
With polyfill applied: Target and Anchor’s right edges line up.
135+
Target’s top edge lines up with the bottom edge of the Anchor.
136+
</p>
137+
<p>
138+
<strong>Note: </strong> this will not work across shadow root
139+
boundaries, and will not work with constructed stylesheets.
140+
</p>
141+
</div>
142+
143+
<pre><code class="language-html"
144+
>&lt;anchor-web-component&gt;
145+
&lt;template shadowrootmode="open"&gt;
146+
&lt;style&gt;
147+
#my-anchor-positioning {
148+
anchor-name: --my-anchor-positioning;
149+
}
150+
151+
#my-target-positioning {
152+
position: absolute;
153+
top: anchor(--my-anchor-positioning bottom);
154+
right: anchor(--my-anchor-positioning right, 50px);
155+
}
156+
&lt;/style&gt;
157+
&lt;div style="position: relative"&gt;
158+
&lt;div id="my-target-positioning"&gt;Target&lt;/div&gt;
159+
&lt;div id="my-anchor-positioning"&gt;Anchor&lt;/div&gt;
160+
&lt;/div&gt;
161+
&lt;/template&gt;
162+
&lt;/anchor-web-component&gt;
163+
&lt;script&gt;
164+
class AnchorDemo extends HTMLElement {
165+
connectedCallback() {
166+
window.ANCHOR_POSITIONING_POLYFILL({ roots: [this.shadowRoot] });
167+
}
168+
}
169+
customElements.define("anchor-web-component", AnchorDemo);
170+
&lt;/script&gt;
171+
</code></pre>
172+
</section>
173+
<section id="sponsor">
174+
<h2>Sponsor OddBird’s OSS Work</h2>
175+
<p>
176+
At OddBird, we love contributing to the languages & tools developers
177+
rely on. We’re currently working on polyfills for new Popover & Anchor
178+
Positioning functionality, as well as CSS specifications for functions,
179+
mixins, and responsive typography. Help us keep this work sustainable
180+
and centered on your needs as a developer! We display sponsor logos and
181+
avatars on our
182+
<a
183+
href="https://www.oddbird.net/polyfill/#open-source-sponsors"
184+
target="_blank"
185+
rel="noopener noreferrer"
186+
>website</a
187+
>.
188+
</p>
189+
<a
190+
href="https://github.com/sponsors/oddbird"
191+
target="_blank"
192+
rel="noopener noreferrer"
193+
>Sponsor OddBird’s OSS Work</a
194+
>
195+
</section>
196+
<footer>
197+
<p>
198+
Spec proposal by
199+
<a
200+
href="http://xanthir.com/contact/"
201+
target="_blank"
202+
rel="noopener noreferrer"
203+
>Tab Atkins-Bittner</a
204+
>. Polyfill and demo by
205+
<a
206+
href="https://www.oddbird.net/"
207+
target="_blank"
208+
rel="noopener noreferrer"
209+
>OddBird</a
210+
>.
211+
</p>
212+
</footer>
213+
</body>
214+
</html>

src/dom.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { platform, type VirtualElement } from '@floating-ui/dom';
22
import { nanoid } from 'nanoid/non-secure';
33

44
import { SHIFTED_PROPERTIES } from './cascade.js';
5+
import { type AnchorPositioningRoot } from './polyfill.js';
56

67
/**
78
* Representation of a CSS selector that allows getting the element part and
@@ -140,18 +141,19 @@ function getContainerScrollPosition(element: HTMLElement) {
140141
* Like `document.querySelectorAll`, but if the selector has a pseudo-element it
141142
* will return a wrapper for the rest of the polyfill to use.
142143
*/
143-
export function getElementsBySelector(selector: Selector) {
144+
export function getElementsBySelector(
145+
selector: Selector,
146+
options: { roots: AnchorPositioningRoot[] },
147+
) {
144148
const { elementPart, pseudoElementPart } = selector;
145149
const result: (HTMLElement | PseudoElement)[] = [];
146150
const isBefore = pseudoElementPart === '::before';
147151
const isAfter = pseudoElementPart === '::after';
148152

149-
// Current we only support `::before` and `::after` pseudo-elements.
153+
// Currently we only support `::before` and `::after` pseudo-elements.
150154
if (pseudoElementPart && !(isBefore || isAfter)) return result;
151155

152-
const elements = Array.from(
153-
document.querySelectorAll<HTMLElement>(elementPart),
154-
);
156+
const elements = querySelectorAllRoots(options.roots, elementPart);
155157

156158
if (!pseudoElementPart) {
157159
result.push(...elements);
@@ -255,3 +257,12 @@ export const getOffsetParent = async (el: HTMLElement) => {
255257
}
256258
return offsetParent as HTMLElement;
257259
};
260+
261+
export const querySelectorAllRoots = (
262+
roots: AnchorPositioningRoot[],
263+
selector: string,
264+
): HTMLElement[] => {
265+
return roots.flatMap(
266+
(e) => [...e.querySelectorAll(selector)] as HTMLElement[],
267+
);
268+
};

src/fetch.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { nanoid } from 'nanoid/non-secure';
22

3+
import { querySelectorAllRoots } from './dom.js';
4+
import { type NormalizedAnchorPositioningPolyfillOptions } from './polyfill.js';
35
import { type StyleData } from './utils.js';
46

57
const INVALID_MIME_TYPE_ERROR = 'InvalidMimeType';
@@ -96,11 +98,10 @@ function fetchInlineStyles(elements?: HTMLElement[]) {
9698
}
9799

98100
export async function fetchCSS(
99-
elements?: HTMLElement[],
100-
excludeInlineStyles?: boolean,
101+
options: NormalizedAnchorPositioningPolyfillOptions,
101102
): Promise<StyleData[]> {
102103
const targetElements: HTMLElement[] =
103-
elements ?? Array.from(document.querySelectorAll('link, style'));
104+
options.elements ?? querySelectorAllRoots(options.roots, 'link, style');
104105
const sources: Partial<StyleData>[] = [];
105106

106107
targetElements
@@ -117,7 +118,9 @@ export async function fetchCSS(
117118
}
118119
});
119120

120-
const elementsForInlines = excludeInlineStyles ? (elements ?? []) : undefined;
121+
const elementsForInlines = options.excludeInlineStyles
122+
? (options.elements ?? [])
123+
: undefined;
121124

122125
const inlines = fetchInlineStyles(elementsForInlines);
123126

0 commit comments

Comments
 (0)