Skip to content

Commit

Permalink
Merge pull request #256 from marchbox/add-to-polyfill
Browse files Browse the repository at this point in the history
Enable manual polyfill
  • Loading branch information
jamesnw authored Oct 14, 2024
2 parents 2c69cc0 + b4915fa commit c9edaae
Show file tree
Hide file tree
Showing 8 changed files with 562 additions and 31 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ lib-cov
coverage
*.lcov
playwright-report
test-results

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
Expand Down
199 changes: 198 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,20 @@
bottom: anchor(--my-anchor-style-tag start);
right: anchor(--my-anchor-style-tag left);
}

#anchor-manual .anchor {
inline-size: fit-content;
margin: 3rem auto;
}
</style>
<script type="module">
import polyfill from '/src/index-fn.ts';

const SUPPORTS_ANCHOR_POSITIONING = CSS.supports('anchor-name: --a');

const btn = document.getElementById('apply-polyfill');

if (!('anchorName' in document.documentElement.style)) {
if (!SUPPORTS_ANCHOR_POSITIONING) {
btn.addEventListener('click', () =>
polyfill().then((rules) => {
btn.innerText = 'Polyfill Applied';
Expand All @@ -80,6 +87,116 @@
updateAnchor.removeAttribute('data-large');
}
});

function prepareManualPolyfill() {
// anchor style element
const anchorStyleEl = document.createElement('style');
anchorStyleEl.id = 'my-style-manual-anchor';
anchorStyleEl.textContent = [
'#my-anchor-manual {',
'anchor-name: --my-anchor-manual;',
'}',
].join('');

// style element
const styleEl = document.createElement('style');
styleEl.id = 'my-style-manual-style-el';
styleEl.textContent = [
'#my-target-manual-style-el {',
'position: absolute;',
'bottom: anchor(--my-anchor-manual top);',
'right: anchor(--my-anchor-manual left);',
'}',
].join('');

// link element
const linkEl = document.createElement('link');
linkEl.id = 'my-style-manual-link-el';
linkEl.rel = 'stylesheet';
linkEl.href = '/anchor-manual.css';

document.head.append(anchorStyleEl, styleEl, linkEl);

// inline style
document
.getElementById('my-target-manual-inline-style')
?.setAttribute(
'style',
[
'position: absolute',
'top: anchor(--my-anchor-manual bottom)',
'left: anchor(--my-anchor-manual right)',
].join(';'),
);
}

// These event listeners are for E2E testing only
document
.getElementById('prepare-manual-polyfill')
?.addEventListener('click', () => prepareManualPolyfill(), {
once: true,
});
document
.getElementById('apply-polyfill-manually-set1')
?.addEventListener('click', () => {
polyfill({
elements: [
document.getElementById('my-style-manual-anchor'),
document.getElementById('my-style-manual-style-el'),
],
excludeInlineStyles: true,
});
});
document
.getElementById('apply-polyfill-manually-set2')
?.addEventListener('click', () => {
polyfill({
elements: [
document.getElementById('my-style-manual-anchor'),
document.getElementById('my-style-manual-link-el'),
document.getElementById('my-target-manual-inline-style'),
],
excludeInlineStyles: true,
});
});
document
.getElementById('apply-polyfill-manually-set3')
?.addEventListener('click', () => {
polyfill({
elements: [
document.getElementById('my-style-manual-anchor'),
document.getElementById('my-style-manual-style-el'),
],
});
});

const manualBtn = document.getElementById('apply-polyfill-manually');
if (SUPPORTS_ANCHOR_POSITIONING) {
manualBtn.innerText = 'Load Anchor Positioning CSS';
}
manualBtn.addEventListener('click', () => {
prepareManualPolyfill();

if (!SUPPORTS_ANCHOR_POSITIONING) {
polyfill({
elements: [
document.getElementById('my-style-manual-anchor'),
document.getElementById('my-style-manual-link-el'),
document.getElementById('my-style-manual-style-el'),
document.getElementById('my-target-manual-inline-style'),
],
}).then((rules) => {
manualBtn.innerText = 'Polyfill Applied';
console.log(rules);
});
} else {
manualBtn.innerText = 'Anchor Positioning CSS applied';
console.log(
'anchor-positioning is supported in this browser; polyfill skipped.',
);
}
manualBtn.setAttribute('disabled', '');
});
</script>
<script src="https://unpkg.com/[email protected]/components/prism-core.min.js"></script>
<script src="https://unpkg.com/[email protected]/plugins/autoloader/prism-autoloader.min.js"></script>
Expand Down Expand Up @@ -1003,6 +1120,86 @@ <h2>
top: anchor(--my-anchor-media-query top);
right: anchor(--my-anchor-media-query right);
}</code></pre>
</section>
<section id="anchor-manual" class="demo-item" style="position: relative">
<h2>
<a href="#manual" aria-hidden="true">🔗</a>
Manually apply polyfill to specific styles
</h2>
<button id="apply-polyfill-manually">Polyfill these elements</button>
<div id="anchor-manual-test-buttons" hidden>
<!-- These buttons are for E2E testing only -->
<button id="prepare-manual-polyfill">Prepare</button>
<button id="apply-polyfill-manually-set1">Polyfill target 1</button>
<button id="apply-polyfill-manually-set2">
Polyfill target 2 and 3
</button>
<button id="apply-polyfill-manually-set3">
Polyfill target 1 and 3
</button>
</div>
<div class="demo-elements">
<div id="my-anchor-manual" class="anchor">Anchor</div>
<div id="my-target-manual-style-el" class="target">
Target 1 (with <code>&lt;style&gt;</code>)
</div>
<div id="my-target-manual-link-el" class="target">
Target 2 (with <code>&lt;link&gt;</code>)
</div>
<div id="my-target-manual-inline-style" class="target">
Target 3 (with inline style)
</div>
</div>
<p class="note">
With polyfill applied: Target 1, 2, and 3 are positioned at Anchor’s
top-left, top-right, and bottom-right corners respectively.
</p>
<pre><code class="language-html" data-dependencies="css,js">&lt;style id="my-style-manual-anchor"&gt;
#my-anchor-manual {
anchor-name: --my-anchor-manual;
}
&lt;/style&gt;
&lt;style id="my-style-manual-style-el"&gt;
#my-target-manual-style-el {
position: absolute;
bottom: anchor(--my-anchor-manual top);
right: anchor(--my-anchor-manual left);
}
&lt;/style&gt;
&lt;link rel="stylesheet" href="/anchor-manual.css" id="my-style-manual-link-el" /&gt;
&lt;!--
CSS inside the anchor-manual.css file:

