Skip to content

Commit

Permalink
Merge pull request #196 from oddbird/position-anchor
Browse files Browse the repository at this point in the history
Position anchor
  • Loading branch information
jamesnw authored Jun 25, 2024
2 parents 3241736 + e205a22 commit 8af0367
Show file tree
Hide file tree
Showing 16 changed files with 361 additions and 53 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ module.exports = {
'simple-import-sort/imports': 1,
'no-console': 1,
'no-warning-comments': [1, { terms: ['todo', 'fixme', '@@@'] }],
'@typescript-eslint/consistent-type-imports': [
1,
{ fixStyle: 'inline-type-imports' },
],
},
};
6 changes: 5 additions & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
"property-no-unknown": [
true,
{
"ignoreProperties": ["anchor-name", "position-fallback"]
"ignoreProperties": [
"anchor-name",
"position-anchor",
"position-fallback"
]
}
]
}
Expand Down
46 changes: 46 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<link rel="stylesheet" href="/anchor.css" />
<link rel="stylesheet" href="/anchor-positioning.css" />
<link rel="stylesheet" href="/anchor-popover.css" />
<link rel="stylesheet" href="/position-anchor.css" />
<link rel="stylesheet" href="/position-fallback.css" />
<link rel="stylesheet" href="/anchor-scroll.css" />
<link rel="stylesheet" href="/anchor-size.css" />
Expand Down Expand Up @@ -308,6 +309,51 @@ <h2>
position: absolute;
right: anchor(implicit left);
bottom: anchor(top);
}</code></pre>
</section>
<section id="position-anchor" class="demo-item">
<h2>
<a href="#position-anchor" aria-hidden="true">🔗</a>
Positioning with <code>anchor()</code> [
<code>position-anchor</code> property]
</h2>
<div style="position: relative" class="demo-elements">
<div id="my-position-anchor-a" class="anchor">Anchor A</div>
<div id="my-position-target-a" class="target">Target A</div>
<div id="my-position-anchor-b" class="anchor">Anchor B</div>
<div id="my-position-target-b" class="target">Target B</div>
</div>
<p class="note">
With polyfill applied: Targets are positioned at the top right corner of
their respective Anchors.
</p>
<pre><code class="language-html"
>&lt;div id="my-position-anchor-a" class="anchor"&gt;Anchor A&lt;/div&gt;
&lt;div id="my-position-target-a" class="target"&gt;Target A&lt;/div&gt;
&lt;div id="my-position-anchor-b" class="anchor"&gt;Anchor B&lt;/div&gt;
&lt;div id="my-position-target-b" class="target"&gt;Target B&lt;/div&gt;</code>

<code class="language-css"
>#my-position-target-a {
position-anchor: --my-position-anchor-a;
}

#my-position-target-b {
position-anchor: --my-position-anchor-b;
}

.target {
position: absolute;
bottom: anchor(top);
left: anchor(right);
}

#my-position-anchor-a {
anchor-name: --my-position-anchor-a;
}

#my-position-anchor-b {
anchor-name: --my-position-anchor-b;
}</code></pre>
</section>
<section id="anchor-positioning-popover" class="demo-item">
Expand Down
19 changes: 19 additions & 0 deletions public/position-anchor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#position-anchor #my-position-target-b {
position-anchor: --my-position-anchor-b;
}

#position-anchor .target {
position: absolute;
bottom: anchor(top);
left: anchor(right);
position-anchor: --my-position-anchor-a;
}

#my-position-anchor-a {
anchor-name: --my-position-anchor-a;
margin-bottom: 3em;
}

#my-position-anchor-b {
anchor-name: --my-position-anchor-b;
}
52 changes: 52 additions & 0 deletions src/cascade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as csstree from 'css-tree';

import { isPositionAnchorDeclaration } from './parse.js';
import {
generateCSS,
getAST,
getDeclarationValue,
POSITION_ANCHOR_PROPERTY,
type StyleData,
} from './utils.js';

// Move `position-anchor` declaration to cascadable `--position-anchor`
// property.
function shiftPositionAnchorData(node: csstree.CssNode, block?: csstree.Block) {
if (isPositionAnchorDeclaration(node) && block) {
block.children.appendData({
type: 'Declaration',
important: false,
property: POSITION_ANCHOR_PROPERTY,
value: {
type: 'Raw',
value: getDeclarationValue(node),
},
});
return { updated: true };
}
return {};
}

