How do I use Tippy in shadow DOM with scoped CSS? - css

I want to use Tippy.js in my shadow DOM.
Since scripts can access my shadow DOM but styles can't, I tried to inline the Tippy CSS within the shadow DOM and link Popper's and Tippy's JS outside. Here is a demo of it not working.
I need the CSS to be scoped so I have the following constraint:
<style>
:host {
all: initial; /* 1st rule so subsequent properties are reset. */
display: block;
contain: content; /* Boom. CSS containment FTW. */
/* any other CSS, so this is where I inlined the Tippy's */
}
</style>

For people getting here from google and wandering what's the current process for putting tippy into shadow dom (e.g. for using with browser extension). The following works for me:
const shadowRoot = initShadowRoot()
function initShadowRoot() {
const shadowContainer = document.createElement("div")
const shadowRoot = shadowContainer.attachShadow({mode: "open"})
let style = document.createElement('style')
style.innerText = shadowCss // inline or use a bundler to give you a string representation
shadowRoot.appendChild(style)
document.body.appendChild(shadowContainer)
return shadowRoot
}
//---
//elsewhere when initializing tippy:
tippy(elment, {
// ...
// in shadow dom to avoid affecting the page styles
appendTo: () => shadowRoot,
//...
})

Get the target element, create a style tag to hold the inline CSS and append the style tag as a child.
const anchor = document.querySelector('#blog-header')
const style = document.createElement('style')
style.type = 'text/css'
const stylesheet = // inline Tippy.js CSS here or fetch it from somewhere else
style.appendChild(document.createTextNode(stylesheet))
anchor.content.prepend(style)

Related

Is it possible to prevent :root styles from bleeding into a shadowed component?

I've created a web component using Stencil that uses the shadow DOM and needs to ignore any CSS that has been added to the page (it has its own stylesheet). To that end, I've included the usual fix to the component stylesheet:
:host {
all: initial;
}
This works to reset any base styles set on the page using the <style> tag or an external stylesheet. However, if an inherited style like font-size is defined on the :root or html elements, its value is used when computing a relative value (e.g. 1rem) instead of using the browser's base size or a font size specified in my component's CSS.
In this example, my dev tools are showing that a font-size: 160px value applied to the :root is being used to calculate the final font size for an element that has been given font-size: 0.875rem, even though I've used the CSS reset above and tried to apply my own base of 16px.
I've done a fair bit of research regarding styles applied to the :root element, but have not found an answer. Is there a way to override styles used to compute relative values if they are set on the :root?
Here is a playground: https://jsfiddle.net/WebComponents/L5kbf8qm/
<style>
:root {
font-size: 15px;
}
</style>
<my-element all="initial"></my-element>
<my-element all="inherit">Note we get the UA 8px margin back</my-element>
<script>
customElements.define("my-element", class extends HTMLElement {
connectedCallback() {
let all = this.getAttribute("all");
this.attachShadow({mode:"open"})
.innerHTML = `<style>`+
` :host{all:${all}}` +
` div{font-size:2rem;background:pink}`+
`</style>` +
`<div>Component all:${all}</div>`+
`<span id=result ></span><slot></slot>`;
const add = (name) => {
let size = window.getComputedStyle(this.shadowRoot.querySelector(name))["font-size"];
this.shadowRoot.querySelector("#result").append(`${name} font-size is: `, size, document.createElement("br"));
}
add("div");
add("span");
}
})
</script>

Vue conditional style tag in single file component

