Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@oddbird/css-anchor-positioning

Package Overview
Dependencies
Maintainers
0
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@oddbird/css-anchor-positioning - npm Package Compare versions

Comparing version 0.1.0 to 0.1.1

dist/dom.d.ts

12

dist/cascade.d.ts
import { type StyleData } from './utils.js';
export declare function cascadeCSS(styleData: StyleData[]): Promise<boolean>;
/**
* Map of CSS property to CSS custom property that the property's value is
* shifted into. This is used to subject properties that are not yet natively
* supported to the CSS cascade and inheritance rules.
*/
export declare const SHIFTED_PROPERTIES: Record<string, string>;
/**
* Update the given style data to enable cascading and inheritance of properties
* that are not yet natively supported.
*/
export declare function cascadeCSS(styleData: StyleData[]): boolean;

10

dist/parse.d.ts

@@ -1,3 +0,4 @@

import * as csstree from 'css-tree';
import { type DeclarationWithValue, type StyleData } from './utils.js';
import { type PseudoElement, type Selector } from './dom.js';
import { type StyleData } from './utils.js';
type AnchorSelectors = Record<string, Selector[]>;
export type InsetProperty = 'top' | 'left' | 'right' | 'bottom' | 'inset-block-start' | 'inset-block-end' | 'inset-inline-start' | 'inset-inline-end' | 'inset-block' | 'inset-inline' | 'inset';

@@ -11,3 +12,3 @@ export type SizingProperty = 'width' | 'height' | 'min-width' | 'min-height' | 'max-width' | 'max-height';

targetEl?: HTMLElement | null;
anchorEl?: HTMLElement | null;
anchorEl?: HTMLElement | PseudoElement | null;
anchorName?: string;

@@ -33,8 +34,7 @@ anchorSide?: AnchorSide;

export declare function isBoxAlignmentProp(property: string): property is BoxAlignmentProperty;
export declare function isPositionAnchorDeclaration(node: csstree.CssNode): node is DeclarationWithValue;
export declare function getCSSPropertyValue(el: HTMLElement, prop: string): string;
export declare function parseCSS(styleData: StyleData[]): Promise<{
rules: AnchorPositions;
inlineStyles: Map<HTMLElement, Record<string, string>>;
anchorScopes: AnchorSelectors;
}>;
export {};

@@ -7,2 +7,3 @@ import * as csstree from 'css-tree';

export declare function generateCSS(ast: csstree.CssNode): string;
export declare function isDeclaration(node: csstree.CssNode): node is DeclarationWithValue;
export declare function getDeclarationValue(node: DeclarationWithValue): string;

@@ -15,2 +16,1 @@ export interface StyleData {

}
export declare const POSITION_ANCHOR_PROPERTY: string;

@@ -0,6 +1,8 @@

