-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
197 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
--- | ||
chapter: 8 | ||
subtitle: Bringing CSS into ShadowDOM | ||
hidden: false | ||
--- | ||
|
||
Components with ShadowDOM typically want to introduce some CSS into their ShadowRoots. This is done with the use of [`adoptedStyleSheets`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/adoptedStyleSheets). Catalyst provides a `@style` decorator to more easily add CSS to your component. | ||
|
||
If your CSS lives in a different file, you can import the file with using the `assert { type: 'css' }` import assertion. You might need to configure your bundler tool to allow for this. If you're unfamiliar with this feature, you can [check out the web.dev article on CSS Module Scripts](https://web.dev/css-module-scripts/): | ||
|
||
```typescript | ||
import {controller, style} from '@github/catalyst' | ||
import DesignSystemCSS from './my-design-system.css' assert { type: 'css' } | ||
|
||
@controller | ||
class UserRow extends HTMLElement { | ||
@style designSystem = DesignSystemCSS | ||
|
||
connectedCallback() { | ||
this.attachShadow({ mode: 'open' }) | ||
// adoptedStyleSheets now includes our DesignSystemCSS! | ||
console.assert(this.shadowRoot.adoptedStyleSheets.includes(this.designSystem)) | ||
} | ||
} | ||
``` | ||
|
||
Multiple `@style` tags are allowed, each one will be applied to the `adoptedStyleSheets` meaning you can split your CSS without worry! | ||
|
||
```typescript | ||
import {controller} from '@github/catalyst' | ||
import UtilityCSS from './my-design-system/utilities.css' assert { type: 'css' } | ||
import NormalizeCSS from './my-design-system/normalize.css' assert { type: 'css' } | ||
import UserRowCSS from './my-design-system/components/user-row.css' assert { type: 'css' } | ||
|
||
@controller | ||
class UserRow extends HTMLElement { | ||
@style utilityCSS = UtilityCSS | ||
@style normalizeCSS = NormalizeCSS | ||
@style userRowCSS = UserRowCSS | ||
|
||
connectedCallback() { | ||
this.attachShadow({ mode: 'open' }) | ||
// adoptedStyleSheets now includes our 3 stylesheets! | ||
console.assert(this.shadowRoot.adoptedStyleSheets.length === 3) | ||
} | ||
} | ||
``` | ||
|
||
### Defining CSS in JS | ||
|
||
The `@style` decorator takes a constructed [`CSSStyleSheet`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet) object. These must be constructed in JavaScript, but can be generated by helper libraries or custom loaders. If, for example, you like writing your CSS the same file as your element, you can manually create a CSSStyleSheet: | ||
|
||
```typescript | ||
import {controller, style} from '@github/catalyst' | ||
|
||
const sheet = new CSSStyleSheet() | ||
sheet.replaceSync(` | ||
:host { | ||
display: flex | ||
} | ||
`) | ||
|
||
@controller | ||
class UserRow extends HTMLElement { | ||
@style componentCSS = sheet | ||
|
||
connectedCallback() { | ||
this.attachShadow({ mode: 'open' }) | ||
} | ||
} | ||
``` | ||
|
||
Alternatively you can import one, as long as the return value is a [Constructable CSSStyleSheet](https://web.dev/constructable-stylesheets/): | ||
|
||
```typescript | ||
import {controller, style} from '@github/catalyst' | ||
import {css} from '@acme/cool-css' | ||
|
||
@controller | ||
class UserRow extends HTMLElement { | ||
@style componentCSS = css` | ||
:host { | ||
display: flex | ||
} | ||
` | ||
|
||
connectedCallback() { | ||
this.attachShadow({ mode: 'open' }) | ||
console.assert(this.componentCSS instanceof CSSStyleSheet) | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import type {CustomElementClass, CustomElement} from './custom-element.js' | ||
import {controllable, attachShadowCallback} from './controllable.js' | ||
import {createMark} from './mark.js' | ||
import {createAbility} from './ability.js' | ||
|
||
const [style, getStyle, initStyle] = createMark<CustomElement>( | ||
({name, kind}) => { | ||
if (kind === 'setter') throw new Error(`@style cannot decorate setter ${String(name)}`) | ||
if (kind === 'method') throw new Error(`@style cannot decorate method ${String(name)}`) | ||
}, | ||
(instance: CustomElement, {name, kind, access}) => { | ||
return { | ||
get: () => (kind === 'getter' ? access.get!.call(instance) : access.value), | ||
set: () => { | ||
throw new Error(`Cannot set @style ${String(name)}`) | ||
} | ||
} | ||
} | ||
) | ||
|
||
export {style, getStyle} | ||
export const stylable = createAbility( | ||
<T extends CustomElementClass>(Class: T): T => | ||
class extends controllable(Class) { | ||
[key: PropertyKey]: unknown | ||
|
||
// TS mandates Constructors that get mixins have `...args: any[]` | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
constructor(...args: any[]) { | ||
super(...args) | ||
initStyle(this) | ||
} | ||
|
||
[attachShadowCallback](root: ShadowRoot) { | ||
super[attachShadowCallback]?.(root) | ||
const styleProps = getStyle(this) | ||
if (!styleProps.size) return | ||
const styles = new Set([...root.adoptedStyleSheets]) | ||
for (const name of styleProps) styles.add(this[name] as CSSStyleSheet) | ||
root.adoptedStyleSheets = [...styles] | ||
} | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import {expect, fixture} from '@open-wc/testing' | ||
import {style, stylable} from '../src/styleable.js' | ||
const html = String.raw | ||
|
||
type TemplateString = {raw: readonly string[] | ArrayLike<string>} | ||
const css = (strings: TemplateString, ...values: unknown[]): CSSStyleSheet => { | ||
const sheet = new CSSStyleSheet() | ||
sheet.replaceSync(String.raw(strings, ...values)) | ||
return sheet | ||
} | ||
|
||
describe('Styleable', () => { | ||
const globalCSS = ({color}: {color: string}) => | ||
css` | ||
:host { | ||
color: ${color}; | ||
} | ||
` | ||
|
||
@stylable | ||
class StylableTest extends HTMLElement { | ||
@style foo = css` | ||
body { | ||
display: block; | ||
} | ||
` | ||
@style bar = globalCSS({color: 'rgb(255, 105, 180)'}) | ||
|
||
constructor() { | ||
super() | ||
this.attachShadow({mode: 'open'}).innerHTML = html`<p>Hello</p>` | ||
} | ||
} | ||
window.customElements.define('stylable-test', StylableTest) | ||
|
||
it('adoptes styles into shadowRoot', async () => { | ||
const instance = await fixture<StylableTest>(html`<stylable-test></stylable-test>`) | ||
expect(instance.foo).to.be.instanceof(CSSStyleSheet) | ||
expect(instance.bar).to.be.instanceof(CSSStyleSheet) | ||
expect(instance.shadowRoot!.adoptedStyleSheets).to.eql([instance.foo, instance.bar]) | ||
}) | ||
|
||
it('updates stylesheets that get recomputed', async () => { | ||
const instance = await fixture<StylableTest>(html`<stylable-test></stylable-test>`) | ||
expect(getComputedStyle(instance.shadowRoot!.children[0]!).color).to.equal('rgb(255, 105, 180)') | ||
globalCSS({color: 'rgb(0, 0, 0)'}) | ||
expect(getComputedStyle(instance.shadowRoot!.children[0]!).color).to.equal('rgb(0, 0, 0)') | ||
}) | ||
|
||
it('throws an error when trying to set stylesheet', async () => { | ||
const instance = await fixture<StylableTest>(html`<stylable-test></stylable-test>`) | ||
expect(() => (instance.foo = css``)).to.throw(/Cannot set @style/) | ||
}) | ||
|
||
describe('css', () => { | ||
it('returns the same CSSStyleSheet for subsequent calls from same template string', () => { | ||
expect(css``).to.not.equal(css``) | ||
const mySheet = () => css`` | ||
expect(mySheet()).to.equal(mySheet()) | ||
}) | ||
}) | ||
}) |