#my-target-manual-link-el {
position: absolute;
bottom: anchor(--my-anchor-manual top);
left: anchor(--my-anchor-manual right);
}
--&gt;

&lt;div id="my-anchor-manual" class="anchor"&gt;...&lt;/div&gt;
&lt;div id="my-target-manual-style-el" class="target"&gt;...&lt;/div&gt;
&lt;div id="my-target-manual-link-el" class="target"&gt;...&lt;/div&gt;
&lt;div id="my-target-manual-inline-style" class="target"
style="position: absolute;
top: anchor(--my-anchor-manual bottom);
left: anchor(--my-anchor-manual right);"
&gt;...&lt;/div&gt;

&lt;script&gt;
polyfill({
elements: [
// The &lt;style&gt; element for anchor
document.getElementById('my-style-manual-anchor'),
// The &lt;style&gt; element
document.getElementById('my-style-manual-style-el'),
// The &lt;link&gt; element
document.getElementById('my-style-manual-link-el'),
// The target element with inline styles
document.getElementById('my-target-manual-inline-style'),
],
});
&lt;/script&gt;</code></pre>
</section>
<section id="sponsor">
<h2>Sponsor OddBird's OSS Work</h2>
Expand Down
5 changes: 5 additions & 0 deletions public/anchor-manual.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#my-target-manual-link-el {
position: absolute;
bottom: anchor(--my-anchor-manual top);
left: anchor(--my-anchor-manual right);
}
18 changes: 18 additions & 0 deletions src/@types/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
export {};

