Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable manual polyfill #256

Merged
merged 29 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c2a3f25
allow polyfilling a single style element
marchbox Oct 8, 2024
8eeb7a8
remove unnecessary import
marchbox Oct 8, 2024
fe5389c
make polyfill options more flexible and allow a list of target elements
marchbox Oct 9, 2024
a58268f
simplify element list type
marchbox Oct 9, 2024
c0ceb70
add demo for imperative polyfill
marchbox Oct 9, 2024
dc5e6d4
remove unnecessary variable
marchbox Oct 9, 2024
96f825a
remove unnecessary variable
marchbox Oct 9, 2024
24df34b
remove unnecessary promise
marchbox Oct 9, 2024
0cfd3a4
fix edge
marchbox Oct 9, 2024
3b63415
add tests for fetch
marchbox Oct 9, 2024
52bc1a6
add e2e tests
marchbox Oct 9, 2024
7be5e54
add comment to clarify backward compatibility concern
marchbox Oct 10, 2024
c97ae3e
fix linting issues
marchbox Oct 10, 2024
bbadbae
use array filter for better readability
marchbox Oct 10, 2024
4f75075
centralize elements option type check
marchbox Oct 10, 2024
73259da
rename imperative to manual
marchbox Oct 10, 2024
d00ab88
load css dynamically for manual polyfill demo in supported browsers
marchbox Oct 10, 2024
912c553
add test coverage for polyfilling multiple sets of elements
marchbox Oct 11, 2024
798561f
support automatic inline style polyfill with manual polyfill
marchbox Oct 11, 2024
e921c41
fix linting issues
marchbox Oct 11, 2024
e5befac
attempt to deflake test
marchbox Oct 11, 2024
167b030
change includeInlineStyles option to excludeInlineStyles
marchbox Oct 11, 2024
ef0b87d
update based on feedback
marchbox Oct 12, 2024
202d17b
fix linting issues
marchbox Oct 12, 2024
00e1a44
support global options
marchbox Oct 12, 2024
9919720
fix linting issues
marchbox Oct 12, 2024
1628931
simplify code for better readability
marchbox Oct 14, 2024
63c1bcb
Merge branch 'oddbird:main' into add-to-polyfill
marchbox Oct 14, 2024
b4915fa
fix unit tests
marchbox Oct 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
jgerigmeyer marked this conversation as resolved.
Show resolved Hide resolved
.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
Loading