export async function cascadeCSS(styleData: StyleData[]) {
for (const styleObj of styleData) {
let changed = false;
const ast = getAST(styleObj.css);
csstree.walk(ast, {
visit: 'Declaration',
enter(node) {
const block = this.rule?.block;
const { updated } = shiftPositionAnchorData(node, block);
if (updated) {
changed = true;
}
},
});

if (changed) {
// Update CSS
styleObj.css = generateCSS(ast);
styleObj.changed = true;
}
}
return styleData.some((styleObj) => styleObj.changed === true);
}
7 changes: 1 addition & 6 deletions src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { nanoid } from 'nanoid/non-secure';

export interface StyleData {
el: HTMLElement;
css: string;
url?: URL;
changed?: boolean;
}
import { type StyleData } from './utils.js';

export function isStyleLink(link: HTMLLinkElement): link is HTMLLinkElement {
return Boolean(
Expand Down
65 changes: 36 additions & 29 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import * as csstree from 'css-tree';
import { nanoid } from 'nanoid/non-secure';

import { StyleData } from './fetch.js';
import {
type DeclarationWithValue,
generateCSS,
getAST,
getDeclarationValue,
POSITION_ANCHOR_PROPERTY,
type StyleData,
} from './utils.js';
import { validatedForPositioning } from './validate.js';

interface DeclarationWithValue extends csstree.Declaration {
value: csstree.Value;
}

interface AtRuleRaw extends csstree.Atrule {
prelude: csstree.Raw | null;
}
Expand Down Expand Up @@ -249,6 +252,12 @@ export function isBoxAlignmentProp(
return BOX_ALIGNMENT_PROPS.includes(property as BoxAlignmentProperty);
}

export function isPositionAnchorDeclaration(
node: csstree.CssNode,
): node is DeclarationWithValue {
return node.type === 'Declaration' && node.property === 'position-anchor';
}

function parseAnchorFn(
node: csstree.FunctionNode,
replaceCss?: boolean,
Expand All @@ -263,7 +272,7 @@ function parseAnchorFn(
const args: csstree.CssNode[] = [];
node.children.toArray().forEach((child) => {
if (foundComma) {
fallbackValue = `${fallbackValue}${csstree.generate(child)}`;
fallbackValue = `${fallbackValue}${generateCSS(child)}`;
return;
}
if (child.type === 'Operator' && child.value === ',') {
Expand Down Expand Up @@ -375,7 +384,7 @@ function getAnchorFunctionData(
) {
if ((isAnchorFunction(node) || isAnchorSizeFunction(node)) && declaration) {
if (declaration.property.startsWith('--')) {
const original = csstree.generate(declaration.value);
const original = generateCSS(declaration.value);
const data = parseAnchorFn(node, true);
// Store the original anchor function so that we can restore it later
customPropOriginals[data.uuid] = original;
Expand All @@ -401,7 +410,7 @@ function getPositionFallbackDeclaration(
rule?: csstree.Raw,
) {
if (isFallbackDeclaration(node) && node.value.children.first && rule?.value) {
const name = (node.value.children.first as csstree.Identifier).name;
const name = getDeclarationValue(node);
return { name, selector: rule.value };
}
return {};
Expand All @@ -425,7 +434,7 @@ function getPositionFallbackRules(node: csstree.Atrule) {
const tryBlock: TryBlock = {
uuid: `${name}-try-${nanoid(12)}`,
declarations: Object.fromEntries(
declarations.map((d) => [d.property, csstree.generate(d.value)]),
declarations.map((d) => [d.property, generateCSS(d.value)]),
),
};
tryBlocks.push(tryBlock);
Expand All @@ -448,7 +457,14 @@ async function getAnchorEl(
const customPropName = anchorObj.customPropName;
if (targetEl && !anchorName) {
const anchorAttr = targetEl.getAttribute('anchor');
if (customPropName) {
const positionAnchorProperty = getCSSPropertyValue(
targetEl,
POSITION_ANCHOR_PROPERTY,
);

if (positionAnchorProperty) {
anchorName = positionAnchorProperty;
} else if (customPropName) {
anchorName = getCSSPropertyValue(targetEl, customPropName);
} else if (anchorAttr) {
return await validatedForPositioning(targetEl, [
Expand All @@ -460,16 +476,6 @@ async function getAnchorEl(
return await validatedForPositioning(targetEl, anchorSelectors);
}

function getAST(cssText: string) {
const ast = csstree.parse(cssText, {
parseAtrulePrelude: false,
parseRulePrelude: false,
parseCustomProperty: true,
});

return ast;
}

export async function parseCSS(styleData: StyleData[]) {
const anchorFunctions: AnchorFunctionDeclarations = {};
const fallbackTargets: FallbackTargets = {};
Expand Down Expand Up @@ -549,7 +555,7 @@ export async function parseCSS(styleData: StyleData[]) {
});
if (changed) {
// Update CSS
styleObj.css = csstree.generate(ast);
styleObj.css = generateCSS(ast);
styleObj.changed = true;
}
}
Expand Down Expand Up @@ -597,7 +603,7 @@ export async function parseCSS(styleData: StyleData[]) {
});
if (changed) {
// Update CSS
styleObj.css = csstree.generate(ast);
styleObj.css = generateCSS(ast);
styleObj.changed = true;
}
}
Expand Down Expand Up @@ -673,7 +679,7 @@ export async function parseCSS(styleData: StyleData[]) {
// now being re-assigned to another custom property...
const uuid = `${child.name}-anchor-${nanoid(12)}`;
// Store the original declaration so that we can restore it later
const original = csstree.generate(declaration.value);
const original = generateCSS(declaration.value);
customPropOriginals[uuid] = original;
// Store a mapping of the new property to the original property
// name, as well as the unique uuid(s) temporarily used to replace
Expand All @@ -697,7 +703,7 @@ export async function parseCSS(styleData: StyleData[]) {
});
if (changed) {
// Update CSS
styleObj.css = csstree.generate(ast);
styleObj.css = generateCSS(ast);
styleObj.changed = true;
}
}
Expand Down Expand Up @@ -854,7 +860,7 @@ export async function parseCSS(styleData: StyleData[]) {
});
if (changed) {
// Update CSS
styleObj.css = csstree.generate(ast);
styleObj.css = generateCSS(ast);
styleObj.changed = true;
}
}
Expand Down Expand Up @@ -887,9 +893,10 @@ export async function parseCSS(styleData: StyleData[]) {
property: `${this.declaration.property}-${propUuid}`,
value: {
type: 'Raw',
value: csstree
.generate(this.declaration.value)
.replace(`var(${child.name})`, `var(${value})`),
value: generateCSS(this.declaration.value).replace(
`var(${child.name})`,
`var(${value})`,
),
},
});
changed = true;
Expand All @@ -908,7 +915,7 @@ export async function parseCSS(styleData: StyleData[]) {
});
if (changed) {
// Update CSS
styleObj.css = csstree.generate(ast);
styleObj.css = generateCSS(ast);
styleObj.changed = true;
}
}
Expand Down
22 changes: 14 additions & 8 deletions src/polyfill.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import {
autoUpdate,
detectOverflow,
MiddlewareState,
type MiddlewareState,
platform,
type Rect,
} from '@floating-ui/dom';

import { cascadeCSS } from './cascade.js';
import { fetchCSS } from './fetch.js';
import {
AnchorFunction,
AnchorFunctionDeclaration,
type AnchorFunction,
type AnchorFunctionDeclaration,
type AnchorPositions,
type AnchorSide,
type AnchorSize,
getCSSPropertyValue,
InsetProperty,
type InsetProperty,
isInsetProp,
isSizingProp,
parseCSS,
SizingProperty,
TryBlock,
type SizingProperty,
type TryBlock,
} from './parse.js';
import { transformCSS } from './transform.js';

Expand Down Expand Up @@ -412,14 +413,19 @@ export async function polyfill(animationFrame?: boolean) {
? Boolean(window.UPDATE_ANCHOR_ON_ANIMATION_FRAME)
: animationFrame;
// fetch CSS from stylesheet and inline style
const styleData = await fetchCSS();
let styleData = await fetchCSS();

// pre parse CSS styles that we need to cascade
const cascadeCausedChanges = await cascadeCSS(styleData);
if (cascadeCausedChanges) {
styleData = await transformCSS(styleData);
}
// parse CSS
const { rules, inlineStyles } = await parseCSS(styleData);

if (Object.values(rules).length) {
// update source code
await transformCSS(styleData, inlineStyles);
await transformCSS(styleData, inlineStyles, true);

// calculate position values
await position(rules, useAnimationFrame);
Expand Down
Loading

0 comments on commit 8af0367

Please sign in to comment.