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

Allow updating anchor on every animationFrame. #87

Merged
merged 9 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,37 @@ To use the polyfill, add this script tag to your document `<head>`:

You can view a more complete demo [here](https://anchor-polyfill.netlify.app/).

## Configuration

The polyfill accepts one argument (type: `boolean`, default: `false`), which
determines whether anchor calculations should [update on every animation
frame](https://floating-ui.com/docs/autoUpdate#animationframe) (e.g. when the
anchor element moves), in addition to always updating on scroll/resize. While
this option is optimized for performance, it should be used sparingly.

```js
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
const { default: polyfill } = await import("https://unpkg.com/@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js");

polyfill(true);
}
</script>
```

When using the default version of the polyfill that executes automatically, this
option can be set by setting the value of
`window.UPDATE_ANCHOR_ON_ANIMATION_FRAME`.

```js
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
window.UPDATE_ANCHOR_ON_ANIMATION_FRAME = true;
import("https://unpkg.com/@oddbird/css-anchor-positioning");
}
</script>
```

## Limitations

This polyfill doesn't (yet) support the following:
Expand All @@ -37,21 +68,19 @@ This polyfill doesn't (yet) support the following:
- anchor functions with `implicit` anchor-element
- automatic anchor positioning: anchor functions with `auto` or `auto-same`
anchor-side
- dynamic anchor movement other than container resize/scroll
([#73](https://github.com/oddbird/css-anchor-positioning/issues/73))
- dynamically added/removed anchors or targets
- anchors or targets in the shadow-dom
- anchor functions assigned to `inset-*` properties or `inset` shorthand
property
- vertical/rtl writing-modes (partial support)
- absolutely-positioned targets with `grid-column`/`grid-row`/`grid-area` in a
CSS Grid layout
- `@position-fallback` where targets overflow the grid area but do not overflow
the containing block
- `@position-fallback` where targets in a CSS Grid layout overflow the grid area
but do not overflow the containing block
- `@position-fallback` where targets overflow their inset-modified containing
block, overlapping the anchor element
- anchors in multi-column layouts
- anchor functions used as the fallback value for another anchor function
- anchor functions used as the fallback value in another anchor function
- anchor functions assigned to `bottom` or `right` properties on inline targets
whose offset-parent is inline with `clientHeight`/`clientWidth` of `0`
(partial support -- does not account for possible scrollbar width)
48 changes: 45 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<link rel="stylesheet" href="/anchor-custom-props.css" />
<link rel="stylesheet" href="/anchor-duplicate-custom-props.css" />
<link rel="stylesheet" href="/anchor-implicit.css" />
<link rel="stylesheet" href="/anchor-update.css" />
<style>
#my-anchor-style-tag {
anchor-name: --my-anchor-style-tag;
Expand All @@ -39,7 +40,7 @@

if (!('anchorName' in document.documentElement.style)) {
btn.addEventListener('click', () =>
polyfill().then((rules) => {
polyfill(true).then((rules) => {
btn.innerText = 'Polyfill Applied';
btn.setAttribute('disabled', '');
console.log(rules);
Expand All @@ -52,6 +53,18 @@
'anchor-positioning is supported in this browser; polyfill skipped.',
);
}

const updateBtn = document.getElementById('toggle-anchor-width');
const updateAnchor = document.getElementById('my-anchor-update');
updateBtn.addEventListener('click', () => {
if (updateAnchor.getAttribute('data-small')) {
updateAnchor.setAttribute('data-large', true);
updateAnchor.removeAttribute('data-small');
} else {
updateAnchor.setAttribute('data-small', true);
updateAnchor.removeAttribute('data-large');
}
});
</script>
<script src="https://unpkg.com/prismjs@v1.x/components/prism-core.min.js"></script>
<script src="https://unpkg.com/prismjs@v1.x/plugins/autoloader/prism-autoloader.min.js"></script>
Expand All @@ -74,7 +87,7 @@ <h1>CSS Anchor Positioning Polyfill</h1>
>WPT results</a
>
<a
href="https://w3c.github.io/csswg-drafts/css-anchor/"
href="https://w3c.github.io/csswg-drafts/css-anchor-position/"
target="_blank"
rel="noopener noreferrer"
>Draft Spec</a
Expand All @@ -89,7 +102,7 @@ <h2>Anchoring Elements Using CSS</h2>
<p>
The CSS anchor positioning
<a
href="https://w3c.github.io/csswg-drafts/css-anchor/"
href="https://w3c.github.io/csswg-drafts/css-anchor-position/"
target="_blank"
rel="noopener noreferrer"
>specification</a
Expand Down Expand Up @@ -558,6 +571,35 @@ <h2>

#my-target-size {
width: anchor-size(--my-anchor width);
}</code></pre>
</section>
<section id="anchor-update" class="demo-item">
<h2>
<a href="#anchor-update" aria-hidden="true">🔗</a>
Dynamically update anchors
</h2>
<div style="position: relative" class="demo-elements">
<div id="my-anchor-update" class="anchor">Anchor</div>
<div id="my-target-update" class="target">Target</div>
</div>
<button id="toggle-anchor-width">Toggle anchor width</button>
<p class="note">
With polyfill applied: Target and Anchor's right edges line up. Target's
top edge lines up with the bottom edge of the Anchor.
<br />
<br />
When Anchor width is changed dynamically, Target position updates
accordingly.
</p>
<pre><code class="language-css"
>#my-anchor-update {
anchor-name: --my-anchor-update;
}

#my-target-update {
position: absolute;
right: anchor(--my-anchor-update right);
top: anchor(--my-anchor-update bottom);
}</code></pre>
</section>
<footer>
Expand Down
17 changes: 17 additions & 0 deletions public/anchor-update.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#my-anchor-update {
anchor-name: --my-anchor-update;
}

#my-anchor-update[data-small] {
width: 100px;
}

#my-anchor-update[data-large] {
width: 400px;
}

#my-target-update {
position: absolute;
right: anchor(--my-anchor-update right);
top: anchor(--my-anchor-update bottom);
}
8 changes: 8 additions & 0 deletions src/@types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {};