declare global {
interface AnchorPositioningPolyfillOptions {
// Whether to use `requestAnimationFrame()` when updating target elements’
// positions
useAnimationFrame?: boolean;

// An array of explicitly targeted elements to polyfill
elements?: HTMLElement[];

// Whether to exclude elements with eligible inline styles. When not defined
// or set to `false`, the polyfill will be applied to all elements that have
// eligible inline styles, regardless of whether the `elements` option is
// defined. When set to `true`, elements with eligible inline styles listed
// in the `elements` option will still be polyfilled, but no other elements
// in the document will be implicitly polyfilled.
excludeInlineStyles?: boolean;
}

interface Window {
UPDATE_ANCHOR_ON_ANIMATION_FRAME?: boolean;
ANCHOR_POSITIONING_POLYFILL_OPTIONS?: AnchorPositioningPolyfillOptions;
CHECK_LAYOUT_DELAY?: boolean;
}
}
67 changes: 42 additions & 25 deletions src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,45 +51,62 @@ async function fetchLinkedStylesheets(
return results.filter((loaded) => loaded !== null);
}

const ELEMENTS_WITH_INLINE_ANCHOR_STYLES_QUERY = '[style*="anchor"]';
// Searches for all elements with inline style attributes that include `anchor`.
// For each element found, adds a new 'data-has-inline-styles' attribute with a
// random UUID value, and then formats the styles in the same manner as CSS from
// style tags.
function fetchInlineStyles() {
const elementsWithInlineAnchorStyles: NodeListOf<HTMLElement> =
document.querySelectorAll('[style*="anchor"]');
function fetchInlineStyles(elements?: HTMLElement[]) {
const elementsWithInlineAnchorStyles: HTMLElement[] = elements
? elements.filter(
(el) =>
el instanceof HTMLElement &&
el.matches(ELEMENTS_WITH_INLINE_ANCHOR_STYLES_QUERY),
)
: Array.from(
document.querySelectorAll(ELEMENTS_WITH_INLINE_ANCHOR_STYLES_QUERY),
);
const inlineStyles: Partial<StyleData>[] = [];

elementsWithInlineAnchorStyles.forEach((el) => {
const selector = nanoid(12);
const dataAttribute = 'data-has-inline-styles';
el.setAttribute(dataAttribute, selector);
const styles = el.getAttribute('style');
const css = `[${dataAttribute}="${selector}"] { ${styles} }`;
inlineStyles.push({ el, css });
});
elementsWithInlineAnchorStyles
.filter((el) => el instanceof HTMLElement)
.forEach((el) => {
const selector = nanoid(12);
const dataAttribute = 'data-has-inline-styles';
el.setAttribute(dataAttribute, selector);
const styles = el.getAttribute('style');
const css = `[${dataAttribute}="${selector}"] { ${styles} }`;
inlineStyles.push({ el, css });
});

return inlineStyles;
}

export async function fetchCSS(): Promise<StyleData[]> {
const elements: NodeListOf<HTMLElement> =
document.querySelectorAll('link, style');
export async function fetchCSS(
elements?: HTMLElement[],
excludeInlineStyles?: boolean,
): Promise<StyleData[]> {
const targetElements: HTMLElement[] =
elements ?? Array.from(document.querySelectorAll('link, style'));
const sources: Partial<StyleData>[] = [];

elements.forEach((el) => {
if (el.tagName.toLowerCase() === 'link') {
const url = getStylesheetUrl(el as HTMLLinkElement);
if (url) {
sources.push({ el, url });
targetElements
.filter((el) => el instanceof HTMLElement)
.forEach((el) => {
if (el.tagName.toLowerCase() === 'link') {
const url = getStylesheetUrl(el as HTMLLinkElement);
if (url) {
sources.push({ el, url });
}
}
}
if (el.tagName.toLowerCase() === 'style') {
sources.push({ el, css: el.innerHTML });
}
});
if (el.tagName.toLowerCase() === 'style') {
sources.push({ el, css: el.innerHTML });
}
});

const elementsForInlines = excludeInlineStyles ? (elements ?? []) : undefined;

const inlines = fetchInlineStyles();
const inlines = fetchInlineStyles(elementsForInlines);

return await fetchLinkedStylesheets([...sources, ...inlines]);
}
Loading

0 comments on commit c9edaae

Please sign in to comment.