Skip to content

No "backwards compatible" way to match custom states on shadow parts #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
tbroyer opened this issue Feb 20, 2022 · 2 comments
Closed

Comments

@tbroyer
Copy link
Contributor

tbroyer commented Feb 20, 2022

The library somehow emulates custom states through attributes so they can be matched in CSS when the polyfill is used. Unfortunately this won't allow matching the custom states when the element is exposed as a shadow part, as ::part() can only be followed by a non-structural state selector or pseudo-element selector.

For instance, given a custom element c-e-inner with custom state --c-s and another custom element c-e whose shadow tree looks like:

<c-e-inner part="inner"></c-e-inner>

In browsers supporting custom states, you can match it using the following selector:

c-e::part(inner):--c-s { /* … */ }

But c-e::part(inner)[state--c-s] is illegal and thus won't work in browsers not supporting custom states.

Proposal: expose custom states on shadow parts using other shadow parts.

Specifically, with the above example, make it work with the following selectors:

c-e::part(inner--state--c-s) { /* … */ }
c-e::part(inner inner--state--c-s) { /* … */ }

In terms of implementation, whenever a custom state is added/deleted, if the element as a non-empty part DOMTokenList (ignoring any parts added to emulate the custom states), then add/delete as many corresponding parts whose name is composed as ${part}--state--${state}. This conversely mean observing attribute mutations to detect part="" changes so that whenever an element gains/loses a part, the corresponding parts added by the polyfill are added/deleted accordingly, all while trying to avoid an infinite recursion. (I would probably try to implement this by getting all non-state-related parts and ensuring that all the corresponding state-emulating parts are present, and to properly handle a removed part name then either make sure all state-emulating part matches a non-state-related part, or parse the attribute's old value to determine which parts were removed if any)

Elements using exportparts would have to manually export all possible parts, renaming them if needed:

<c-e exportparts="inner: renamed, inner--state--c-s: renamed--state--c-s"></c-e>
@calebdwilliams
Copy link
Owner

I wonder if the overhead (both in terms of bundle size and watchers) is worth the payoff. Unfortunately custom states can’t quite be polyfilled fully because CSS won’t parse rules it doesn’t recognize.

Is there a reason you couldn’t do [state—foo]::part(inner) as a stop gap instead of adding additional overhead?

@tbroyer
Copy link
Contributor Author

tbroyer commented Feb 21, 2022

[state-foo]::part(inner) wouldn't work here: it's the inner part (c-e-inner element) that has a state-foo attribute, not the containing (c-e) element.

How about only adding state--${state} parts, just like with the state--${state} attributes, so you'd match it with ::part(inner state--foo)? (::part(state--foo) would also match, but could match other parts, such as ::part(other state--foo))
That'd be much lighter weight: no need for any observer, no combinatorial explosion of parts, no overhead of having to tell non-state-related parts from state-emulating ones, just surgical add/delete the exact same way this is done currently with the attributes.
Of course the downside is that this could theoretically allow matching too many elements when used incorrectly (i.e. without the "actual part" name)

For example, with the following shadow tree:

<c-e-inner part="inner"></c-e-inner>
<c-e-inner></c-e-inner>

and both elements having state --foo, ::part(inner state-foo) would correctly match only the "exposed part" (inner), but ::part(state-foo) would also match the second element, which wasn't explicitly exposed.
I think this is an acceptable trade-off, the selectors to use for supporting and non-supporting browsers would be:

c-e::part(inner):--foo { /* using native support */ }
c-e::part(inner state--foo) { /* using the polyfill */ }

tbroyer added a commit to tbroyer/element-internals-polyfill that referenced this issue Feb 21, 2022
tbroyer added a commit to tbroyer/element-internals-polyfill that referenced this issue Feb 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants