Skip to content

Commit 5245044

Browse files
committed
add stylable behavior
1 parent bf71d4a commit 5245044

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed

docs/_guide/styleable.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
chapter: 8
3+
subtitle: Bringing CSS into ShadowDOM
4+
hidden: false
5+
---
6+
7+
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.
8+
9+
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/):
10+
11+
```typescript
12+
import {controller, style} from '@github/catalyst'
13+
import DesignSystemCSS from './my-design-system.css' assert { type: 'css' }
14+
15+
@controller
16+
class UserRow extends HTMLElement {
17+
@style designSystem = DesignSystemCSS
18+
19+
connectedCallback() {
20+
this.attachShadow({ mode: 'open' })
21+
// adoptedStyleSheets now includes our DesignSystemCSS!
22+
console.assert(this.shadowRoot.adoptedStyleSheets.includes(this.designSystem))
23+
}
24+
}
25+
```
26+
27+
Multiple `@style` tags are allowed, each one will be applied to the `adoptedStyleSheets` meaning you can split your CSS without worry!
28+
29+
```typescript
30+
import {controller} from '@github/catalyst'
31+
import UtilityCSS from './my-design-system/utilities.css' assert { type: 'css' }
32+
import NormalizeCSS from './my-design-system/normalize.css' assert { type: 'css' }
33+
import UserRowCSS from './my-design-system/components/user-row.css' assert { type: 'css' }
34+
35+
@controller
36+
class UserRow extends HTMLElement {
37+
@style utilityCSS = UtilityCSS
38+
@style normalizeCSS = NormalizeCSS
39+
@style userRowCSS = UserRowCSS
40+
41+
connectedCallback() {
42+
this.attachShadow({ mode: 'open' })
43+
// adoptedStyleSheets now includes our 3 stylesheets!
44+
console.assert(this.shadowRoot.adoptedStyleSheets.length === 3)
45+
}
46+
}
47+
```
48+
49+
### Defining CSS in JS
50+
51+
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:
52+
53+
```typescript
54+
import {controller, style} from '@github/catalyst'
55+
56+
const sheet = new CSSStyleSheet()
57+
sheet.replaceSync(`
58+
:host {
59+
display: flex
60+
}
61+
`)
62+
63+
@controller
64+
class UserRow extends HTMLElement {
65+
@style componentCSS = sheet
66+
67+
connectedCallback() {
68+
this.attachShadow({ mode: 'open' })
69+
}
70+
}
71+
```
72+
73+
Alternatively you can import one, as long as the return value is a [Constructable CSSStyleSheet](https://web.dev/constructable-stylesheets/):
74+
75+
```typescript
76+
import {controller, style} from '@github/catalyst'
77+
import {css} from '@acme/cool-css'
78+
79+
@controller
80+
class UserRow extends HTMLElement {
81+
@style componentCSS = css`
82+
:host {
83+
display: flex
84+
}
85+
`
86+
87+
connectedCallback() {
88+
this.attachShadow({ mode: 'open' })
89+
console.assert(this.componentCSS instanceof CSSStyleSheet)
90+
}
91+
}
92+
```

src/styleable.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type {CustomElementClass, CustomElement} from './custom-element.js'
2+
import {controllable, attachShadowCallback} from './controllable.js'
3+
import {createMark} from './mark.js'
4+
import {createAbility} from './ability.js'
5+
6+
const [style, getStyle, initStyle] = createMark<CustomElement>(
7+
({name, kind}) => {
8+
if (kind === 'setter') throw new Error(`@style cannot decorate setter ${String(name)}`)
9+
if (kind === 'method') throw new Error(`@style cannot decorate method ${String(name)}`)
10+
},
11+
(instance: CustomElement, {name, kind, access}) => {
12+
return {
13+
get: () => (kind === 'getter' ? access.get!.call(instance) : access.value),
14+
set: () => {
15+
throw new Error(`Cannot set @style ${String(name)}`)
16+
}
17+
}
18+
}
19+
)
20+
21+
export {style, getStyle}
22+
export const stylable = createAbility(
23+
<T extends CustomElementClass>(Class: T): T =>
24+
class extends controllable(Class) {
25+
[key: PropertyKey]: unknown
26+
27+
// TS mandates Constructors that get mixins have `...args: any[]`
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
constructor(...args: any[]) {
30+
super(...args)
31+
initStyle(this)
32+
}
33+
34+
[attachShadowCallback](root: ShadowRoot) {
35+
super[attachShadowCallback]?.(root)
36+
const styleProps = getStyle(this)
37+
if (!styleProps.size) return
38+
const styles = new Set([...root.adoptedStyleSheets])
39+
for (const name of styleProps) styles.add(this[name] as CSSStyleSheet)
40+
root.adoptedStyleSheets = [...styles]
41+
}
42+
}
43+
)

test/styleable.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {expect, fixture} from '@open-wc/testing'
2+
import {style, stylable} from '../src/styleable.js'
3+
const html = String.raw
4+
5+
type TemplateString = {raw: readonly string[] | ArrayLike<string>}
6+
const css = (strings: TemplateString, ...values: unknown[]): CSSStyleSheet => {
7+
const sheet = new CSSStyleSheet()
8+
sheet.replaceSync(String.raw(strings, ...values))
9+
return sheet
10+
}
11+
12+
describe('Styleable', () => {
13+
const globalCSS = ({color}: {color: string}) =>
14+
css`
15+
:host {
16+
color: ${color};
17+
}
18+
`
19+
20+
@stylable
21+
class StylableTest extends HTMLElement {
22+
@style foo = css`
23+
body {
24+
display: block;
25+
}
26+
`
27+
@style bar = globalCSS({color: 'rgb(255, 105, 180)'})
28+
29+
constructor() {
30+
super()
31+
this.attachShadow({mode: 'open'}).innerHTML = html`<p>Hello</p>`
32+
}
33+
}
34+
window.customElements.define('stylable-test', StylableTest)
35+
36+
it('adoptes styles into shadowRoot', async () => {
37+
const instance = await fixture<StylableTest>(html`<stylable-test></stylable-test>`)
38+
expect(instance.foo).to.be.instanceof(CSSStyleSheet)
39+
expect(instance.bar).to.be.instanceof(CSSStyleSheet)
40+
expect(instance.shadowRoot!.adoptedStyleSheets).to.eql([instance.foo, instance.bar])
41+
})
42+
43+
it('throws an error when trying to set stylesheet', async () => {
44+
const instance = await fixture<StylableTest>(html`<stylable-test></stylable-test>`)
45+
expect(() => (instance.foo = css``)).to.throw(/Cannot set @style/)
46+
})
47+
})

0 commit comments

Comments
 (0)