I have started development on a vue web component library. Members of my team asked for the potential to remove default styles via an HTML attribute on the web component. I know that I could use CSS class bindings on the template elements, however, I was wondering if there is a way to conditionally include the style tag itself so that I would not need to change the class names in order to include the base styles or not.
Example of a component's structure
<template>
<section class="default-class" />
</template>
<script>
export default {
props: {
useDefault: Boolean
}
}
</script>
<style>
// Default styles included here
// Ideally the style tag or it's content could be included based off useDefault prop
</style>
Potential implementation
<web-component use-default="false"></web-component>
As I read your question; you want to keep <style> both affecting Global DOM and shadowDOM
One way is to clone those <style> elements into shadowDOM
But maybe ::parts works better for you; see: https://meowni.ca/posts/part-theme-explainer/
customElements.define("web-component", class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:"open"})
.innerHTML = "<div>Inside Web Component</div>";
}
connectedCallback() {
// get all styles from global DOM and clone them inside the Web Component
let includeStyles = this.getAttribute("clone-styles");
let globalStyles = document.querySelectorAll(`[${includeStyles}]`);
let clonedStyles = [...globalStyles].map(style => style.cloneNode(true));
this.shadowRoot.prepend(...clonedStyles);
}
});
<style mystyles>
div {
background: gold
}
</style>
<style mystyles>
div {
color: blue
}
</style>
<div>I am Global</div>
<web-component clone-styles="mystyles"></web-component>

Lit-element - :host selector is not triggering render on Safari

In a project I'm working on, I have this LitElement component, which is absolute-positioned and determines its left or top locations according to its reactive properties.
I've encountered a problem in Safari only, including iOS Safari/Chrome/Firefox. The element has updated its shadow styles, but in the view it does not move at all. I realized its a render issue when I found that when the cursor hovers the element, or exits the browser view, the element pops to the expected location.
I managed to reproduce the problem with a simpler code:
my-elem.ts:
import { LitElement, html, property, customElement, css } from 'lit-element';
#customElement('my-elem')
export class MyElem extends LitElement {
#property({ type: Number, reflect: true })
left: number = 30
static get styles() {
return css`
:host { position: absolute; }
div { position: absolute; }
`;
}
render() {
return html`
<style>
:host { left: ${this.left}px; }
</style>
<div> Hello </div>
`;
}
}
index.html:
<html lang="en">
<head>
<script src="my-elem.ts"></script>
</head>
<body>
<my-elem id="my-elem" left="50"></my-elem>
<button id="move-btn">move</button>
<script>
const elem = document.getElementById('ma-elem');
const moveBtn = document.getElementById('move-btn');
moveBtn.onclick = function() {
elem.left += 30;
}
</script>
</body>
</html>
I found that this happens only on :host selector. If the shadowed styling updates the div's left, it renders without any problems.
I wish to avoid forcing the browser to layout/paint, due to performance issues.
I think the root of your problem is that you are trying to use an expression inside a style element in your render template.
The LitElement guide mentions that using them that way has limitations and can cause performance issues. I think your problem is just facing one of them so you should remove that style element.
As an alternative, since you want to affect the host element, you can actually do this in an easier way if you just don't use a property but style the host directly.
So when using my-elem the code would look like:
<my-elem style="left: 30px;"></my-elem>
This is because styles applied to :host in shadow DOM have less priority than those applied to it using classes or the style attribute by its parent.
Alternatively, if you really want to keep the property, you can create property accessors for the left property and set the style to the host from there like this:
export class MyElem extends LitElement {
// other code
set left(value) {
const oldValue = this.left;
this._left = value;
this.style.setProperty('left', `${value}px`)
this.requestUpdate('left', oldValue);
}
get left() {
return this._left;
}
}

Responsive Props in Vue Component

