Skip to content

Commit

Permalink
Merge pull request #87 from oddbird/animation-frame-api
Browse files Browse the repository at this point in the history
Allow updating anchor on every animationFrame.
  • Loading branch information
jgerigmeyer authored Mar 9, 2023
2 parents b179a94 + 6c62eba commit 6222c88
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 74 deletions.
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

0 comments on commit 6222c88

Please sign in to comment.