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

Position anchor #196

Merged
merged 19 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
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 {
jgerigmeyer marked this conversation as resolved.
Show resolved Hide resolved
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
Loading