Apply constructed styles across shadow roots
This commit is contained in:
90
crates/obscura-js/js/bootstrap.js
vendored
90
crates/obscura-js/js/bootstrap.js
vendored
@@ -7654,6 +7654,7 @@ class CSSStyleSheet {
|
||||
this._sourceText = "";
|
||||
this._rules = [];
|
||||
this._cssRules = new CSSRuleList(this);
|
||||
this._adopters = new Set();
|
||||
}
|
||||
get type() { return "text/css"; }
|
||||
get ownerNode() { return this._ownerNode; }
|
||||
@@ -7815,28 +7816,49 @@ globalThis.CSSStyleSheet = CSSStyleSheet;
|
||||
globalThis.StyleSheetList = StyleSheetList;
|
||||
|
||||
function _syncAdoptedStyleSheet(sheet) {
|
||||
const doc = globalThis.document;
|
||||
if (doc && doc._adoptedStyleSheets?.includes(sheet)) {
|
||||
_syncDocumentAdoptedStyles(doc);
|
||||
for (const root of Array.from(sheet._adopters || [])) {
|
||||
_syncAdoptedStyles(root);
|
||||
}
|
||||
}
|
||||
|
||||
function _syncDocumentAdoptedStyles(doc) {
|
||||
const sheets = doc._adoptedStyleSheets || [];
|
||||
const nodes = doc._adoptedStyleNodes || (doc._adoptedStyleNodes = new Map());
|
||||
function _reconcileAdoptedStyleSheetAdopters(root, sheets) {
|
||||
const previous = root._registeredAdoptedStyleSheets
|
||||
|| (root._registeredAdoptedStyleSheets = new Set());
|
||||
const current = new Set(Array.from(sheets || []).filter(sheet => sheet instanceof CSSStyleSheet));
|
||||
for (const sheet of previous) {
|
||||
if (!current.has(sheet)) sheet._adopters?.delete(root);
|
||||
}
|
||||
for (const sheet of current) {
|
||||
if (!previous.has(sheet)) sheet._adopters.add(root);
|
||||
}
|
||||
root._registeredAdoptedStyleSheets = current;
|
||||
}
|
||||
|
||||
function _adoptedStyleTarget(root) {
|
||||
if (!root) return null;
|
||||
if (root.nodeType === 9) return root.head || root.documentElement;
|
||||
return root instanceof globalThis.ShadowRoot ? root : null;
|
||||
}
|
||||
|
||||
function _syncAdoptedStyles(root) {
|
||||
const sheets = root._adoptedStyleSheets || [];
|
||||
_reconcileAdoptedStyleSheetAdopters(root, sheets);
|
||||
const nodes = root._adoptedStyleNodes || (root._adoptedStyleNodes = new Map());
|
||||
for (const [sheet, node] of Array.from(nodes.entries())) {
|
||||
if (!sheets.includes(sheet)) {
|
||||
node.remove();
|
||||
nodes.delete(sheet);
|
||||
}
|
||||
}
|
||||
const target = _adoptedStyleTarget(root);
|
||||
if (!target) return;
|
||||
for (const sheet of sheets) {
|
||||
if (!(sheet instanceof CSSStyleSheet)) continue;
|
||||
let node = nodes.get(sheet);
|
||||
if (!node || !node.parentNode) {
|
||||
node = doc.createElement("style");
|
||||
if (!node || node.parentNode !== target) {
|
||||
node = (root.ownerDocument || globalThis.document).createElement("style");
|
||||
node.setAttribute("data-obscura-adopted", "");
|
||||
(doc.head || doc.documentElement).appendChild(node);
|
||||
target.appendChild(node);
|
||||
nodes.set(sheet, node);
|
||||
}
|
||||
const css = Array.from(sheet.cssRules || [], rule => rule.cssText || "").join("\n");
|
||||
@@ -7844,32 +7866,49 @@ function _syncDocumentAdoptedStyles(doc) {
|
||||
}
|
||||
}
|
||||
|
||||
function _makeAdoptedSheetList(doc, values) {
|
||||
// Keep the [SameObject] array identity stable even when the IDL setter replaces
|
||||
// its contents. Mutating the backing target directly avoids intermediate
|
||||
// materializations while assignment is in progress; ordinary array mutations
|
||||
// still pass through the proxy and synchronize immediately.
|
||||
const _adoptedSheetListTargets = new WeakMap();
|
||||
function _makeAdoptedSheetList(root, values) {
|
||||
const target = Array.from(values || []);
|
||||
return new Proxy(target, {
|
||||
const list = new Proxy(target, {
|
||||
set(array, property, value) {
|
||||
Reflect.set(array, property, value);
|
||||
_syncDocumentAdoptedStyles(doc);
|
||||
_syncAdoptedStyles(root);
|
||||
return true;
|
||||
},
|
||||
deleteProperty(array, property) {
|
||||
Reflect.deleteProperty(array, property);
|
||||
_syncDocumentAdoptedStyles(doc);
|
||||
_syncAdoptedStyles(root);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
_adoptedSheetListTargets.set(root, target);
|
||||
return list;
|
||||
}
|
||||
|
||||
function _adoptedStyleSheetsFor(root) {
|
||||
if (!root._adoptedStyleSheets) {
|
||||
root._adoptedStyleSheets = _makeAdoptedSheetList(root, []);
|
||||
}
|
||||
return root._adoptedStyleSheets;
|
||||
}
|
||||
|
||||
function _replaceAdoptedStyleSheets(root, sheets) {
|
||||
const list = _adoptedStyleSheetsFor(root);
|
||||
const values = Array.from(sheets || []);
|
||||
const target = _adoptedSheetListTargets.get(root);
|
||||
target.splice(0, target.length, ...values);
|
||||
_syncAdoptedStyles(root);
|
||||
return list;
|
||||
}
|
||||
|
||||
Object.defineProperty(Document.prototype, 'adoptedStyleSheets', {
|
||||
get() {
|
||||
if (!this._adoptedStyleSheets) {
|
||||
this._adoptedStyleSheets = _makeAdoptedSheetList(this, []);
|
||||
}
|
||||
return this._adoptedStyleSheets;
|
||||
},
|
||||
get() { return _adoptedStyleSheetsFor(this); },
|
||||
set(sheets) {
|
||||
this._adoptedStyleSheets = _makeAdoptedSheetList(this, sheets);
|
||||
_syncDocumentAdoptedStyles(this);
|
||||
_replaceAdoptedStyleSheets(this, sheets);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8026,7 +8065,10 @@ globalThis.ShadowRoot = class ShadowRoot extends DocumentFragment {
|
||||
return options?.composed ? this._host.getRootNode(options) : this;
|
||||
}
|
||||
get activeElement() { return null; }
|
||||
get styleSheets() { return []; }
|
||||
get styleSheets() {
|
||||
if (!this._styleSheetList) this._styleSheetList = new StyleSheetList(this);
|
||||
return this._styleSheetList;
|
||||
}
|
||||
cloneNode() {
|
||||
throw new DOMException(
|
||||
'Failed to execute cloneNode on Node: ShadowRoot nodes are not clonable.',
|
||||
@@ -8038,8 +8080,8 @@ globalThis.ShadowRoot = class ShadowRoot extends DocumentFragment {
|
||||
};
|
||||
// Constructible-stylesheet adoption, mirroring Document.adoptedStyleSheets.
|
||||
Object.defineProperty(globalThis.ShadowRoot.prototype, 'adoptedStyleSheets', {
|
||||
get() { return this._adoptedStyleSheets || []; },
|
||||
set(sheets) { this._adoptedStyleSheets = sheets; },
|
||||
get() { return _adoptedStyleSheetsFor(this); },
|
||||
set(sheets) { _replaceAdoptedStyleSheets(this, sheets); },
|
||||
configurable: true,
|
||||
});
|
||||
globalThis.__obscura_shadowHostNames = new Set(['article','aside','blockquote','body','div','footer','h1','h2','h3','h4','h5','h6','header','main','nav','p','section','span']);
|
||||
|
||||
@@ -10103,6 +10103,161 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadow_stylesheet_lists_and_adoption_are_live_across_roots() {
|
||||
let mut rt = setup_runtime(
|
||||
"<html><head></head><body><div id='one'></div><div id='two'></div></body></html>",
|
||||
);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
(() => {
|
||||
const first = document.getElementById('one').attachShadow({ mode: 'open' });
|
||||
const second = document.getElementById('two').attachShadow({ mode: 'open' });
|
||||
const inline = document.createElement('style');
|
||||
inline.textContent = '.local { width: 17px }';
|
||||
first.appendChild(inline);
|
||||
const inlineSheet = inline.sheet;
|
||||
const firstList = first.styleSheets;
|
||||
const firstAdopted = first.adoptedStyleSheets;
|
||||
const secondAdopted = second.adoptedStyleSheets;
|
||||
const documentAdopted = document.adoptedStyleSheets;
|
||||
|
||||
const shared = new CSSStyleSheet();
|
||||
first.adoptedStyleSheets = [shared];
|
||||
second.adoptedStyleSheets.push(shared);
|
||||
document.adoptedStyleSheets = [shared];
|
||||
|
||||
const firstNode = first.querySelector('style[data-obscura-adopted]');
|
||||
const secondNode = second.querySelector('style[data-obscura-adopted]');
|
||||
const documentNode = document.querySelector('style[data-obscura-adopted]');
|
||||
const initial = [
|
||||
first.styleSheets === firstList,
|
||||
firstList.length,
|
||||
firstList[0] === inlineSheet,
|
||||
firstList.item(0) === inlineSheet,
|
||||
second.styleSheets === second.styleSheets,
|
||||
second.styleSheets.length,
|
||||
first.adoptedStyleSheets === firstAdopted,
|
||||
second.adoptedStyleSheets === secondAdopted,
|
||||
document.adoptedStyleSheets === documentAdopted,
|
||||
firstAdopted.length,
|
||||
secondAdopted.length,
|
||||
documentAdopted.length,
|
||||
firstNode.parentNode === first,
|
||||
secondNode.parentNode === second,
|
||||
documentNode.parentNode === document.head,
|
||||
];
|
||||
|
||||
shared.insertRule('.shared { width: 31px }', 0);
|
||||
const synchronized = [firstNode, secondNode, documentNode]
|
||||
.map(node => node.textContent.includes('width: 31px'));
|
||||
|
||||
second.adoptedStyleSheets = [];
|
||||
shared.replaceSync('.shared { width: 47px }');
|
||||
const afterRemoval = [
|
||||
second.adoptedStyleSheets === secondAdopted,
|
||||
secondAdopted.length,
|
||||
secondNode.parentNode,
|
||||
second.querySelectorAll('style[data-obscura-adopted]').length,
|
||||
firstNode.textContent.includes('width: 47px'),
|
||||
documentNode.textContent.includes('width: 47px'),
|
||||
secondNode.textContent.includes('width: 31px'),
|
||||
];
|
||||
|
||||
inline.remove();
|
||||
const inlineRemoval = [
|
||||
first.styleSheets === firstList,
|
||||
firstList.length,
|
||||
inlineSheet.ownerNode,
|
||||
];
|
||||
return { initial, synchronized, afterRemoval, inlineRemoval };
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!({
|
||||
"initial": [
|
||||
true, 1, true, true, true, 0,
|
||||
true, true, true, 1, 1, 1,
|
||||
true, true, true
|
||||
],
|
||||
"synchronized": [true, true, true],
|
||||
"afterRemoval": [true, 0, null, 0, true, true, true],
|
||||
"inlineRemoval": [true, 0, null],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[test]
|
||||
fn shadow_adopted_stylesheets_apply_and_sync_the_live_cascade() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<html style="margin:0"><head>
|
||||
<style>.target { width:11px; height:10px }</style>
|
||||
</head><body style="margin:0">
|
||||
<div id="one"></div><div id="two"></div><div class="target" id="outside"></div>
|
||||
</body></html>"#,
|
||||
);
|
||||
rt.set_viewport(200.0, 100.0);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
(() => {
|
||||
const first = document.getElementById('one').attachShadow({ mode: 'open' });
|
||||
const second = document.getElementById('two').attachShadow({ mode: 'open' });
|
||||
first.innerHTML = '<style>.target { width:18px; height:10px }</style><div class="target"></div>';
|
||||
second.innerHTML = '<style>.target { width:23px; height:10px }</style><div class="target"></div>';
|
||||
const firstTarget = first.querySelector('.target');
|
||||
const secondTarget = second.querySelector('.target');
|
||||
const outside = document.getElementById('outside');
|
||||
const widths = () => [firstTarget, secondTarget, outside]
|
||||
.map(node => node.getBoundingClientRect().width);
|
||||
|
||||
const inline = widths();
|
||||
const shared = new CSSStyleSheet();
|
||||
shared.replaceSync('.target { width:42px; height:10px }');
|
||||
first.adoptedStyleSheets = [shared];
|
||||
second.adoptedStyleSheets = [shared];
|
||||
document.adoptedStyleSheets = [shared];
|
||||
const adopted = widths();
|
||||
|
||||
shared.cssRules[0].style.setProperty('width', '67px');
|
||||
const mutated = widths();
|
||||
|
||||
second.adoptedStyleSheets = [];
|
||||
document.adoptedStyleSheets = [];
|
||||
const selectivelyRemoved = widths();
|
||||
|
||||
shared.replaceSync('.target { width:81px; height:10px }');
|
||||
const remainingRootUpdated = widths();
|
||||
return [
|
||||
inline, adopted, mutated, selectivelyRemoved, remainingRootUpdated,
|
||||
first.styleSheets.length,
|
||||
second.styleSheets.length,
|
||||
];
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
[18, 23, 11],
|
||||
[42, 42, 42],
|
||||
[67, 67, 67],
|
||||
[67, 23, 11],
|
||||
[81, 23, 11],
|
||||
1,
|
||||
1,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_webgl_context_does_not_claim_success() {
|
||||
let mut rt = setup_runtime("<html><body><canvas></canvas></body></html>");
|
||||
|
||||
Reference in New Issue
Block a user