Skip to content

Commit 21cad6e

Browse files
committed
Add position-anchor
1 parent 29bb583 commit 21cad6e

File tree

2 files changed

+153
-2
lines changed

2 files changed

+153
-2
lines changed

src/parse.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ const ANCHOR_SIDES: AnchorSideKeyword[] = [
100100
'center',
101101
];
102102

103+
const POSITION_ANCHOR_DATASET_KEY = 'polyfillPositionAnchorName';
103104
export type AnchorSide = AnchorSideKeyword | number;
104105

105106
export type AnchorSize =
@@ -221,6 +222,10 @@ function isIdentifier(node: csstree.CssNode): node is csstree.Identifier {
221222
return Boolean(node.type === 'Identifier' && node.name);
222223
}
223224

225+
function getDeclarationValue(node: DeclarationWithValue) {
226+
return (node.value.children.first as csstree.Identifier).name;
227+
}
228+
224229
function isPercentage(node: csstree.CssNode): node is csstree.Percentage {
225230
return Boolean(node.type === 'Percentage' && node.value);
226231
}
@@ -249,6 +254,12 @@ export function isBoxAlignmentProp(
249254
return BOX_ALIGNMENT_PROPS.includes(property as BoxAlignmentProperty);
250255
}
251256

257+
function isPositionAnchorDeclaration(
258+
node: csstree.CssNode,
259+
): node is DeclarationWithValue {
260+
return node.type === 'Declaration' && node.property === 'position-anchor';
261+
}
262+
252263
function parseAnchorFn(
253264
node: csstree.FunctionNode,
254265
replaceCss?: boolean,
@@ -338,7 +349,7 @@ function getAnchorNameData(node: csstree.CssNode, rule?: csstree.Raw) {
338349
node.value.children.first &&
339350
rule?.value
340351
) {
341-
const name = (node.value.children.first as csstree.Identifier).name;
352+
const name = getDeclarationValue(node);
342353
return { name, selector: rule.value };
343354
}
344355
return {};
@@ -394,12 +405,25 @@ function getAnchorFunctionData(
394405
return {};
395406
}
396407

408+
function getPositionAnchorData(node: csstree.CssNode, rule?: csstree.Raw) {
409+
if (isPositionAnchorDeclaration(node) && rule?.value) {
410+
const targets = document.querySelectorAll(rule.value);
411+
const name = getDeclarationValue(node);
412+
413+
for (const targetEl of targets) {
414+
(targetEl as HTMLElement).dataset[POSITION_ANCHOR_DATASET_KEY] = name;
415+
}
416+
return { name, selector: rule.value };
417+
}
418+
return {};
419+
}
420+
397421
function getPositionFallbackDeclaration(
398422
node: csstree.Declaration,
399423
rule?: csstree.Raw,
400424
) {
401425
if (isFallbackDeclaration(node) && node.value.children.first && rule?.value) {
402-
const name = (node.value.children.first as csstree.Identifier).name;
426+
const name = getDeclarationValue(node);
403427
return { name, selector: rule.value };
404428
}
405429
return {};
@@ -452,6 +476,8 @@ async function getAnchorEl(
452476
return await validatedForPositioning(targetEl, [
453477
`#${CSS.escape(anchorAttr)}`,
454478
]);
479+
} else if (targetEl.dataset[POSITION_ANCHOR_DATASET_KEY]) {
480+
anchorName = targetEl.dataset[POSITION_ANCHOR_DATASET_KEY];
455481
}
456482
}
457483
const anchorSelectors = anchorName ? anchorNames[anchorName] ?? [] : [];
@@ -571,6 +597,17 @@ export async function parseCSS(styleData: StyleData[]) {
571597
}
572598
}
573599

600+
// Parse `position-anchor` data
601+
const { name: positionAnchorName, selector: positionAnchorSelector } =
602+
getPositionAnchorData(node, rule);
603+
if (positionAnchorName && positionAnchorSelector) {
604+
if (anchorNames[positionAnchorName]) {
605+
anchorNames[positionAnchorName].push(positionAnchorSelector);
606+
} else {
607+
anchorNames[positionAnchorName] = [positionAnchorSelector];
608+
}
609+
}
610+
574611
// Parse `anchor()` function
575612
const {
576613
prop,

tests/unit/parse.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,120 @@ describe('parseCSS', () => {
163163
expect(rules).toEqual(expected);
164164
});
165165

166+
it('parses `position-anchor` on different selector', async () => {
167+
document.body.innerHTML = `
168+
<div style="position: relative">
169+
<div id="my-anchor"></div>
170+
<div id="my-target-1" class="my-targets"></div>
171+
<div id="my-target-2" class="my-targets"></div>
172+
</div>`;
173+
const css = `
174+
#my-target-1 {
175+
top: anchor(bottom);
176+
}
177+
#my-target-2 {
178+
bottom: anchor(top);
179+
}
180+
.my-targets {
181+
position: absolute;
182+
position-anchor: --my-anchor;
183+
}
184+
#my-anchor {
185+
anchor-name: --my-anchor;
186+
}
187+
`;
188+
document.head.innerHTML = `<style>${css}</style>`;
189+
const { rules } = await parseCSS([{ css }] as StyleData[]);
190+
const expected = {
191+
'#my-target-1': {
192+
declarations: {
193+
top: [
194+
{
195+
anchorName: undefined,
196+
anchorEl: document.getElementById('my-anchor'),
197+
targetEl: document.getElementById('my-target-1'),
198+
anchorSide: 'bottom',
199+
fallbackValue: '0px',
200+
uuid: expect.any(String),
201+
},
202+
],
203+
},
204+
},
205+
'#my-target-2': {
206+
declarations: {
207+
bottom: [
208+
{
209+
anchorName: undefined,
210+
anchorEl: document.getElementById('my-anchor'),
211+
targetEl: document.getElementById('my-target-2'),
212+
anchorSide: 'top',
213+
fallbackValue: '0px',
214+
uuid: expect.any(String),
215+
},
216+
],
217+
},
218+
},
219+
};
220+
expect(rules).toEqual(expected);
221+
});
222+
223+
it('parses `position-anchor` declared multiple times', async () => {
224+
document.body.innerHTML = `
225+
<div style="position: relative">
226+
<div id="my-anchor"></div>
227+
<div id="my-target-1"></div>
228+
<div id="my-target-2"></div>
229+
</div>`;
230+
const css = `
231+
#my-target-1 {
232+
top: anchor(bottom);
233+
position-anchor: --my-anchor;
234+
position: absolute;
235+
}
236+
#my-target-2 {
237+
bottom: anchor(top);
238+
position-anchor: --my-anchor;
239+
position: absolute;
240+
}
241+
#my-anchor {
242+
anchor-name: --my-anchor;
243+
}
244+
`;
245+
document.head.innerHTML = `<style>${css}</style>`;
246+
const { rules } = await parseCSS([{ css }] as StyleData[]);
247+
const expected = {
248+
'#my-target-1': {
249+
declarations: {
250+
top: [
251+
{
252+
anchorName: undefined,
253+
anchorEl: document.getElementById('my-anchor'),
254+
targetEl: document.getElementById('my-target-1'),
255+
anchorSide: 'bottom',
256+
fallbackValue: '0px',
257+
uuid: expect.any(String),
258+
},
259+
],
260+
},
261+
},
262+
'#my-target-2': {
263+
declarations: {
264+
bottom: [
265+
{
266+
anchorName: undefined,
267+
anchorEl: document.getElementById('my-anchor'),
268+
targetEl: document.getElementById('my-target-2'),
269+
anchorSide: 'top',
270+
fallbackValue: '0px',
271+
uuid: expect.any(String),
272+
},
273+
],
274+
},
275+
},
276+
};
277+
expect(rules).toEqual(expected);
278+
});
279+
166280
it('handles duplicate anchor-names', async () => {
167281
document.body.innerHTML =
168282
'<div style="position: relative"><div id="f1"></div><div id="a2"></div></div>';

0 commit comments

Comments
 (0)