declare global {
interface Window {
UPDATE_ANCHOR_ON_ANIMATION_FRAME?: boolean;
CHECK_LAYOUT_DELAY?: boolean;
}
}
6 changes: 3 additions & 3 deletions src/index-wpt.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { polyfill } from './polyfill.js';

// @ts-expect-error Used by the WPT test harness to delay test assertions
// Used by the WPT test harness to delay test assertions
// and give the polyfill time to apply changes
window.CHECK_LAYOUT_DELAY = true;

// apply polyfill
if (document.readyState !== 'complete') {
window.addEventListener('load', () => {
polyfill();
polyfill(true);
});
} else {
polyfill();
polyfill(true);
}
146 changes: 84 additions & 62 deletions src/polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,10 @@ export const getPixelValue = async ({
return fallback;
};

async function applyAnchorPositions(declarations: AnchorFunctionDeclaration) {
async function applyAnchorPositions(
declarations: AnchorFunctionDeclaration,
useAnimationFrame = false,
) {
const root = document.documentElement;

for (const [property, anchorValues] of Object.entries(declarations) as [
Expand All @@ -287,22 +290,27 @@ async function applyAnchorPositions(declarations: AnchorFunctionDeclaration) {
const anchor = anchorValue.anchorEl;
const target = anchorValue.targetEl;
if (anchor && target) {
autoUpdate(anchor, target, async () => {
const rects = await platform.getElementRects({
reference: anchor,
floating: target,
strategy: 'absolute',
});
const resolved = await getPixelValue({
targetEl: target,
targetProperty: property,
anchorRect: rects.reference,
anchorSide: anchorValue.anchorSide,
anchorSize: anchorValue.anchorSize,
fallback: anchorValue.fallbackValue,
});
root.style.setProperty(anchorValue.uuid, resolved);
});
autoUpdate(
anchor,
target,
async () => {
const rects = await platform.getElementRects({
reference: anchor,
floating: target,
strategy: 'absolute',
});
const resolved = await getPixelValue({
targetEl: target,
targetProperty: property,
anchorRect: rects.reference,
anchorSide: anchorValue.anchorSide,
anchorSize: anchorValue.anchorSize,
fallback: anchorValue.fallbackValue,
});
root.style.setProperty(anchorValue.uuid, resolved);
},
{ animationFrame: useAnimationFrame },
);
} else {
// Use fallback value
const resolved = await getPixelValue({
Expand All @@ -320,6 +328,7 @@ async function applyAnchorPositions(declarations: AnchorFunctionDeclaration) {
async function applyPositionFallbacks(
targetSel: string,
fallbacks: TryBlock[],
useAnimationFrame = false,
) {
if (!fallbacks.length) {
return;
Expand All @@ -330,65 +339,78 @@ async function applyPositionFallbacks(
for (const target of targets) {
let checking = false;
const offsetParent = await getOffsetParent(target);
autoUpdate(offsetParent, target, async () => {
// If this auto-update was triggered while the polyfill is already looping
// through the possible `@try` blocks, do not check again.
if (checking) {
return;
}
checking = true;
// Apply the styles from each `@try` block (in order), stopping when we
// reach one that does not cause the target's margin-box to overflow
// its offsetParent (containing block).
for (const [index, { uuid }] of fallbacks.entries()) {
target.setAttribute('data-anchor-polyfill', uuid);
if (index === fallbacks.length - 1) {
checking = false;
break;
autoUpdate(
target,
target,
async () => {
// If this auto-update was triggered while the polyfill is already looping
// through the possible `@try` blocks, do not check again.
if (checking) {
return;
}
const rects = await platform.getElementRects({
reference: offsetParent,
floating: target,
strategy: 'absolute',
});
const overflow = await detectOverflow(
{
x: target.offsetLeft,
y: target.offsetTop,
platform: platformWithCache,
rects,
elements: { floating: target },
checking = true;
// Apply the styles from each `@try` block (in order), stopping when we
// reach one that does not cause the target's margin-box to overflow
// its offsetParent (containing block).
for (const [index, { uuid }] of fallbacks.entries()) {
target.setAttribute('data-anchor-polyfill', uuid);
if (index === fallbacks.length - 1) {
checking = false;
break;
}
const rects = await platform.getElementRects({
reference: target,
floating: target,
strategy: 'absolute',
} as unknown as MiddlewareState,
{
boundary: offsetParent,
rootBoundary: 'document',
padding: getMargins(target),
},
);
// If none of the sides overflow, use this `@try` block and stop loop...
if (Object.values(overflow).every((side) => side <= 0)) {
checking = false;
break;
});
const overflow = await detectOverflow(
{
x: target.offsetLeft,
y: target.offsetTop,
platform: platformWithCache,
rects,
elements: { floating: target },
strategy: 'absolute',
} as unknown as MiddlewareState,
{
boundary: offsetParent,
rootBoundary: 'document',
padding: getMargins(target),
},
);
// If none of the sides overflow, use this `@try` block and stop loop...
if (Object.values(overflow).every((side) => side <= 0)) {
checking = false;
break;
}
}
}
});
},
{ animationFrame: useAnimationFrame },
);
}
}

async function position(rules: AnchorPositions) {
async function position(rules: AnchorPositions, useAnimationFrame = false) {
for (const pos of Object.values(rules)) {
// Handle `anchor()` and `anchor-size()` functions...
await applyAnchorPositions(pos.declarations ?? {});
await applyAnchorPositions(pos.declarations ?? {}, useAnimationFrame);
}

for (const [targetSel, position] of Object.entries(rules)) {
// Handle `@position-fallback` blocks...
await applyPositionFallbacks(targetSel, position.fallbacks ?? []);
await applyPositionFallbacks(
targetSel,
position.fallbacks ?? [],
useAnimationFrame,
);
}
}

export async function polyfill() {
export async function polyfill(animationFrame?: boolean) {
const useAnimationFrame =
animationFrame === undefined
? Boolean(window.UPDATE_ANCHOR_ON_ANIMATION_FRAME)
: animationFrame;
// fetch CSS from stylesheet and inline style
const styleData = await fetchCSS();

Expand All @@ -400,7 +422,7 @@ export async function polyfill() {
await transformCSS(styleData, inlineStyles);

// calculate position values
await position(rules);
await position(rules, useAnimationFrame);
}

return rules;
Expand Down
Loading