Skip to content
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

Allowing for more complex slotted styling by rewriting host rules into global CSS rules #1038

Open
sirisian opened this issue Nov 20, 2023 · 0 comments

Comments

@sirisian
Copy link

It's been a while since the discussion of allowing more complex ::slotted() styling. Being able to style only the slotted elements in a specific slot is still missing.

Goal: To apply complex styling rules to slotted elements using their rendered order.

This kind of nested styling with ::slotted() is needed to properly style items based on their rendered order in their specific slots. Attempting to use basic global CSS rules become fragile when a component has multiple slots as pseudo-classes referencing positions are evaluating on all the children not just the specific slot elements.

Previous discussions on allowing complex rules for ::slotted() have mentioned performance issues. https://stackoverflow.com/a/61631668/254381 ( #889 (comment) )

However, the performance issues appear to be related to finding rules. This leads to many situations where one can simply write a global CSS rule that does an almost identical behavior. The major difference being it runs the selector on the light dom and not the rendered slotted elements, so the list of elements is different as mentioned before. The fix would then be to modify the global rule to only style the slotted elements.

Example 1

A simple example is slot:not([name])::slotted(ul:first-child > li) {}

<my-component>
	<div slot="other"></div>
	<ul>
		<li>a</li>
	</ul>
	<ul>
		<li>b</li>
	</ul>
</my-component>

When li is looking for rules it would need to search up the tree for all slots and then their rules. Rewriting the rule in the global CSS as:

my-component > ul:first-child > li {}

But it needs to know the ul must be in the default slot so extra metadata is added to the rule:

my-component:slot(slot:not([name])) > ul:first-child > li {}

When the style is first processed like a regular global CSS rule it would be valid for both li tags. The metadata :slot() would reevaluate the rule in the context of the rendered position in the slot. In theory this transformed rule basically tags the elements affected by the rule to run the rule in the slot rendered context.

I'm not familiar with implementations, but would such an idea work and bypass any performance issue?

Example 2

If that idea does work, I have a more specific example of how I want to use this. This uses some syntax I described in #1037 to handle inheritance. The goal in this example is to apply custom styling to children nodes that have previous and next siblings. (Note: I'm introducing syntactic sugar - sibling selector which would just transform to the :has() pseudo-class):

Rough example with a global CSS rule: https://jsfiddle.net/sirisian/kjzt2qdb/15/

<ui-treenode>
	<ui-treenode>
		<ui-treenode2></ui-treenode2>
		<span slot="test">def</span>
		<ui-treenode2></ui-treenode2>
		<ui-treenode2></ui-treenode2>
	</ui-treenode>
	<ui-treenode></ui-treenode>
	<ui-treenode></ui-treenode>
</ui-treenode>
const uiTreeNodeStyle = new CSSStyleSheet();
uiTreeNodeStyle.replaceSync(`:host {
	display: block;
}
slot:not([name]) {
	display: flex;
	flex-flow: column nowrap;
	padding-left: 20px;
}
slot:not([name])::slotted(ui-treenode:has(- :is(ui-treenode / all))::part(text)) {
	border-left: 10px solid red;
}
slot:not([name])::slotted(ui-treenode:has(+ :is(ui-treenode / all))::part(text)) {
	border-right: 10px solid blue;
}
`);

class UITreeNode extends HTMLElement {
	constructor() {
		super();
		const $span = document.createElement('span');
		$span.textContent = 'abc';
		$span.part = 'text';
		const $namedSlot = document.createElement('slot');
		$namedSlot.name = 'test';
		$span.appendChild($namedSlot);
		this.attachShadow({mode: 'open'}).append(
			$span,
			document.createElement('slot')
		);
		this.shadowRoot.adoptedStyleSheets.push(uiTreeNodeStyle);
	}
}
customElements.define('ui-treenode', UITreeNode);

class UITreeNode2 extends UITreeNode {
	constructor() {
		super();
	}
}
customElements.define('ui-treenode2', UITreeNode2);

The two CSS lines used to style relative to siblings:

slot:not([name])::slotted(:is(ui-treenode / all):has(- :is(ui-treenode / all))::part(text)) {
	border-left: 10px solid red;
}
slot:not([name])::slotted(:is(ui-treenode / all):has(+ :is(ui-treenode / all))::part(text)) {
	border-right: 10px solid blue;
}

Would be converted to the global rules:

:is(ui-treenode / all):slot(slot:not([name])) > :is(ui-treenode / all) + :is(ui-treenode / all)::part(text) {
	border-left: 10px solid red;
}
:is(ui-treenode / all):slot(slot:not([name])) > :is(ui-treenode / all):has(+ :is(ui-treenode / all))::part(text) {
	border-right: 10px solid blue;
}

The rule runs as normal matching the ui-treenode elements that have ui-treenode's before and after respectively. But

Conclusion

I have further thoughts about ::part() as well if this is possible.

Also as a lot of these issues are very old (with discussions going back before 2018) so I don't know who to tag that would be familiar with the implementation feasibility.

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

1 participant