Skip to content

Commit

Permalink
Fix @position-try with multiple selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
jgerigmeyer committed Aug 30, 2024
1 parent 7b1c45f commit 53b3b85
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 80 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ following features:
- a `position-area` as a `try-tactic`
- Fallback does does not support anchor functions that are nested or passed
through custom properties.
- Multiple selectors for a single `position-try-fallbacks` rule that uses an
`@position-try` rule is not supported.
- Polyfill allows anchoring in scroll more permissively than the spec allows,
for instance without a default `position-anchor`.
- `anchor-scope` property on pseudo-elements
Expand Down
65 changes: 51 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -444,26 +444,27 @@ <h2>
<ol>
<li>
Align the bottom, left edge of the target with the top, right edge
of the anchor.
of its anchor.
</li>
<li>
Apply <code>flip-block</code>, flipping across the inline axis, and
align the top, left edge of the target with the bottom, right edge
of the anchor.
of its anchor.
</li>
<li>
Apply <code>flip-inline</code>, flipping across the block axis, and
align the bottom, right edge of the target with the top, left edge
of the anchor.
of its anchor.
</li>
<li>
Apply <code>flip-start</code>, flipping across a diagonal line from
upper left to lower right, and align the top, right edge of the
target with the bottom, left edge of the anchor.
target with the bottom, left edge of its anchor.
</li>
<li>
When every position overflows, use the last fallback position that
was successful.
When every position overflows, revert to the last successful
fallback position if there is one, otherwise revert to the initial
base styles.
</li>
</ol>
<p>
Expand All @@ -478,9 +479,21 @@ <h2>
anchor-name: --my-anchor-try-tactics;
}

#my-anchor-try-tactics-2 {
anchor-name: --my-anchor-try-tactics-2;
}

#my-target-try-tactics {
position: absolute;
position-anchor: --my-anchor-try-tactics;
}

#my-target-try-tactics-2 {
position-anchor: --my-anchor-try-tactics-2;
}

#my-target-try-tactics,
#my-target-try-tactics-2 {
position: absolute;
bottom: anchor(top);
left: anchor(right);
position-try-fallbacks: flip-block, flip-inline, flip-start;
Expand All @@ -500,6 +513,12 @@ <h2>
>@position-try Anchor</span
>
<div id="my-target-fallback" class="target">Target</div>
<span id="my-anchor-fallback-2" class="anchor"
>@position-try Anchor 2</span
>
<div id="my-target-fallback-2" style="opacity: 0.8" class="target">
Target 2
</div>
<div class="placefiller-after-anchor"></div>
</div>
</div>
Expand All @@ -510,23 +529,24 @@ <h2>
<ol>
<li>
Align the bottom, left edge of the target with the top, left edge of
the anchor.
its anchor.
</li>
<li>
Align the top, left edge of the target with the bottom, left edge of
the anchor.
its anchor.
</li>
<li>
Align the bottom, right edge of the target with the top, right edge
of the anchor.
of its anchor.
</li>
<li>
Align the top, right edge of the target with the bottom, right edge
of the anchor, and set the height and width of the target to 100px.
of its anchor, and set the height and width of the target to 100px.
</li>
<li>
When every position overflows, revert to the initial base styles in
#1, even though it overflows.
When every position overflows, revert to the last successful
fallback position if there is one, otherwise revert to the initial
base styles.
</li>
</ol>
</div>
Expand All @@ -535,9 +555,21 @@ <h2>
anchor-name: --my-anchor-fallback;
}

#my-anchor-fallback-2 {
anchor-name: --my-anchor-fallback-2;
}

#my-target-fallback {
position: absolute;
position-anchor: --my-anchor-fallback;
}

#my-target-fallback-2 {
position-anchor: --my-anchor-fallback-2;
}

#my-target-fallback,
#my-target-fallback-2 {
position: absolute;

/* First try to align the bottom, left edge of the target
with the top, left edge of the anchor. */
Expand Down Expand Up @@ -612,6 +644,11 @@ <h2>
across the block axis, and align the top, right edge of the target
with the bottom, right edge of the anchor.
</li>
<li>
When every position overflows, revert to the last successful
fallback position if there is one, otherwise revert to the initial
base styles.
</li>
</ol>
</div>
<pre><code class="language-css"
Expand Down
14 changes: 13 additions & 1 deletion public/position-try.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@
anchor-name: --my-anchor-fallback;
}

#my-anchor-fallback-2 {
anchor-name: --my-anchor-fallback-2;
}

#my-target-fallback {
position: absolute;
position-anchor: --my-anchor-fallback;
}

#my-target-fallback-2 {
position-anchor: --my-anchor-fallback-2;
}

#my-target-fallback,
#my-target-fallback-2 {
position: absolute;

/* First try to align the bottom, left edge of the target
with the top, left edge of the anchor. */
Expand Down
90 changes: 35 additions & 55 deletions src/fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,15 @@ interface AtRuleRaw extends csstree.Atrule {

// `key` is the `@position-try` block uuid
// `value` is the target element selector
type FallbackTargets = Record<string, string>;
type FallbackTargets = Record<string, string[]>;

type Fallbacks = Record<
// `key` is a reference to a specific `position-try-fallbacks` value, which
// may be a dashed ident name of a `@position-try` rule, or the selector
// combined with `try-tactics` and `@position-try` rules.
string,
{
// `targets` is an array of selectors where this `position-fallback` is used
targets: string[];
// `blocks` is an array of `@try` block declarations (in order)
blocks: TryBlock[];
}
// `value` is a block of `@position-try` declarations
TryBlock
>;

const POSITION_AREA_PROPS = [
Expand Down Expand Up @@ -575,7 +571,6 @@ export function getPositionTryRules(node: csstree.Atrule) {
node.block?.children
) {
const name = node.prelude.value;
const tryBlocks: TryBlock[] = [];
const declarations = node.block.children.filter(
(d): d is DeclarationWithValue =>
isDeclaration(d) && isAcceptedPositionTryProperty(d),
Expand All @@ -587,9 +582,7 @@ export function getPositionTryRules(node: csstree.Atrule) {
),
};

tryBlocks.push(tryBlock);

return { name, blocks: tryBlocks };
return { name, tryBlock };
}
return {};
}
Expand All @@ -606,18 +599,12 @@ export function parsePositionFallbacks(styleData: StyleData[]) {
visit: 'Atrule',
enter(node) {
// Parse `@position-try` rules
const { name, blocks } = getPositionTryRules(node);
if (name && blocks?.length) {
const { name, tryBlock } = getPositionTryRules(node);
if (name && tryBlock) {
// This will override earlier `@position-try` lists with the same
// name: (e.g. multiple `@position-try --my-fallback {...}` uses with
// the same `--my-fallback` name)

// Todo: this doesn't account for multiple selectors for a single
// `position-try-fallbacks` rule that uses an `@position-try` rule.
fallbacks[name] = {
targets: [],
blocks: blocks,
};
fallbacks[name] = tryBlock;
}
},
});
Expand All @@ -627,6 +614,7 @@ export function parsePositionFallbacks(styleData: StyleData[]) {
// and add in block contents (scoped to unique data-attrs)
for (const styleObj of styleData) {
let changed = false;
const fallbacksAdded = new Set();
const ast = getAST(styleObj.css);
csstree.walk(ast, {
visit: 'Declaration',
Expand Down Expand Up @@ -658,47 +646,39 @@ export function parsePositionFallbacks(styleData: StyleData[]) {
if (tacticAppliedRules) {
// add new item to fallbacks store
fallbacks[name] = {
targets: [selector],
blocks: [
{
uuid: `${selector}-${tryObject.tactics.join('-')}-try-${nanoid(12)}`,
declarations: tacticAppliedRules,
},
],
uuid: `${selector}-${tryObject.tactics.join('-')}-try-${nanoid(12)}`,
declarations: tacticAppliedRules,
};
}
} else if (tryObject.type === 'at-rule-with-try-tactic') {
// get `@position-try` block styles and adjust based on the tactic
name = `${selector}-${tryObject.atRule}-${tryObject.tactics.join('-')}`;
const declarations = fallbacks[tryObject.atRule].blocks[0];
const declarations = fallbacks[tryObject.atRule];
const tacticAppliedRules = applyTryTacticsToAtRule(
declarations,
tryObject.tactics,
);
if (tacticAppliedRules) {
// add new item to fallbacks store
fallbacks[name] = {
targets: [selector],
blocks: [
{
uuid: `${selector}-${tryObject.atRule}-${tryObject.tactics.join('-')}-try-${nanoid(12)}`,
declarations: tacticAppliedRules,
},
],
uuid: `${selector}-${tryObject.atRule}-${tryObject.tactics.join('-')}-try-${nanoid(12)}`,
declarations: tacticAppliedRules,
};
}
}

if (name && fallbacks[name]) {
anchorPosition.fallbacks ??= [];
anchorPosition.fallbacks.push(...fallbacks[name].blocks);
const dataAttr = `[data-anchor-polyfill="${fallbacks[name].uuid}"]`;
// Store mapping of data-attr to target selectors
fallbackTargets[dataAttr] ??= [];
fallbackTargets[dataAttr].push(selector);

if (!fallbacks[name].targets.includes(selector)) {
fallbacks[name].targets.push(selector);
}
// Add each `@position-try` block, scoped to a unique data-attr
for (const block of fallbacks[name].blocks) {
const dataAttr = `[data-anchor-polyfill="${block.uuid}"]`;
if (!fallbacksAdded.has(name)) {
anchorPosition.fallbacks ??= [];
anchorPosition.fallbacks.push(fallbacks[name]);
fallbacksAdded.add(name);

// Add `@position-try` block, scoped to a unique data-attr
this.stylesheet?.children.prependData({
type: 'Rule',
prelude: {
Expand All @@ -708,22 +688,22 @@ export function parsePositionFallbacks(styleData: StyleData[]) {
block: {
type: 'Block',
children: new csstree.List<csstree.CssNode>().fromArray(
Object.entries(block.declarations).map(([prop, val]) => ({
type: 'Declaration',
important: true,
property: prop,
value: {
type: 'Raw',
value: val,
},
})),
Object.entries(fallbacks[name].declarations).map(
([prop, val]) => ({
type: 'Declaration',
important: true,
property: prop,
value: {
type: 'Raw',
value: val,
},
}),
),
),
},
});
// Store mapping of data-attr to target selector
fallbackTargets[dataAttr] = selector;
changed = true;
}
changed = true;
}
});
if (Object.keys(anchorPosition).length > 0) {
Expand Down
6 changes: 3 additions & 3 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,11 +675,11 @@ export async function parseCSS(styleData: StyleData[]) {
let targets: NodeListOf<HTMLElement>;
if (
targetSel.startsWith('[data-anchor-polyfill=') &&
fallbackTargets[targetSel]
fallbackTargets[targetSel]?.length
) {
// If we're dealing with a `@position-fallback` `@try` block,
// If we're dealing with a `@position-try` block,
// then the targets are places where that `position-fallback` is used.
targets = document.querySelectorAll(fallbackTargets[targetSel]);
targets = document.querySelectorAll(fallbackTargets[targetSel].join(','));
} else {
targets = document.querySelectorAll(targetSel);
}
Expand Down
14 changes: 9 additions & 5 deletions src/polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@ async function applyPositionFallbacks(
target,
async () => {
// If this auto-update was triggered while the polyfill is already
// looping through the possible `@try` blocks, do not check again.
// looping through the possible `position-try-fallbacks` blocks, do not
// check again.
if (checking) {
return;
}
Expand All @@ -382,18 +383,19 @@ async function applyPositionFallbacks(
const defaultOverflow = await checkOverflow(target, offsetParent);
// If none of the sides overflow, don't try fallbacks
if (Object.values(defaultOverflow).every((side) => side <= 0)) {
target.removeAttribute('data-anchor-polyfill-last-successful');
checking = false;
return;
}
// Apply the styles from each `@position-try` block (in order), stopping
// when we reach one that does not cause the target's margin-box to
// overflow its offsetParent (containing block).
// Apply the styles from each fallback block (in order), stopping when
// we reach one that does not cause the target's margin-box to overflow
// its offsetParent (containing block).
for (const [index, { uuid }] of fallbacks.entries()) {
target.setAttribute('data-anchor-polyfill', uuid);

const overflow = await checkOverflow(target, offsetParent);

// If none of the sides overflow, use this `@try` block and stop loop.
// If none of the sides overflow, use this fallback and stop loop.
if (Object.values(overflow).every((side) => side <= 0)) {
checking = false;
target.setAttribute('data-anchor-polyfill-last-successful', uuid);
Expand All @@ -408,6 +410,8 @@ async function applyPositionFallbacks(
);
if (lastSuccessful) {
target.setAttribute('data-anchor-polyfill', lastSuccessful);
} else {
target.removeAttribute('data-anchor-polyfill');
}
break;
}
Expand Down

0 comments on commit 53b3b85

Please sign in to comment.