fix: add patchCSSOMto support setting anchor positioning properties in JS - #447
fix: add patchCSSOMto support setting anchor positioning properties in JS#447jpzwarte wants to merge 30 commits into
patchCSSOMto support setting anchor positioning properties in JS#447Conversation
`anchor-name` and `position-anchor` assigned from JavaScript are dropped by the CSSOM in a browser without native support, so nothing lands in the `style` attribute the polyfill reads. The demo wires up its anchor at runtime the way a design system component would, and does not work as a result. Refs oddbird#445 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Deploy Preview for anchor-polyfill ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for anchor-position-wpt canceled.
|
jamesnw
left a comment
There was a problem hiding this comment.
Mostly conceptual questions at this point-
Idea- instead of writing the unsupported values to the style tag, could we shift the values to custom properties? I think that would sidestep a lot of this. We could do it like the shifted properties in cascade.ts, so like --anchor-name-${INSTANCE_UUID}, and then it might just work when we need to read it.
Does this change make it work better for Lit, for instance?
This is useful for non-shadow DOM things, correct? It might be worth not tying the examples together.
Would it be possible to expose this as an option when the polyfill is run, or does it need to be applied as a separate step, so that the application can do some work before the polyfill can be run?
| // patch it never sees anchors that are wired up from JavaScript. | ||
| const PATCHED_PROPERTIES = { | ||
| anchorName: 'anchor-name', | ||
| positionAnchor: 'position-anchor', |
There was a problem hiding this comment.
Why not positionArea or other anchor-related properties?
There was a problem hiding this comment.
Because anchor-name and position-anchor are the dynamic parts that i'm setting from Lit. Example:
<sl-button id="button">Button</sl-button>
<sl-tooltip for="button">Tooltip</sl-tooltip>Which results in:
<sl-button id="button" style="anchor-name: --sl-tooltip-1">Button</sl-button>
<sl-tooltip for="button" style="position-anchor: --sl-tooltip-1">Tooltip</sl-tooltip>Everything else is part of :host.
I could add more properties of course, but i'm not sure its worth it?
There was a problem hiding this comment.
Perhaps including at least position-area as well is a good idea. I can at least think of a web component where you could specify where it is anchored. But where do you stop?
There was a problem hiding this comment.
There are only a handful of new properties for anchor positioning, and I'd rather just support them all.
There was a problem hiding this comment.
See 5b315b0. This was more than just adding more properties to PATCHED_PROPERTIES:
patchCSSOM works by stashing whatever you assign into a private custom property, so el.style.positionArea = 'top' really lands in the style attribute as --position-area-<id>: top. Nothing else in the polyfill knows to look there — the two properties that already worked did so because two spots in the parser had been hand-taught to recognise that private spelling.
So rather than hand-teach five more spots (which would have made every author rule match twice, since the cascade leaves both spellings behind), this commit adds one step to cascadeCSS that renames the private property back to the real one before anything parses it; from there the CSSOM-set value is indistinguishable from CSS the author wrote, and every existing parser handles it for free.
The position-try properties also had to be added to the shifted list so their values survive being written back to the style attribute, and that in turn exposed a bug where transformCSS mangled that attribute whenever the polyfill generated a fallback rule for an inline style.
I think it's a matter of preference. If you run |
True, but where would we put an example for it? A new |
Done! |
jamesnw
left a comment
There was a problem hiding this comment.
This is looking pretty close, I think!
This is useful for non-shadow DOM things, correct? It might be worth not tying the examples together.
True, but where would we put an example for it? A new cssom.html page?
It can just be added to index.html. I know it's super long, and eventually it would be nice to have a nicer way to add demos.
| // patch it never sees anchors that are wired up from JavaScript. | ||
| const PATCHED_PROPERTIES = { | ||
| anchorName: 'anchor-name', | ||
| positionAnchor: 'position-anchor', |
There was a problem hiding this comment.
There are only a handful of new properties for anchor positioning, and I'd rather just support them all.
patchCSSOMto support setting anchor-name and position-anchor in JSpatchCSSOMto support setting anchor positioning properties in JS
Normalize each rule's CSSOM-stored custom properties before the declaration walk, keeping the last stored value and dropping the stale literal, so a re-run can't re-shift the old value after the new one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
Clear the queued-host entry when its run starts, so a later `adoptedStyleSheets` assignment queues a fresh one. The polyfill's own swap of a transformed copy now skips queuing via an explicit flag, rather than relying on the entry never being cleared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
The guard against clobbering an element's `style` attribute only matched `--anchor-*`, missing the five CSSOM properties that shift into `--position-*`. Match all of them, and append them so the element's value wins over the text the run captured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
CSS property names are ASCII case-insensitive, so `setProperty( 'Anchor-Name', ...)` has to hit the patch too; falling through handed it to the native method, which drops it. Custom property names stay case-sensitive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
`setProperty` stores the priority on the shifted custom property, so reading the literal name always reported none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
The root adopts a private copy, so reading its own rules pinned the styles to whatever the source said when the copy was made: a later `replaceSync` on the sheet the application still holds never took effect, however many times the polyfill re-ran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
Only a custom element has a `connectedCallback` to signal the deferred run, so a built-in element with a shadow root was never positioned. Check back at the end of the adopting task as well, where the usual build-then-append sequence has already connected it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
Only the empty string removes a declaration. A whitespace-only value is a parse failure natively, which leaves the previous value in place rather than clearing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
The wrapper has to go on before the native call, which captures it. A rejected definition left it behind, so offering the same constructor again under another name stacked a second layer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
Derive a `ShiftedProperty` union from the shifted names and type `CSSOM_PROPERTIES` with it, so an entry missing from `SHIFTED_PROPERTIES` no longer resolves to an `undefined` custom property. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
A single property-to-storage record answers both whether a property is ours and what it is stored in, replacing the duplicated name list and the linear scan that preceded every derivation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
Parsing and re-serializing per element per run costs ~5µs each to recover a string the CSS almost always already is. Fall back to the parse only when a generated rule is actually present. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
The document is searched alongside the roots, so a shadow host with a `style` attribute is found twice -- once directly, once by the `:host` resolution in `querySelectorAllRoots`. Nothing covered the dedupe that relies on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLFaCumxtMb9bMW1zPN6sb
Fixes #445
patchCSSOM()(opt-in, exported from/fn) makes every anchor positioning property the polyfill supports settable from JavaScript:anchor-name,anchor-scope,position-anchor,position-area, and theposition-tryproperties.position-visibilityis left out, as the polyfill doesn't support it. It defines each onCSSStyleDeclarationand stores the value in the custom property the polyfill shifts that declaration into internally — the browser can't drop a custom property, and it's already what the polyfill reads back.setProperty(),getPropertyValue()andremoveProperty()take the dashed names as well.cascadeCSSthen restores those declarations to the property they were set on, before anything parses them, so a value set through the CSSOM is indistinguishable from one written in a stylesheet. That's what makes the properties beyondanchor-name/position-anchorwork at all, and it keeps the CSSOM out of every parser — no special cases inparse.ts,position-area.tsorfallback.ts.'anchorName' in element.stylebecomes true once it has run.CSS.supports('anchor-name: --a')is unaffected, and is what the README now recommends for feature detection.Two bugs fixed along the way, both of which predate the CSSOM work:
fetchInlineStyles()only searcheddocument. It now searches the polyfill roots too — both, since a shadow-scoped run still needs its host and any light-DOM anchors.transformCSS()corrupted an element'sstyleattribute whenever the polyfill generated a fallback rule for that element's inline styles. It pulled the declarations back out with a fixed-length slice, which breaks as soon as anything else shares the block, writing a mangled selector into the attribute and losing the element's real styles. Generated rules now go into a stylesheet of their own. This was already reachable from plain HTML (<div style="position-try-fallbacks: --flip">), where the fallback silently never applied either.Adds demos to
index.htmlandshadow-dom.html.