Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ value of `window.ANCHOR_POSITIONING_POLYFILL_OPTIONS`.
window.ANCHOR_POSITIONING_POLYFILL_OPTIONS = {
elements: undefined,
excludeInlineStyles: false,
positionAreaContainingBlock: true,
roots: [document],
useAnimationFrame: false,
};
Expand All @@ -122,6 +123,7 @@ an argument.
polyfill({
elements: undefined,
excludeInlineStyles: false,
positionAreaContainingBlock: true,
roots: [document],
useAnimationFrame: false,
});
Expand Down Expand Up @@ -149,6 +151,30 @@ defined. When set to `true`, elements with eligible inline styles listed in the
`elements` option will still be polyfilled, but no other elements in the
document will be implicitly polyfilled.

### positionAreaContainingBlock

type: `boolean | 'auto'`, default: `true`

By default, `position-area` is polyfilled by wrapping the target with a
`<polyfill-position-area>` element that approximates the containing block
created by `position-area`. When set to `false`, no wrapper element is added,
and the polyfill computes and applies inset values on the target itself
instead. This avoids breaking selectors that rely on a direct relationship
with the target (for instance `> target` or `:nth-child()`), but comes with
its own trade-offs. See [Limitations](#limitations) for the differences
between the two approaches.

When set to `'auto'`, the choice is made per target: the wrapper is added only
for targets whose styles resolve against the containing block — percentage
sizes (`width`, `height`, `min-*`, `max-*`), `stretch`/`-webkit-fill-available`
sizes, percentage or `auto` margins, percentage padding, or `stretch`/
`anchor-center` self-alignment. Every other target is positioned directly,
without a wrapper. This keeps the wrapper (and its selector trade-offs) only
where it is needed to preserve correct sizing. The detection is conservative
and reads authored values, so targets whose containing-block dependence is only
expressed dynamically (e.g. set via script after the polyfill runs) may not be
detected.

### roots

type: `(Document | HTMLElement | ShadowRoot)[]`, default: `[document]`
Expand Down Expand Up @@ -201,11 +227,26 @@ following features:
which adds a few differences:
- This breaks selectors that rely on a direct relationship with the target,
for instance `~ target`, `+ target`, `> target` or using `:nth` selectors.
Set the [`positionAreaContainingBlock`](#positionareacontainingblock)
option to `false` to position the target directly, without a wrapping
element.
- Overflow alignment is not applied for a target that overflows its
inset-modified containing block but would still fit within its original
containing block. In other words, a polyfilled target may be placed in a
`position-area` grid section outside its containing block, where the
implementation would move the target inside the containing block.
- When the [`positionAreaContainingBlock`](#positionareacontainingblock) option
is set to `false`, `position-area` is polyfilled by computing and applying
inset values on the target element instead of adding a wrapping element,
which adds a few other differences:
- Inset properties (`top`, `left`, etc.) set by the author on the target are
overridden by the polyfill, rather than resolved relative to the
`position-area` containing block.
- Self-alignment properties (`justify-self` and `align-self`) on the target
do not override the alignment derived from `position-area`.
- A target that is center-aligned on an axis is not constrained by the size
of the `position-area` grid area on that axis, and may overflow it.
- Overflow alignment is not applied (same as the default behavior above).

In addition, JS APIs like `CSSPositionTryRule` or `CSS.supports` will not be
polyfilled.
Expand Down
22 changes: 21 additions & 1 deletion position-area.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,20 @@

const btn = document.getElementById('apply-polyfill');

// Add `?no-containing-block` to the URL to apply the polyfill with
// `positionAreaContainingBlock: false`, which positions targets
// directly instead of wrapping them. Add `?auto` to use `'auto'`, which
// wraps only targets whose styles resolve against the containing block.
const params = new URLSearchParams(location.search);
const positionAreaContainingBlock = params.has('no-containing-block')
? false
: params.has('auto')
? 'auto'
: true;

if (!SUPPORTS_ANCHOR_POSITIONING) {
btn.addEventListener('click', () =>
polyfill().then((rules) => {
polyfill({ positionAreaContainingBlock }).then((rules) => {
btn.innerText = 'Polyfill Applied';
btn.setAttribute('disabled', '');
console.log(rules);
Expand Down Expand Up @@ -113,6 +124,15 @@ <h2>Placing elements with <code>position-area</code></h2>
this may impact selectors that target the positioned element using
direct child or sibling selectors.
</p>
<p>
Alternatively, set the <code>positionAreaContainingBlock</code> polyfill
option to <code>false</code> to compute and apply inset values directly
on the positioned element, without a wrapper element.
<a href="?no-containing-block">Try these examples without the wrapper</a
>. Or set it to <code>'auto'</code> to add the wrapper only for targets
whose styles resolve against the containing block.
<a href="?auto">Try these examples in <code>auto</code> mode</a>.
</p>
<button id="toggle-wrapper" type="button" disabled>
Apply polyfill to enable wrapper visibility
</button>
Expand Down
58 changes: 56 additions & 2 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import type {
NormalizedAnchorPositioningPolyfillOptions,
} from './polyfill.js';
import {
activeTargetStyles,
activeWrapperStyles,
addPositionAreaDeclarationBlockStyles,
dataForPositionAreaTarget,
getPositionAreaDeclaration,
isContainingBlockDependentDeclaration,
type PositionAreaDeclaration,
type PositionAreaTargetData,
} from './position-area.js';
Expand Down Expand Up @@ -263,6 +265,24 @@ function getAnchorFunctionData(node: CssNode, declaration: Declaration | null) {
return {};
}

/**
* Whether the element matches any of the given selectors. Selectors that are
* not valid for `Element.matches()` (e.g. those containing pseudo-elements) are
* skipped rather than throwing.
*/
function matchesAnySelector(el: HTMLElement, selectors: Set<string>): boolean {
for (const selector of selectors) {
try {
if (el.matches(selector)) {
return true;
}
} catch {
// Ignore selectors that can't be used with `matches()`.
}
}
return false;
}

async function getAnchorEl(
targetEl: HTMLElement | null,
anchorObj: AnchorFunction | null,
Expand Down Expand Up @@ -304,6 +324,11 @@ export async function parseCSS(
) {
const anchorFunctions: AnchorFunctionDeclarations = {};
const positionAreas: PositionAreaDeclarations = {};
const positionAreaContainingBlock =
options.positionAreaContainingBlock ?? true;
// In `'auto'` mode, the selectors whose declarations resolve against the
// containing block. A `position-area` target matching any of these is wrapped.
const containingBlockDependentSelectors = new Set<string>();
resetStores();

// Parse `position-try` and related declarations/rules
Expand Down Expand Up @@ -332,6 +357,22 @@ export async function parseCSS(
}
}

// Record selectors with containing-block-dependent declarations, so
// `'auto'` mode can decide per target whether the wrapper is needed.
if (
positionAreaContainingBlock === 'auto' &&
node.type === 'Declaration' &&
selectors.length &&
isContainingBlockDependentDeclaration(
node.property,
generateCSS(node.value),
)
) {
for (const { selector } of selectors) {
containingBlockDependentSelectors.add(selector);
}
}

// Parse `anchor()` function
const {
prop,
Expand Down Expand Up @@ -359,6 +400,7 @@ export async function parseCSS(
addPositionAreaDeclarationBlockStyles(
positionAreaDeclaration,
this.block,
positionAreaContainingBlock,
);
for (const { selector } of selectors) {
positionAreas[selector] = [
Expand Down Expand Up @@ -775,15 +817,27 @@ export async function parseCSS(
const anchorEl = await getAnchorEl(targetEl, null, {
roots: options.roots,
});
// Resolve whether this target needs the wrapper. In `'auto'` mode, wrap
// only targets with containing-block-dependent styles; otherwise the
// option value is used directly.
const needsWrapper =
positionAreaContainingBlock === 'auto'
? matchesAnySelector(targetEl, containingBlockDependentSelectors)
: positionAreaContainingBlock;
// For every position-area declaration with this selector, create a new
// UUID, and make sure the target has a wrapper.
// UUID, and make sure the target has a wrapper (or is marked with a
// matching attribute, when the wrapper is not needed).
for (const positionData of positions) {
const targetData = await dataForPositionAreaTarget(
targetEl,
positionData,
anchorEl,
needsWrapper,
);
positionAreaMappingStyleElement.css += activeWrapperStyles(
const activeStyles = needsWrapper
? activeWrapperStyles
: activeTargetStyles;
positionAreaMappingStyleElement.css += activeStyles(
targetData.targetUUID,
positionData.selectorUUID,
);
Expand Down
Loading
Loading