-
Notifications
You must be signed in to change notification settings - Fork 2
/
emotion.tsx
228 lines (208 loc) · 7.98 KB
/
emotion.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import React from 'react';
import createCache, { Options } from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { ComponentType, StylesInjector } from './types.js';
import { useConnectedPortalsEffect, useShadowDom } from './hooks.js';
// Copied from emotion-cache with minor adjustments to work with multiple containers
// https://github.com/emotion-js/emotion/blob/main/packages/sheet/src/index.js
function sheetForTag(tag: HTMLStyleElement): CSSStyleSheet {
if (tag.sheet) {
return tag.sheet
}
// this weirdness brought to you by firefox
for (let i = 0; i < document.styleSheets.length; i++) {
if (document.styleSheets[i]!.ownerNode === tag) {
return document.styleSheets[i]!
}
}
return new CSSStyleSheet();
}
type SheetOptions = {
nonce?: string,
key: string,
containers?: Node[],
container?: Node,
speedy?: boolean,
prepend?: boolean,
insertionPoint?: HTMLElement
}
function createStyleElement(options: {
key: string,
nonce: string | void
}): HTMLStyleElement {
let tag = document.createElement('style')
tag.setAttribute('data-emotion', options.key)
if (options.nonce !== undefined) {
tag.setAttribute('nonce', options.nonce)
}
tag.appendChild(document.createTextNode(''))
tag.setAttribute('data-s', '')
return tag
}
export class StyleSheet {
isSpeedy: boolean
ctr: number
tags: Array<HTMLStyleElement[]>
// Using Node instead of HTMLElement since container may be a ShadowRoot
containers: Node[]
// HACK: emotion sometimes uses this prop to pass it to newly created StyleSheet (when inserting global styles),
// so we keep it around
container: Node[]
key: string
nonce: string | void
prepend: boolean | void
before: Element | null
insertionPoint: HTMLElement | void
constructor(options: SheetOptions) {
this.isSpeedy =
options.speedy === undefined
? process.env['NODE_ENV'] === 'production'
: options.speedy
if (!options.containers) {
options.containers = []
}
if (options.container) {
options.containers.push(...(Array.isArray(options.container) ? options.container : [options.container]))
}
if (!options.containers) options.containers = []
this.tags = options.containers.map(c => []);
this.ctr = 0
this.nonce = options.nonce
// key is the value of the data-emotion attribute, it's used to identify different sheets
this.key = options.key
this.containers = options.containers
this.prepend = options.prepend
this.insertionPoint = options.insertionPoint
this.before = null
}
_insertTags = (tag: HTMLStyleElement) => {
this.containers.forEach((container, i) => {
const tagCopy = tag.cloneNode(true);
let before
if (this.tags[i]!.length === 0) {
if (this.insertionPoint) {
before = this.insertionPoint.nextSibling
} else if (this.prepend) {
before = container.firstChild
} else {
before = this.before
}
} else {
before = this.tags[i]![this.tags[i]!.length - 1]!.nextSibling
}
container.insertBefore(tagCopy, before)
// @ts-ignore
this.tags[i].push(tagCopy)
});
}
hydrate(nodes: HTMLStyleElement[]) {
nodes.forEach(this._insertTags)
}
insert(rule: string) {
// the max length is how many rules we have per style tag, it's 65000 in speedy mode
// it's 1 in dev because we insert source maps that map a single rule to a location
// and you can only have one source map per style tag
if (this.ctr % (this.isSpeedy ? 65000 : 1) === 0) {
this._insertTags(createStyleElement(this))
}
this.containers.forEach((cont, i) => {
const tag = this.tags[i]![this.tags[i]!.length - 1]!
if (process.env['NODE_ENV'] !== 'production') {
const isImportRule =
rule.charCodeAt(0) === 64 && rule.charCodeAt(1) === 105
if (isImportRule && (this as any)._alreadyInsertedOrderInsensitiveRule) {
// this would only cause problem in speedy mode
// but we don't want enabling speedy to affect the observable behavior
// so we report this error at all times
console.error(
`You're attempting to insert the following rule:\n` +
rule +
'\n\n`@import` rules must be before all other types of rules in a stylesheet but other rules have already been inserted. Please ensure that `@import` rules are before all other rules.'
)
}
; (this as any)._alreadyInsertedOrderInsensitiveRule =
(this as any)._alreadyInsertedOrderInsensitiveRule || !isImportRule
}
if (this.isSpeedy) {
const sheet = sheetForTag(tag)
try {
// this is the ultrafast version, works across browsers
// the big drawback is that the css won't be editable in devtools
sheet.insertRule(rule, sheet.cssRules.length)
} catch (e) {
if (
process.env['NODE_ENV'] !== 'production' &&
!/:(-moz-placeholder|-moz-focus-inner|-moz-focusring|-ms-input-placeholder|-moz-read-write|-moz-read-only|-ms-clear|-ms-expand|-ms-reveal){/.test(
rule
)
) {
console.error(
`There was a problem inserting the following rule: "${rule}"`,
e
)
}
}
} else {
tag.appendChild(document.createTextNode(rule))
}
});
this.ctr++
}
flush() {
this.tags.forEach(tags => tags.forEach(tag => tag.parentNode && tag.parentNode.removeChild(tag)));
this.tags = this.containers.map(c => []);
this.ctr = 0
if (process.env['NODE_ENV'] !== 'production') {
; (this as any)._alreadyInsertedOrderInsensitiveRule = false
}
}
addContainer(tag: Node) {
this.containers.push(tag);
this.tags.push([]);
}
}
interface EmotionInjectorOptions {
stylisPlugins?: Options['stylisPlugins'];
}
export default (options: EmotionInjectorOptions = {}): StylesInjector => {
return <P extends JSX.IntrinsicAttributes>(
Component: ComponentType<P>,
shadowHost: HTMLElement,
shadowRoot: ShadowRoot,
mountingInto: HTMLDivElement,
stylesWrapper: HTMLDivElement
) => {
const { stylisPlugins = [] } = options;
const key = Math.random()
.toString(36)
.replace(/[^a-z]+/g, '')
.slice(0, 5);
// Some fluck with types. TS thinks createCache isn't callable
// @ts-ignore
const cache = createCache({
key,
stylisPlugins: stylisPlugins,
container: stylesWrapper,
});
const sheet = new StyleSheet({
key,
containers: [stylesWrapper],
});
// @ts-ignore
cache.sheet = sheet;
return (props: P) => {
useConnectedPortalsEffect((portals) => {
portals.forEach(p => {
if (!sheet.containers.includes(p.stylesWrapper)) {
sheet.addContainer(p.stylesWrapper);
}
});
});
return (
<CacheProvider value={cache}>
<Component {...props} />
</CacheProvider>
);
};
};
};