import { type Selector } from './dom.js';
/**
* Validates that el is a acceptable anchor element for an absolutely positioned element query el
* Validates that el is a acceptable anchor element for an absolutely positioned
* element query el
* https://drafts.csswg.org/css-anchor-position-1/#acceptable-anchor-element
*/
export declare function isAcceptableAnchorElement(el: HTMLElement, queryEl: HTMLElement): Promise<boolean>;
export declare function isAcceptableAnchorElement(el: HTMLElement, anchorName: string | null, queryEl: HTMLElement, scopeSelector: string | null): Promise<boolean>;
/**

@@ -12,2 +14,2 @@ * Given a target element and CSS selector(s) for potential anchor element(s),

*/
export declare function validatedForPositioning(targetEl: HTMLElement | null, anchorSelectors: string[]): Promise<HTMLElement | null>;
export declare function validatedForPositioning(targetEl: HTMLElement | null, anchorName: string | null, anchorSelectors: Selector[], scopeSelectors: Selector[]): Promise<HTMLElement | import("./dom.js").PseudoElement | null>;
{
"name": "@oddbird/css-anchor-positioning",
"version": "0.1.0",
"version": "0.1.1",
"description": "Polyfill for the proposed CSS anchor positioning spec",

@@ -5,0 +5,0 @@ "license": "BSD-3-Clause",

@@ -86,3 +86,3 @@ # CSS Anchor Positioning Polyfill

- The `position-try-options`, `position-try-order`, or `position-try` properties
- `anchor-scope` property
- `anchor-scope` property on pseudo-elements
- `inset-area` property

@@ -89,0 +89,0 @@ - `anchor-center` value for `justify-self`, `align-self`, `justify-items`, and

import * as csstree from 'css-tree';
import { nanoid } from 'nanoid/non-secure';
import { isPositionAnchorDeclaration } from './parse.js';
import {
generateCSS,
getAST,
getDeclarationValue,
POSITION_ANCHOR_PROPERTY,
type StyleData,
} from './utils.js';
import { generateCSS, getAST, isDeclaration, 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) {
/**
* Map of CSS property to CSS custom property that the property's value is
* shifted into. This is used to subject properties that are not yet natively
* supported to the CSS cascade and inheritance rules.
*/
export const SHIFTED_PROPERTIES: Record<string, string> = {
'position-anchor': `--position-anchor-${nanoid(12)}`,
'anchor-scope': `--anchor-scope-${nanoid(12)}`,
'anchor-name': `--anchor-name-${nanoid(12)}`,
};
/**
* Shift property declarations for properties that are not yet natively
* supported into custom properties.
*/
function shiftUnsupportedProperties(
node: csstree.CssNode,
block?: csstree.Block,
) {
if (isDeclaration(node) && SHIFTED_PROPERTIES[node.property] && block) {
block.children.appendData({
type: 'Declaration',
important: false,
property: POSITION_ANCHOR_PROPERTY,
value: {
type: 'Raw',
value: getDeclarationValue(node),
},
...node,
property: SHIFTED_PROPERTIES[node.property],
});

@@ -30,3 +35,7 @@ return { updated: true };

export async function cascadeCSS(styleData: StyleData[]) {
/**
* Update the given style data to enable cascading and inheritance of properties
* that are not yet natively supported.
*/
export function cascadeCSS(styleData: StyleData[]) {
for (const styleObj of styleData) {

@@ -39,3 +48,3 @@ let changed = false;

const block = this.rule?.block;
const { updated } = shiftPositionAnchorData(node, block);
const { updated } = shiftUnsupportedProperties(node, block);
if (updated) {

@@ -42,0 +51,0 @@ changed = true;

@@ -5,2 +5,8 @@ import * as csstree from 'css-tree';

import {
AnchorScopeValue,
getCSSPropertyValue,
type PseudoElement,
type Selector,
} from './dom.js';
import {
type DeclarationWithValue,

@@ -10,3 +16,3 @@ generateCSS,

getDeclarationValue,
POSITION_ANCHOR_PROPERTY,
isDeclaration,
type StyleData,

@@ -21,4 +27,4 @@ } from './utils.js';

// `key` is the `anchor-name` value
// `value` is an array of all element selectors with that anchor name
type AnchorNames = Record<string, string[]>;
// `value` is an array of all selectors associated with that `anchor-name`
type AnchorSelectors = Record<string, Selector[]>;

@@ -130,3 +136,3 @@ export type InsetProperty =

targetEl?: HTMLElement | null;
anchorEl?: HTMLElement | null;
anchorEl?: HTMLElement | PseudoElement | null;
anchorName?: string;

@@ -183,6 +189,2 @@ anchorSide?: AnchorSide;

function isDeclaration(node: csstree.CssNode): node is DeclarationWithValue {
return node.type === 'Declaration';
}
function isAnchorNameDeclaration(

@@ -194,2 +196,8 @@ node: csstree.CssNode,

function isAnchorScopeDeclaration(
node: csstree.CssNode,
): node is DeclarationWithValue {
return node.type === 'Declaration' && node.property === 'anchor-scope';
}
function isAnchorFunction(

@@ -261,8 +269,2 @@ node: csstree.CssNode | null,

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

@@ -351,17 +353,34 @@ node: csstree.FunctionNode,

function getAnchorNameData(node: csstree.CssNode, rule?: csstree.Raw) {
if (
isAnchorNameDeclaration(node) &&
!node.value.children.isEmpty &&
rule?.value
) {
return node.value.children.map((item) => {
const { name } = item as csstree.Identifier;
return { name, selector: rule.value };
});
}
return [];
function getAnchorNames(node: DeclarationWithValue) {
return (node.value.children as csstree.List<csstree.Identifier>).map(
({ name }) => name,
);
}
let anchorNames: AnchorNames = {};
function getSelectors(rule: csstree.SelectorList | undefined) {
if (!rule) return [];
return (rule.children as csstree.List<csstree.Selector>)
.map((selector) => {
let pseudoElementPart: string | undefined;
if (selector.children.last?.type === 'PseudoElementSelector') {
selector = csstree.clone(selector) as csstree.Selector;
pseudoElementPart = generateCSS(selector.children.last!);
selector.children.pop();
}
const elementPart = generateCSS(selector);
return {
selector: elementPart + (pseudoElementPart ?? ''),
elementPart,
pseudoElementPart,
} satisfies Selector;
})
.toArray();
}
let anchorNames: AnchorSelectors = {};
let anchorScopes: AnchorSelectors = {};
// Mapping of custom property names, to anchor function data objects referenced

@@ -384,2 +403,3 @@ // in their values

anchorNames = {};
anchorScopes = {};
customPropAssignments = {};

@@ -417,11 +437,7 @@ customPropOriginals = {};

function getPositionFallbackDeclaration(
node: csstree.Declaration,
rule?: csstree.Raw,
) {
if (isFallbackDeclaration(node) && node.value.children.first && rule?.value) {
const name = getDeclarationValue(node);
return { name, selector: rule.value };
function getPositionFallbackDeclaration(node: csstree.Declaration) {
if (isFallbackDeclaration(node) && node.value.children.first) {
return getDeclarationValue(node);
}
return {};
return null;
}

@@ -458,6 +474,2 @@

export function getCSSPropertyValue(el: HTMLElement, prop: string) {
return getComputedStyle(el).getPropertyValue(prop).trim();
}
async function getAnchorEl(

@@ -473,3 +485,3 @@ targetEl: HTMLElement | null,

targetEl,
POSITION_ANCHOR_PROPERTY,
'position-anchor',
);

@@ -482,9 +494,25 @@

} else if (anchorAttr) {
return await validatedForPositioning(targetEl, [
`#${CSS.escape(anchorAttr)}`,
]);
const elementPart = `#${CSS.escape(anchorAttr)}`;
return await validatedForPositioning(
targetEl,
null,
[{ selector: elementPart, elementPart }],
[],
);
}
}
const anchorSelectors = anchorName ? anchorNames[anchorName] ?? [] : [];
return await validatedForPositioning(targetEl, anchorSelectors);
const anchorSelectors = anchorName ? anchorNames[anchorName] || [] : [];
const allScopeSelectors = anchorName
? anchorScopes[AnchorScopeValue.All] || []
: [];
const anchorNameScopeSelectors = anchorName
? anchorScopes[anchorName] || []
: [];
return await validatedForPositioning(
targetEl,
anchorName || null,
anchorSelectors,
[...allScopeSelectors, ...anchorNameScopeSelectors],
);
}

@@ -530,10 +558,15 @@

enter(node) {
const rule = this.rule?.prelude as csstree.Raw | undefined;
const rule = this.rule?.prelude as csstree.SelectorList | undefined;
const selectors = getSelectors(rule);
// Parse `position-fallback` declaration
const { name, selector } = getPositionFallbackDeclaration(node, rule);
if (name && selector && fallbacks[name]) {
validPositions[selector] = { fallbacks: fallbacks[name].blocks };
if (!fallbacks[name].targets.includes(selector)) {
fallbacks[name].targets.push(selector);
const name = getPositionFallbackDeclaration(node);
if (name && selectors.length && fallbacks[name]) {
for (const { selector } of selectors) {
validPositions[selector] = { fallbacks: fallbacks[name].blocks };
if (!fallbacks[name].targets.includes(selector)) {
fallbacks[name].targets.push(selector);
}
}
// Add each `@try` block, scoped to a unique data-attr

@@ -564,3 +597,5 @@ for (const block of fallbacks[name].blocks) {

// Store mapping of data-attr to target selector
fallbackTargets[dataAttr] = selector;
fallbackTargets[dataAttr] = selectors
.map(({ selector }) => selector)
.join(', ');
}

@@ -582,18 +617,21 @@ changed = true;

csstree.walk(ast, function (node) {
const rule = this.rule?.prelude as csstree.Raw | undefined;
const rule = this.rule?.prelude as csstree.SelectorList | undefined;
const selectors = getSelectors(rule);
// Parse `anchor-name` declaration
const anchorNameData = getAnchorNameData(node, rule);
anchorNameData.forEach(
({ name: anchorName, selector: anchorSelector }) => {
if (anchorName && anchorSelector) {
if (anchorNames[anchorName]) {
anchorNames[anchorName].push(anchorSelector);
} else {
anchorNames[anchorName] = [anchorSelector];
}
}
},
);
if (isAnchorNameDeclaration(node) && selectors.length) {
for (const name of getAnchorNames(node)) {
anchorNames[name] ??= [];
anchorNames[name].push(...selectors);
}
}
// Parse `anchor-scope` declarations
if (isAnchorScopeDeclaration(node) && selectors.length) {
for (const name of getAnchorNames(node)) {
anchorScopes[name] ??= [];
anchorScopes[name].push(...selectors);
}
}
// Parse `anchor()` function

@@ -605,3 +643,3 @@ const {

} = getAnchorFunctionData(node, this.declaration);
if (prop && data && rule?.value) {
if (prop && data && selectors.length) {
// This will override earlier declarations

@@ -612,6 +650,8 @@ // with the same exact rule selector

// for the same `.foo {...}` selector)
anchorFunctions[rule.value] = {
...anchorFunctions[rule.value],
[prop]: [...(anchorFunctions[rule.value]?.[prop] ?? []), data],
};
for (const { selector } of selectors) {
anchorFunctions[selector] = {
...anchorFunctions[selector],
[prop]: [...(anchorFunctions[selector]?.[prop] ?? []), data],
};
}
}

@@ -669,7 +709,7 @@ if (updated) {

enter(node) {
const rule = this.rule?.prelude as csstree.Raw | undefined;
const rule = this.rule?.prelude as csstree.SelectorList | undefined;
const declaration = this.declaration;
const prop = declaration?.property;
if (
rule?.value &&
rule?.children.isEmpty === false &&
isVarFunction(node) &&

@@ -740,7 +780,8 @@ declaration &&

enter(node) {
const rule = this.rule?.prelude as csstree.Raw | undefined;
const rule = this.rule?.prelude as csstree.SelectorList | undefined;
const declaration = this.declaration;
const prop = declaration?.property;
if (
rule?.value &&
rule?.children.isEmpty === false &&
isVarFunction(node) &&

@@ -766,3 +807,2 @@ declaration &&

/*
An anchor (or anchor-size) fn was assigned to an inset (or sizing)

@@ -809,4 +849,3 @@ property.

}
*/
*/
const propUuid = `${prop}-${nanoid(12)}`;

@@ -854,2 +893,4 @@

// to the new uuid...
const selectors = getSelectors(rule);
for (const anchorFnData of [...anchorFns, ...referencedFns]) {

@@ -860,6 +901,9 @@ const data = { ...anchorFnData };

data.uuid = uuidWithProp;
anchorFunctions[rule.value] = {
...anchorFunctions[rule.value],
[prop]: [...(anchorFunctions[rule.value]?.[prop] ?? []), data],
};
for (const { selector } of selectors) {
anchorFunctions[selector] = {
...anchorFunctions[selector],
[prop]: [...(anchorFunctions[selector]?.[prop] ?? []), data],
};
}
// Store new name with declaration prop appended,

@@ -1003,3 +1047,3 @@ // so that we can go back and update the original custom

return { rules: validPositions, inlineStyles };
return { rules: validPositions, inlineStyles, anchorScopes };
}

@@ -10,2 +10,3 @@ import {

import { cascadeCSS } from './cascade.js';
import { getCSSPropertyValue } from './dom.js';
import { fetchCSS } from './fetch.js';

@@ -18,3 +19,2 @@ import {

type AnchorSize,
getCSSPropertyValue,
type InsetProperty,

@@ -82,3 +82,4 @@ isInsetProp,

// This should also check the writing-mode
// See: https://github.com/oddbird/css-anchor-positioning/pull/22#discussion_r966348526
// See:
// https://github.com/oddbird/css-anchor-positioning/pull/22#discussion_r966348526
// https://trello.com/c/KnqCnHx3

@@ -350,4 +351,4 @@ export const getAxis = (position?: string) => {

async () => {
// If this auto-update was triggered while the polyfill is already looping
// through the possible `@try` blocks, do not check again.
// If this auto-update was triggered while the polyfill is already
// looping through the possible `@try` blocks, do not check again.
if (checking) {

@@ -386,3 +387,4 @@ return;

);
// If none of the sides overflow, use this `@try` block and stop loop...
// If none of the sides overflow, use this `@try` block and stop
// loop...
if (Object.values(overflow).every((side) => side <= 0)) {

@@ -389,0 +391,0 @@ checking = false;

import * as csstree from 'css-tree';
import { nanoid } from 'nanoid/non-secure';

@@ -11,3 +10,2 @@ export interface DeclarationWithValue extends csstree.Declaration {

parseAtrulePrelude: false,
parseRulePrelude: false,
parseCustomProperty: true,

@@ -25,2 +23,8 @@ });

export function isDeclaration(
node: csstree.CssNode,
): node is DeclarationWithValue {
return node.type === 'Declaration';
}
export function getDeclarationValue(node: DeclarationWithValue) {

@@ -36,3 +40,1 @@ return (node.value.children.first as csstree.Identifier).name;

}
export const POSITION_ANCHOR_PROPERTY = `--position-anchor-${nanoid(12)}`;
import { platform } from '@floating-ui/dom';
import { getCSSPropertyValue } from './parse.js';
import {
getCSSPropertyValue,
getElementsBySelector,
hasAnchorName,
hasAnchorScope,
hasStyle,
type Selector,
} from './dom.js';
// Given an element and CSS style property,
// checks if the CSS property equals a certain value
function hasStyle(element: HTMLElement, cssProperty: string, value: string) {
return getCSSPropertyValue(element, cssProperty) === value;
}
// Given a target element's containing block (CB) and an anchor element,

@@ -79,3 +80,4 @@ // determines if the anchor element is a descendant of the target CB.

/**
* Validates that el is a acceptable anchor element for an absolutely positioned element query el
* Validates that el is a acceptable anchor element for an absolutely positioned
* element query el
* https://drafts.csswg.org/css-anchor-position-1/#acceptable-anchor-element

@@ -85,3 +87,5 @@ */

el: HTMLElement,
anchorName: string | null,
queryEl: HTMLElement,
scopeSelector: string | null,
) {

@@ -139,4 +143,2 @@ const elContainingBlock = await getContainingBlock(el);

// TODO el is either an element or a part-like pseudo-element.
// el is not in the skipped contents of another element.

@@ -155,3 +157,12 @@ {

// TODO el is in scope for query el, per the effects of anchor-scope on query el or its ancestors.
// el is in scope for query el, per the effects of anchor-scope on query el or
// its ancestors.
if (
anchorName &&
scopeSelector &&
getScope(el, anchorName, scopeSelector) !==
getScope(queryEl, anchorName, scopeSelector)
) {
return false;
}

@@ -161,2 +172,23 @@ return true;

function getScope(
element: HTMLElement,
anchorName: string,
scopeSelector: string,
) {
// Unlike the real `anchor-scope`, our `--anchor-scope` custom property
// inherits. We first check that the element matches the scope selector, so we
// can be guaranteed that the computed value we read was set explicitly, not
// inherited. Then we verify that the specified anchor scope is actually the
// one applied by the CSS cascade.
while (
!(element.matches(scopeSelector) && hasAnchorScope(element, anchorName))
) {
if (!element.parentElement) {
return null;
}
element = element.parentElement;
}
return element;
}
/**

@@ -170,3 +202,5 @@ * Given a target element and CSS selector(s) for potential anchor element(s),

targetEl: HTMLElement | null,
anchorSelectors: string[],
anchorName: string | null,
anchorSelectors: Selector[],
scopeSelectors: Selector[],
) {

@@ -183,10 +217,28 @@ if (

const anchorElements: NodeListOf<HTMLElement> = document.querySelectorAll(
anchorSelectors.join(', '),
);
const anchorElements = anchorSelectors
// Any element that matches a selector that sets the specified `anchor-name`
// could be a potential match.
.flatMap(getElementsBySelector)
// Narrow down the potential match elements to just the ones whose computed
// `anchor-name` matches the specified one. This accounts for the
// `anchor-name` value that was actually applied by the CSS cascade.
.filter((el) => hasAnchorName(el, anchorName));
// TODO: handle anchor-scope for pseudo-elements.
const scopeSelector = scopeSelectors.map((s) => s.selector).join(',') || null;
for (let index = anchorElements.length - 1; index >= 0; index--) {
const anchor = anchorElements[index];
const isPseudoElement = 'fakePseudoElement' in anchor;
if (await isAcceptableAnchorElement(anchor, targetEl)) {
if (
await isAcceptableAnchorElement(
isPseudoElement ? anchor.fakePseudoElement : anchor,
anchorName,
targetEl,
scopeSelector,
)
) {
if (isPseudoElement) anchor.removeFakePseudoElement();
return anchor;

@@ -193,0 +245,0 @@ }

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc