w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 668 forks source link

[css-scoping] Breaking name encapsulation #10808

Open tabatkins opened 2 months ago

tabatkins commented 2 months ago

The Scoping spec defines "tree-scoped names" and "tree-scoped references" to explain the encapsulation effects of Shadow DOM on CSS constructs. These concepts have been reused across many specs now, and overall seem to do what we want - shadows can safely define and reference names without having to worry about the outer page accidentally defining or referencing a conflicting name.

However, authors regularly ask for ways to defeat this encapsulation, and reference things across shadows. Generally this is because they're using shadow dom just as an organization tool, not an encapsulation boundary - it's the easiest way to use custom elements. It's possible that the correct answer to these requests is that we need a way to opt a shadow tree out of being encapsulated, so it acts like it's part of the outer page as much as possible (no more event censoring, etc either).

However, in the absence of that, it might make sense to have a CSS mechanism for this opt-out, to give authors a way to (a) write shadow CSS that can refer to names defined in the outer tree, or in other shadow trees entirely, and (b) write shadow CSS that defines names that the outer page, or other shadow trees, can reference.

As a conversation starter, I suggest a new global(<ident>, <ident>?) function, allowed anywhere that takes idents for tree-scoped names or tree-scoped references. If an ident foo would define a tree-scoped name, global(foo) defines it "globally" instead, without a tree scope. Similarly, if an ident foo would be a tree-scoped reference, then global(foo) is a "global" reference, matching names without a tree scope.

The second argument is a scope name; the name/ref is still global, but must match the scope name as well as the ident to be considered matching. Omitting the ident indicates the "default" global scope, and still only matches with other names/refs in the "default" global scope.

(Another way of looking at this is that global() just changes the way we construct a tree-scoped name/ref. Normally it's a (name, scope) pair with the scope being an automatically-filled-in tree reference; if you use global(), we instead set the scope to the given ident. Matching rules still work otherwise the same, requiring both names and scopes to match. )

For example, by default, using anchor-name: --foo; on an element in a shadow DOM, and then position-anchor: --foo; in a light-dom positioned element, the two elements won't see each other. Each is tree-scoped, and the anchor-matching rules require the name and the reference to have the same tree scope. But if you used anchor-name: global(--foo); and position-anchor: global(--foo);, they'd find each other, because they have matching tree scopes (the default global scope).

Coordinating components can still avoid accidentally clashing with globals from other sources by using the scope-name argument with a reasonably unique name, if they want, and pages can achieve hostile interoperability with those components by using the same scope name if they want.

(Note: The first draft of the tree-scoped idea, back in #1995, proposed something similar, but not as good. We ended up concluding that we didn't need the complexity of letting references switch into "global" mode at the time, but I think continuing author requests shows that was a wrong decision in the end.)

(Also, if we did this, we should probably define <tree-scoped-ref> and <tree-scoped-name> productions, and do an audit to use those everywhere we're currently just using idents and declaring the tree-scoped-ness in prose.)

andruud commented 2 months ago

(Another way of looking at this is that global() just changes the way we construct a tree-scoped name/ref. Normally it's a (name, scope) pair with the scope being an automatically-filled-in tree reference; if you use global(), we instead set the scope to the given ident. Matching rules still work otherwise the same, requiring both names and scopes to match. )

Hmm, tree-scoped lookup works by traversing the tree-scope chain, looking for a match (at least for some @-rule cases). We'd now need to place the thing constructed by @whatever global(--foo) into a non-home tree-scope (for the first time), which might cause some issues.

But even with that, this sounds straightforward.

Regarding the scope-name parameter: is it wise to add another scoping mechanism to the soup of (Shadow DOM, @scope, timeline-scope, anchor-scope)? Is global(--foo, --wikipedia) a huge improvement over global(--wikipedia-foo)? Maybe we can drop this parameter, and just rely on prefixing. If dynamic prefixing is needed, maybe consider #9141.

It's possible that the correct answer to these requests is that we need a way to opt a shadow tree out of being encapsulated, so it acts like it's part of the outer page as much as possible (no more event censoring, etc either).

I'd expect many authors to prefer this. Related: #10420

tabatkins commented 1 month ago

(Sorry, me and Anders discussed this in person and I didn't come back to update the thread.)

Regarding the scope-name parameter: is it wise to add another scoping mechanism

Yeah, let's drop that. Having a scoping name is only useful if we can rely on some variety of defaults to let you avoid specifying the scope most of the time. But here it would be required in every instance, meaning it's just part of the name anyway; you might as well just bake it into the name itself.

So consider the proposal simplified: just a global(<custom-ident>)

css-meeting-bot commented 1 month ago

The CSS Working Group just discussed [css-scoping] Breaking name encapsulation.

The full IRC log of that discussion <khush> TabAtkins: we talked about tree scoping, names and references. filling context
<khush> when you are in a shadow tree you can see names defined in your tree and outer tree but not in other shadow trees
<khush> similarly you can't in the main page see names defined in inner shadow trees
<khush> good thing when you don't want to expose. but people complain about not being able to do componenets, example to anchor to elements in shadow trees
<khush> proposing a solution, allow exposing names in accessible fashion
<khush> new global func takes a custom ident. can be used anywehre you define a custom ident
<khush> instead of carrying the scope where it's defined, it becomes a global tree scope. not a root tree scope. explicit opt-in to the scope
<khush> so intentional process to both define and name things
<khush> you can get names from anywhere inn the tree, any shadow
<khush> if you're not using global, it acts the same as today
<khush> anders says its reasonable
<khush> right now tree scope is a tuple of ident and scope. this would just set scope to null. and null matches null
<khush> i'd like to turn this into the scoping spec
<emilio> q+
<noamr> q+
<lea> You might want to look at reference target and follow a similar design
<khush> we can resolve to republish the spec them
<khush> *then
<astearns> ack emilio
<khush> emilio: would this only be anchors or other names too
<khush> depending on the kind of data refernced by the name, might not trivial to get random data
<khush> TabAtkins: it should work for all tree scoped names and references
<khush> you'd still be able to do the encapsulation by default
<khush> only things defined by global are accessible
<khush> emilio: unless you have a global map of name or something else. you have to go through all the shadow trees
<lea> What is the use case for eg keyframes?
<khush> TabAtkins yes a global map is needed but not all references
<keithamus> q+
<khush> emilio: any other opinions? global map is based on the DOM. it's different for keyframes. That data in a centralized space in a shadow tree. keyframes won't be tree scoped names?
<westbrook> q+
<khush> TabAtkins: it's in the list of things in the scoping spec that need to be updated to be tree scoped
<astearns> ack noamr
<khush> noamr: i find globals to be messy. replacing something encapsulated with something which causes a footgun. people would want global but not too global. export semantic is better. we use the host pseudo class in the shadow dom to export names and import from outside. so you can track the chain, rather than one global map everything goes into
<khush> global is simpler than this, you just have one thing. css has shown that these things become a problem like z-index.
<khush> web apps become more complex as a result
<khush> TabAtkins hoping to get away with one additional scope. ok with import/export. it makes the concept harder. you need a way to explicitly refer, this name from keyframe is exported. this name from anchor is exported
<khush> so harder to use but not impossible
<astearns> ack keithamus
<khush> explicit import syntax would help emilio's concern. having to search everything in a global map
<khush> keithamus: what happens in a collision?
<khush> TabAtkins same as defining a name twice in the same scope. one wins, based on some order
<astearns> ack westbrook
<khush> TabAtkins you should rename using import/export
<khush> westbrook: like this idea. passing them across subtrees is good. could also extend to anchor across containing blocks?
<khush> TabAtkins wouldn't let you violate that. it's a required thing based on timing of layout. it's logically impossible, it would let you lift a name from a shadow and use it somewhere else
<khush> layout constraints are still obeyed
<khush> astearns any other comments?
<khush> q+
<fantasai> scribe+
<fantasai> khush: I just agree conceptually with what noamr said.
<fantasai> khush: a single global map is a footgun, better if can explicitly export / import / rename
<fserb> q+
<fantasai> khush: that gives an opt-in from both sides, otherwise shadow tree can export into global() stuff the outer context doesn't want
<astearns> ack khush
<fantasai> TabAtkins: how much can we get away with using global and just have shadow tree say
<khush> ScribeNick khush
<khush> TabAtkins you can grab what you want as a whole, don't have to ref everything indvidiauly. prevent accidental collision in the global space. let you know which shadow trees to recurse into so you don't have to walk all trees
<astearns> ack fserb
<noamr> It's like "import * from"
<khush> fserb: for this idea if you do something like import/export, you would have to go through the parent. Is this a use-case that matters, sharing things. you mention that on the explainer.
<noamr> q+
<astearns> ack noamr
<khush> fserb sibling shadow trees
<khush> noamr: would have to go through the parent. you can redo this with parts, probably have a semantic with this for names. anchor/ view transition are peer to peer rather than hierarchical.
<khush> fserb is there a real use-case for more direct sibling to sibling? global would do that.
<khush> fserb export from here, import in the root and then export to sibling
<khush> TabAtkins most refs walk the tree to find in parent space so just export to sibling. anchor is not doing but likely change has to happen for anchor. the use-case is important
<khush> TabAtkins: can take it back to the issue now
<khush> astearns come up with an import/export mechanism. get anders feedback
kizu commented 1 month ago

Slightly aside, but related: another thing to think about in relation to the name encapsulation are things like a potential implicit anchor created by an anchor attribute (see https://github.com/whatwg/html/pull/9144). Ideally, when we have such an implicit anchor, it would be nice to be able to “export” it as a named entity as well in some way from CSS.

Also, feels noteworthy to think if we could (and if we should) somehow make this consistent (at least in some ways) with the cross-root ARIA (and maybe other similar proposals for non-ARIA things; I don't know if there are any, but maybe?)

Westbrook commented 1 month ago

In case it's useful to the conversation: an example of passing anchor via Reference Target API: https://github.com/web-platform-tests/wpt/blob/51a20f3fb332907bdaf041da5aa6ab2d8ace62e8/shadow-dom/reference-target/tentative/anchor.html#L4