Is there any way to use composition to alter the CSS of an web-component with a shadow dom? - shadow-dom

my team lead has decided to use LitElement to create framework-agnostic web components, which makes sense, as we're creating a company-wide UI library that would be ideal to be used with Vue2, Vue3, React, Preact, and others.
However, I'm extremely worried about styling. Right now with our current (Vue 2) based UI library, if one of our teams needs to override the internal styling of an element for whatever reason (usually edge cases), one of the things they can do is use CSS Composition in order to do exactly that. Something like:
// in components/my-element.js
class MyElement extends LitElement {
render(){
return html`
<div class="stackui-my-element">
<p class="paragraph">A paragraph</p>
</div>
`;
}
}
customElements.define('my-element', MyElement);
// in app/businessthing.js
import React from 'react';
import {css} from 'styled-components'
const customCSS = css`
.this-thing {
& .stackui-my-element .paragraph {
border: 2px dotted pink;
}
}
`
export default (props) => <div class="this-thing"><my-element /></div>
As far as I can tell, the above code won't work ("My Paragraph" would not be surrounded with pink polka dots) because MyElement has its own shadow dom, and you can't reach in it from without to change the styles of the element.
Is there some sort of exception to the rule, or can you reach inside the shadow dom somehow? Other than losing CSS encapsulation, what are the other effects of disabling the shadow dom?

As you mention, when using Shadow DOM, style encapsulation would prevent users of the component from affecting the styling of the component's internals.
However, there are several ways to achieve what you want with different degrees of flexibility, all of them are part of how the Shadow DOM spec behaves and not Lit-specific, so, you could use this with Vanilla Web Components or components created with other libraries too.
Which of the following ways works better will depend on how strict you want to enforce styling rules for your component.
All the samples below will assume the internal DOM of the component looks like your sample
<div class="stackui-my-element">
<p class="paragraph">A paragraph</p>
</div>
Use custom CSS properties (CSS variables)
This approach is good when you want to limit the customization to only specific parts.
For example, to allow for the user to only be able to change the border for the paragraph, you could use a CSS variable with a fallback for the border in the style for your component's shadow DOM like this:
.paragraph {
border: var(--myel-paragraph-border, 1px solid black);
}
And then, users who wish to customize said border could just change the value for that CSS variable through inline styles or a class.
<style>
.fancy-border {
--myel-paragraph-border: 2px dotted pink;
}
</style>
<my-element class="fancy-border"></my-element>
The biggest limitation of this approach is that you would need to add a CSS variable for every property you wish to allow to be customized.
However, this can be an advantage for some use cases (like say, strict design systems) because it will not allow users to customize anything you don't wish to customize.
Use Shadow Parts
Shadow parts are one of the newer parts of the shadow DOM spec but browser support is pretty good by this point. They allow you to define arbitrary parts of your component you wish to be fully customizable from outside the shadow DOM.
To use them, you need to add the part attribute to the HTML node you wish to define as the part.
<div class="stackui-my-element">
<p part="paragraph" class="paragraph">A paragraph</p>
</div>
And when using the component, add the ::part() selector to the styles so that you can customize that specific part rather than the component host. It works pretty similar to how you would styles things such as a native input placeholder and so on.
<style>
.fancy::part(paragraph) {
/* you can do whatever you want here */
border: 2px dotted pink;
color: blue;
font-style: italic;
}
</style>
<my-element class="fancy"></my-element>
As you can see, shadow parts will allow you to override every style applied to the node, so, you must be careful when to use them as users might end up being able to customize things you don't want them too.
Final notes:
You could also achieve a similar thing using slots, but that might not be an easy change considering the contents of your question.
Here's an article in case you want more info on how styling Shadow DOM from outside the component works. (Disclaimer: I'm the author of that article.)

Related

How to center align the text in Vaadin TextField

