From a1047e1cc2cd7880b3afba1587bb4990b2ce3072 Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Wed, 26 Jun 2024 11:56:09 -0400 Subject: [PATCH] WIP on try tactics" --- index.html | 84 ++++++++++++++++++++++++++++++++- public/position-try-tactics.css | 11 +++++ src/cascade.ts | 23 +++++++++ src/parse.ts | 18 ++----- src/utils.ts | 23 +++++++++ tests/unit/cascade.test.ts | 11 +++++ tests/unit/parse.test.ts | 2 +- tests/unit/utils.test.ts | 22 +++++++++ 8 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 public/position-try-tactics.css create mode 100644 tests/unit/utils.test.ts diff --git a/index.html b/index.html index a25cc495..9260dff6 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,7 @@ + @@ -420,7 +421,7 @@

- Positioning with @position-try + Fallbacks with @position-try

@@ -489,6 +490,87 @@

right: anchor(--my-anchor-fallback right); } +@position-try --bottom-right { + /* Finally, try to align the top, right edge of the target + with the bottom, right edge of the anchor. */ + top: anchor(--my-anchor-fallback bottom); + right: anchor(--my-anchor-fallback right); + width: 100px; + height: 100px; +} +

+
+

+ + Positioning with try tactics +

+
+
+
+
+ @position-try-tactics Anchor +
Target
+
+
+
+
+

+ With polyfill applied, the following positions are attempted in order: +

+
    +
  1. + Align the bottom, left edge of the target with the top, left edge of + the anchor. +
  2. +
  3. + Align the top, left edge of the target with the bottom, left edge of + the anchor. +
  4. +
  5. + Align the bottom, right edge of the target with the top, right edge + of the anchor. +
  6. +
  7. + Align the top, right edge of the target with the bottom, right edge + of the anchor, and increase the height and width of the target. +
  8. +
  9. + When every position overflows, revert to the initial base styles in + #1, even though it overflows. +
  10. +
+
+
#my-anchor-fallback {
+  anchor-name: --my-anchor-fallback;
+}
+
+#my-target-fallback {
+  position: absolute;
+
+  /* First try to align the bottom, left edge of the target
+  with the top, left edge of the anchor. */
+  bottom: anchor(--my-anchor-fallback top);
+  left: anchor(--my-anchor-fallback left);
+  position-try-options: --bottom-left, --top-right, --bottom-right;
+}
+
+@position-try --bottom-left {
+  /* Next try to align the top, left edge of the target
+     with the bottom, left edge of the anchor. */
+  top: anchor(--my-anchor-fallback bottom);
+  left: anchor(--my-anchor-fallback left);
+}
+
+@position-try --top-right {
+  /* Next try to align the bottom, right edge of the target
+     with the top, right edge of the anchor. */
+  bottom: anchor(--my-anchor-fallback top);
+  right: anchor(--my-anchor-fallback right);
+}
+
 @position-try --bottom-right {
   /* Finally, try to align the top, right edge of the target
      with the bottom, right edge of the anchor. */
diff --git a/public/position-try-tactics.css b/public/position-try-tactics.css
new file mode 100644
index 00000000..9f25c308
--- /dev/null
+++ b/public/position-try-tactics.css
@@ -0,0 +1,11 @@
+#my-anchor-try-tactics {
+  anchor-name: --my-anchor-try-tactics;
+}
+
+#my-target-try-tactics {
+  position: absolute;
+  position-anchor: --my-anchor-try-tactics;
+  bottom: anchor(top);
+  left: anchor(left);
+  position-try-options: flip-block, flip-inline, flip-start;
+}
diff --git a/src/cascade.ts b/src/cascade.ts
index 8dbd4561..237cb151 100644
--- a/src/cascade.ts
+++ b/src/cascade.ts
@@ -5,6 +5,7 @@ import {
   generateCSS,
   getAST,
   getDeclarationValue,
+  isAnchorFunction,
   POSITION_ANCHOR_PROPERTY,
   type StyleData,
 } from './utils.js';
@@ -27,6 +28,24 @@ function shiftPositionAnchorData(node: csstree.CssNode, block?: csstree.Block) {
   return {};
 }
 
+// Move inset declarations to cascadable properties
+// property.
+function shiftInsetData(node: csstree.CssNode, block?: csstree.Block) {
+  if (isAnchorFunction(node) && block) {
+    block.children.appendData({
+      type: 'Declaration',
+      important: false,
+      property: POSITION_ANCHOR_PROPERTY,
+      value: {
+        type: 'Raw',
+        value: node,
+      },
+    });
+    return { updated: true };
+  }
+  return {};
+}
+
 export async function cascadeCSS(styleData: StyleData[]) {
   for (const styleObj of styleData) {
     let changed = false;
@@ -39,6 +58,10 @@ export async function cascadeCSS(styleData: StyleData[]) {
         if (updated) {
           changed = true;
         }
+        const { updated: insetUpdated} = shiftInsetData(node, block);
+        if (insetUpdated) {
+          changed = true;
+        }
       },
     });
 
diff --git a/src/parse.ts b/src/parse.ts
index 997703d0..48d2bfd6 100644
--- a/src/parse.ts
+++ b/src/parse.ts
@@ -5,7 +5,9 @@ import {
   type DeclarationWithValue,
   generateCSS,
   getAST,
+  isAnchorFunction,
   POSITION_ANCHOR_PROPERTY,
+  splitCommaList,
   type StyleData,
 } from './utils.js';
 import { validatedForPositioning } from './validate.js';
@@ -141,8 +143,7 @@ type PositionTryOptionsTryTactics = 'flip-block' | 'flip-inline' | 'flip-start';
 type PositionTryOption =
   | 'none'
   | PositionTryOptionsTryTactics
-  // | csstree.Identifier
-  // todo: what's the dashed ident type
+  | csstree.Identifier
   | InsetProperty;
 
 export interface AnchorFunction {
@@ -210,12 +211,6 @@ function isAnchorNameDeclaration(
   return node.type === 'Declaration' && node.property === 'anchor-name';
 }
 
-function isAnchorFunction(
-  node: csstree.CssNode | null,
-): node is csstree.FunctionNode {
-  return Boolean(node && node.type === 'Function' && node.name === 'anchor');
-}
-
 function isAnchorSizeFunction(
   node: csstree.CssNode | null,
 ): node is csstree.FunctionNode {
@@ -456,12 +451,7 @@ function getPositionTryOptionsDeclaration(
     node.value.children.first &&
     rule?.value
   ) {
-    return node.value.children
-      .map((item) => {
-        const { name } = item as csstree.Identifier;
-        return name as PositionTryOption;
-      })
-      .toArray();
+    return splitCommaList(node.value.children);
   }
   return [];
 }
diff --git a/src/utils.ts b/src/utils.ts
index 39053b47..73915515 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -5,6 +5,12 @@ export interface DeclarationWithValue extends csstree.Declaration {
   value: csstree.Value;
 }
 
+export function isAnchorFunction(
+  node: csstree.CssNode | null,
+): node is csstree.FunctionNode {
+  return Boolean(node && node.type === 'Function' && node.name === 'anchor');
+}
+
 export function getAST(cssText: string) {
   return csstree.parse(cssText, {
     parseAtrulePrelude: false,
@@ -33,3 +39,20 @@ export interface StyleData {
 }
 
 export const POSITION_ANCHOR_PROPERTY = `--position-anchor-${nanoid(12)}`;
+
+export function splitCommaList(list: csstree.List) {
+  return list.toArray().reduce(
+    (acc: csstree.Identifier[][], child) => {
+      if (child.type === 'Operator' && child.value === ',') {
+        acc.push([]);
+        return acc;
+      }
+      if (child.type === 'Identifier') {
+        acc[acc.length - 1].push(child);
+      }
+
+      return acc;
+    },
+    [[]],
+  );
+}
diff --git a/tests/unit/cascade.test.ts b/tests/unit/cascade.test.ts
index 679092d0..c705bdfe 100644
--- a/tests/unit/cascade.test.ts
+++ b/tests/unit/cascade.test.ts
@@ -14,4 +14,15 @@ describe('cascadeCSS', () => {
     expect(css).toContain(`${POSITION_ANCHOR_PROPERTY}:--my-position-anchor-b`);
     expect(css).toContain(`${POSITION_ANCHOR_PROPERTY}:--my-position-anchor-a`);
   });
+  it('adds insets with anchors as custom properties', async () => {
+    const srcCSS = getSampleCSS('position-try-tactics');
+    const styleData: StyleData[] = [
+      { css: srcCSS, el: document.createElement('div') },
+    ];
+    const cascadeCausedChanges = await cascadeCSS(styleData);
+    expect(cascadeCausedChanges).toBe(true);
+    const { css } = styleData[0];
+    expect(css).toContain('--bottom: anchor(top);');
+    expect(css).toContain('--left: anchor(top);');
+  });
 });
diff --git a/tests/unit/parse.test.ts b/tests/unit/parse.test.ts
index f8a680b9..20c781d5 100644
--- a/tests/unit/parse.test.ts
+++ b/tests/unit/parse.test.ts
@@ -692,7 +692,7 @@ describe('parseCSS', () => {
               targetEl,
             },
           ],
-          width: expect.any(Array)
+          width: expect.any(Array),
         },
         fallbacks: [
           {
diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts
new file mode 100644
index 00000000..4f8bfc5b
--- /dev/null
+++ b/tests/unit/utils.test.ts
@@ -0,0 +1,22 @@
+import type * as csstree from 'css-tree';
+
+import { getAST, splitCommaList } from '../../src/utils.js';
+
+describe('splitCommaList', () => {
+  it('works', () => {
+    const { children } = getAST('a{b: c d, e, f;}') as csstree.StyleSheet;
+    const value = (
+      (children.first as csstree.Rule).block.children
+        .first as csstree.Declaration
+    ).value as csstree.Value;
+    const res = splitCommaList(value.children);
+    expect(res).toEqual([
+      [
+        { name: 'c', type: 'Identifier', loc: null },
+        { name: 'd', type: 'Identifier', loc: null },
+      ],
+      [{ name: 'e', type: 'Identifier', loc: null }],
+      [{ name: 'f', type: 'Identifier', loc: null }],
+    ]);
+  });
+});