I have a prop called src in a Vue Component that binds to a :style like this:
<template>
<section :class="color" class="hero" :style="{ backgroundImage: src && 'url(' + src + ')' }">
<slot></slot>
</section>
</template>
<script>
export default {
props: ['src', 'color']
}
</script>
What I would like to do is to create a list of responsive props that get used depending on the device or screen size of the site visitor.
For instance, I imagine a list of props like src-sm, src-md, src-lg, etc. The user would enter different image urls for different device sizes and the style attr would use the appropriate url depending on the screen/size.
Is this possible in VueJS. If so, any idea how?
Thanks.
Unfortuently what you are trying to do is not trivial. This is because inline style tags can not accept media queries.
The spec declares:
The value of the style attribute must match the syntax of the contents of a CSS declaration block
Solution 1:
This solution is the simplest, perhaps not entirely what you are looking for.
It works by including img elements, and showing an hiding them via CSS.
<template>
<div>
<img class="image--sm" :src="src.sm" />
<img class="image--md" :src="src.md" />
<img class="image--lg" :src="src.lg" />
</div>
</template>
<script>
export default {
props: {
src: Object
}
}
</script>
<style>
.image--md,
.image--lg {
display: none;
}
#media (min-width: 400px) {
.image--sm {
display: none;
}
.image--md {
display: block;
}
}
#media (min-width: 600px) {
.image--md {
display: none;
}
.image--lg {
display: block;
}
}
</style>
Example: https://jsfiddle.net/h3c5og08/1/
Solution 2:
Image tags may not be the desired effect you are trying to achieve. This solution creates a style tag in the head and injecting the css content to change the background images.
You can not have style tags in Vue template. It will throw an error like:
Templates should only be responsible for mapping the state to the UI. Avoid placing tags with side-effects in your templates, such as , as they will not be parsed.
As the error describes vue is designed the map state the UI. Using style tags in the template is prohibited because you can cause leaks to the outer world.
Although you can not declaratively styles in a template, we can use a bit of JS in the mounted hook of the component to add targetted and dynamic styles.
First we will need to constrain dynamic styles to this element. We can use the internal id of the created component this._uid, attaching to scope the css. (Note this is internal API so can be subject to change)
<template>
<div class="image" :data-style-scope="_uid">
</div>
</template>
The next part is to generate the style in a computed property, to later inject into a style block. You can expand on this computed property, to conditionaly assign properties ect. Note: keep the properties to the dynamic values only.
css () {
const selector = `.image[data-style-scope="${this._uid}"]`
const img = val => `${selector} { background-image: url("${val}"); }`
const sm = img(this.sm)
const md = img(this.md)
const lg = img(this.lg)
return `
${sm}
#media (min-width: 200px) { ${md} }
#media (min-width: 300px) { ${lg} }
`
}
This generated string from the css computed property is what we will now use when creating the style tag at mount. At mount we create a style node and append to the head. Assigning the nodes to the vm for references.
Using the references in the vm we can watch changes to the computed updating the style node.
Remember to clean up before destorying the component, removing the style node.
{
data () {
return {
// Reference data properties
style: null,
styleRef: null
}
},
mounted () {
// Create style node
let style = document.createElement('style')
style.type = "text/css"
style.appendChild(document.createTextNode(''))
// Assign references on vm
this.styleRef = style
this.style = style.childNodes[0]
// Assign css the the style node
this.style.textContent = this.css
// Append to the head
document.head.appendChild(style)
},
beforeDestroy () {
// Remove the style node from the head
this.style.parentElement.removeChild(this.style)
},
computed: {
css () {
// ...
}
},
watch: {
css (value) {
// On css value change update style content
this.style.textContent = this.css
}
}
}
Working Example: https://jsfiddle.net/bLkc51Lz/4/
You could also try the module described here: https://alligator.io/vuejs/vue-responsive-components/ which is called vue-responsive-components
It lets the component change its CSS depending on its own width (not on the entire browser's width)

Can Reactjs programmatically handle :before?

I somehow have to programmatically set the width of the :before for a div.
<div className="something" style={someStyle}> </div>
How can I define the someStyle so that the width of the :before of .something``div can change accordingly??
Yes, you can programmatically change the value of pseudo-elements like ::before, ::after in react.
Here is a trick.
app.js
const widthVar = 34;
const someStyle = {
"--width": widthVar
}
<div className="something" style={someStyle}> </div>
style.css
.something:before{
width: var(--width),
// remaining code
}
Pseudo elements cannot be styled with inline styles as explained in https://stackoverflow.com/a/14141821/368697. You will have to style the something class name in a stylesheet with the .something:before selector. This is not a limitation of React but rather a design choice for HTML + CSS.
If you need to programmatically change the width of the pseudo :before element, it is probably more appropriate as a regular DOM element rendered by React.
I got insight from #am2505 to use CSS variables as it helped me however, this way avoids inline styling.
HTML
<div className="something"> </div>
CSS
:root {
--width: <yourDefaultValue>
}
.something:before{
width: var(--width),
}
JS
const changeWidth=() => {
let root = document.querySelector(':root');
root.style.setProperty('--width', '<yourNewValue>px');
call the function at the event you want the width to change.
The changeWidth function can be further modified to dynamically work with state using conditional statements.

Resources