I read the CSS styling section (https://vaadin.com/docs/v14/flow/styling/styling-components) and it mentions that global CSS doesnt affect the 'INPUT" field in the shadow DOM, So styling has to be added to shdaow DOM, But unfortunately no where does it explicitly say HOW to add the styling to the shadow DOM. Note. im using mainly Flow pure java with a bit of CSS.
I tried retrieving the elementt from component then retrieving the shadowRoot, then from root, retrieve the 'input' element to add styling to it, unfortunately that didnt work, shadowroot was null (this code executed from the onAttach() method in the view class)
private void setTextAlignCenterForTextFields(TextField textField) {
//find the internal 'Input' and set the styling to text-align=center, unfortunately
// you cant do that with global css, since the 'input' element is in shadow root
textField.getElement()
.getShadowRoot()
.get()
.getChildren()
.filter( elem -> "INPUT".equalsIgnoreCase(elem.getTag()))
.forEach(inputElem -> inputElem.getStyle().set("text-align", "center"));
}
Any ideas would be appreciated. I'm using Vaadin version 14.5.1.
There's already a theme variant to align the text
centerTextField.addThemeVariants(TextFieldVariant.LUMO_ALIGN_CENTER);
see https://vaadin.com/components/vaadin-text-field/java-examples/theme-variants
As for how to attach CSS to shadow root, basically use themeFor, see https://vaadin.com/docs/v14/flow/styling/importing-style-sheets/#component-styles
You can use CSS to target the value part:
.textfieldClass::part(value) {
text-align: center;
}
This video explains styling CSS parts: https://youtu.be/Y0uxb4ga44Y

How to use an Angular Material mat-form-field, but with a normal placeholder

Angular Material form fields are potentially very convenient because they add a bunch of classes to the surrounding element depending on whether the field is selected, empty, filled, etc. I want to use these classes to customize the style of the label and other custom elements placed inside the field container (example: making the label change color when the input is focused).
The problem is that Angular Material also adds a bunch of other properties, styles and elements that I don't want to deal with. Even if I add floatLabel="never" and floatPlaceholder="never", the placeholder is still removed from the input and turned into a label, which is positioned relative to the entire container. If I place other elements inside the mat-form-field element (like a regular label), this messes up the positioning of the placeholder-turned-label, causing it to appear outside the input.
Is there any way I can make Angular Material not turn the placeholder into a label, but just leave it as a normal placeholder?
So I wasn't able to actually fix it properly, but I was able to get around the issue by adding styles to undo the style changes that Angular adds.
mat-form-field.mat-form-field-hide-placeholder .mat-input-element::placeholder{
color: #ccc !important;
-webkit-text-fill-color: #ccc !important;
}
mat-form-field.mat-form-field-hide-placeholder .mat-form-field-label-wrapper{
display: none;
}
It would be nicer if there was a way to not have the .mat-form-field-hide-placeholder class added in the first place, but until someone figures this out this will have to do.
Turns out all you need to do is add appearance="none" to the field tag, e.g.:
<mat-form-field appearance="none">
<input matInput [(ngModel)]="email" placeholder="Email">
</mat-form-field>

Load Angular Material before own styles

We have an Angular project, with Material and we're having some issues with overriding styles.
For example, if we want to change the border-radius globally on <mat-card>, we currently need to add important to the styles:
.mat-card { border-radius: $some-var !important; }
This seems to me to be caused by the material styles loading after our own custom styles. At least according to "traditional" CSS standards. So usually you could just change the load order around, and the last loaded styles would overwrite the previous.
Is there a way to achieve this? Or how are we supposed to style these kinds of elements, without adding !important all over?
You are not really supposed to "style these kinds of elements" - that's not what Angular Material is about. But some customization can be done - and a guide is available: https://v6.material.angular.io/guide/customizing-component-styles.
You especially need to understand how style is encapsulated and dynamically applied. You can control when the global Angular Material style sheet is loaded in the "traditional" way, but you cannot control when all component style is applied because some of it is dynamic. If you hope to completely restyle everything - you should probably consider a different library as it is not always merely a matter of redefining class properties.

Controlling spacing size in Vaadin 12/13 horizontal/vertical layouts

In Vaadin 12/13, we can turn on/off spacing in a veritcal/horizontal layout by calling setSpacing(...). But, what if we want spacing but a much smaller amount of spacing? How can we (via Java) set the spacing to a much smaller amount? (For margins and padding, I figured out the css -- it's a straightfoward this.getStyle().set("margin", "2rem") or this.getStyle().set("padding", "2rem") etc., but I couldn't figure it out for spacing. Also, is it "dangerous" if we also run setSpacing(true) (if we do it before any code we write to explicilty set a different value for the spacing?)
Probably the easiest way to customize the spacing is to set using the predefined custom properties as described in this document. As you see, the "padding" is the right way to do.
https://cdn.vaadin.com/vaadin-lumo-styles/1.4.1/demo/sizing-and-spacing.html#spacing
While writing all things on the server/JVM side seem tempting, you end up littering your code with style manipulation.
Usually a better place do setup things like that is in the actual styles of your application. This is an example now to do it (uses v13 beta 2, the code is Groovy - the take away there is just to add a theme to the element).
<dom-module id="vertical-layout-different-spacing" theme-for="vaadin-vertical-layout">
<template>
<style>
:host([theme~="spacing"][theme~="xs"]) ::slotted(*) {
margin-top: 8px;
}
:host([theme~="spacing"][theme~="xl"]) ::slotted(*) {
margin-top: 32px;
}
</style>
</template>
</dom-module>
def demoContent = { theme ->
new VerticalLayout(
*[1, 2, 3].collect{ new Div(new Text("Text $it")) }
).tap {
element.themeList.add(theme)
}
}
content.add(
// styles `xs` and `xl` are defined in the style override
demoContent('xs'),
demoContent('m'),
demoContent('xl'),
)
If you are using Lumo and you are on v13 already, there is a compact variant of the theme, if that is all you are after:
https://vaadin.com/releases/vaadin-13#compact-theme
If you are using the Material theme, then there is already built in support for different spacings. See https://cdn.vaadin.com/vaadin-material-styles/1.2.0/demo/ordered-layouts.html ; The names forthe theme to add are e.g. spacing-xl

Panel with Collapsible Set and Listview set to em25 too wide in 'Overlay' Mode

I have some nested recursive functions which dynamically create collapsible with listviews that are in a side panel. I found the default 17em to be too small, especially as the nested text starts to get short. So I found the styles in the css which set it and I overrode those to use 25em. This might be a bit too much after testing on some devices. However I digress.
The thing I am here to ask is why my collapsible overflows the panel when I use data-display="overlay", when I set it to 'reveal' it looks fine. I thought it might be all my nested content, so I made a fiddle with static content here: http://jsfiddle.net/LF6UR/
<div data-role="panel" id="left-panel" data-display="overlay" data-position="left" data-position-fixed="true" data-swipe-close="true" data-dismissible="true">
and I can see it is not that, perhaps there is some other CSS property for the panel that I am not aware of. There seem to be lots of niggly little settings to get to know with this framework. Hope someone out there can help because I really think 'overlay' is better than pushing my main content area.
jQM adds a negative left and right margin to collapsibles within the collapsible set. You can override the margin like this:
.ui-collapsible-set .ui-collapsible{
margin-left: 0;
margin-right: 0;
}
Updated FIDDLE
Also, changing your collapsible set to data-inset="true" fixes the issue.
a solution without setting collapsibles to inset...which is important because I have nested collapsibles is to simply set the 'magic' .ui-panel-inner class which JQM puts in as an 'enhancement' but which makes it a bit difficult for traditional webdevelopers to know to apply styles to their controls.
.ui-panel-inner {
/*width: 25em;*/
padding: .2em 1.2em;
}
http://jsfiddle.net/magister/LF6UR/8/

Resources