Skip to content

Commit 4d8cc8b

Browse files
authored
feat(core): add copyStylesToShadowDom global configuration option (T1276942) (#31188)
1 parent ce031e8 commit 4d8cc8b

File tree

6 files changed

+199
-10
lines changed

6 files changed

+199
-10
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { ClientFunction } from 'testcafe';
2+
import url from '../../helpers/getPageUrl';
3+
4+
fixture.disablePageReloads`Shadow DOM - Adopted DX css styles`
5+
.page(url(__dirname, '../container.html'));
6+
7+
const dxWidgetHostStyles = '.dx-widget-shadow { font-size: 20px; }';
8+
const dxWidgetShadowStyles = '.dx-widget-shadow { font-size: 40px; }';
9+
10+
const setupShadowDomTest = (copyStylesToShadowDom) => ClientFunction((copyStyles) => {
11+
if (!copyStyles) {
12+
(window as any).DevExpress.config({ copyStylesToShadowDom: copyStyles });
13+
}
14+
15+
const container = document.createElement('div');
16+
container.id = 'shadow-host';
17+
document.body.appendChild(container);
18+
19+
const hostStyleElement = document.createElement('style');
20+
hostStyleElement.innerHTML = dxWidgetHostStyles;
21+
document.head.appendChild(hostStyleElement);
22+
23+
const shadowRoot = container.attachShadow({ mode: 'open' });
24+
25+
const shadowStyleElement = shadowRoot.ownerDocument.createElement('style');
26+
shadowStyleElement.innerHTML = dxWidgetShadowStyles;
27+
shadowRoot.appendChild(shadowStyleElement);
28+
29+
const shadowContainerElement = document.createElement('div');
30+
shadowContainerElement.id = 'shadow-container';
31+
shadowRoot.appendChild(shadowContainerElement);
32+
33+
(window as any).testShadowRoot = shadowRoot;
34+
35+
// eslint-disable-next-line new-cap, no-new
36+
new (window as any).DevExpress.ui.dxButton(shadowContainerElement, {
37+
text: 'Test button',
38+
});
39+
}, {
40+
dependencies: {
41+
dxWidgetHostStyles,
42+
dxWidgetShadowStyles,
43+
},
44+
})(copyStylesToShadowDom);
45+
46+
const getAdoptedStyleSheets = ClientFunction(() => {
47+
const shadowRoot = (window as any).testShadowRoot;
48+
const { adoptedStyleSheets } = shadowRoot;
49+
50+
const results: { [key: string]: string[] | null } = {
51+
firstSheetRules: null,
52+
secondSheetRules: null,
53+
};
54+
55+
if (adoptedStyleSheets.length > 1) {
56+
results.firstSheetRules = Array
57+
.from(adoptedStyleSheets[0].cssRules).map((rule) => (rule as CSSRule).cssText);
58+
59+
results.secondSheetRules = Array
60+
.from(adoptedStyleSheets[1].cssRules).map((rule) => (rule as CSSRule).cssText);
61+
}
62+
63+
return results;
64+
});
65+
66+
test('Copies DX css styles from the host to the shadow root when rendering a DX widget', async (t) => {
67+
await setupShadowDomTest(true);
68+
69+
const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets();
70+
71+
const hasHostStyle = firstSheetRules?.some((rule) => rule === dxWidgetHostStyles);
72+
await t.expect(hasHostStyle).ok('First adopted stylesheet contains host styles');
73+
74+
const hasShadowStyle = secondSheetRules?.some((rule) => rule === dxWidgetShadowStyles);
75+
await t.expect(hasShadowStyle).ok('Second adopted stylesheet contains shadow styles');
76+
});
77+
78+
test('Does not copy DX css styles when copyStylesToShadowDom is disabled', async (t) => {
79+
await setupShadowDomTest(false);
80+
81+
const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets();
82+
await t.expect(firstSheetRules === null && secondSheetRules === null)
83+
.ok('No adopted stylesheets should be created when copyStylesToShadowDom is disabled');
84+
});

packages/devextreme/js/__internal/core/m_config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const config = {
1717
useJQuery: undefined,
1818
editorStylingMode: undefined,
1919
useLegacyVisibleIndex: false,
20+
copyStylesToShadowDom: true,
2021

2122
floatingActionButtonConfig: {
2223
icon: 'add',

packages/devextreme/js/__internal/core/utils/m_shadow_dom.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import config from '@js/core/config';
2+
13
const DX_RULE_PREFIX = 'dx-';
24

35
let ownerDocumentStyleSheet = null;
46

57
function createConstructedStyleSheet(rootNode) {
68
try {
7-
// eslint-disable-next-line no-undef
89
return new CSSStyleSheet();
910
} catch (err) {
1011
const styleElement = rootNode.ownerDocument.createElement('style');
@@ -43,22 +44,60 @@ function insertRule(targetStyleSheet, rule, needApplyAllStyles) {
4344
}
4445
}
4546

46-
export function addShadowDomStyles($element) {
47-
const el = $element.get(0);
48-
const root = el.getRootNode?.();
47+
const FNV_OFFSET_BASIS = 2166136261;
48+
const sheetHashes = new WeakMap();
49+
export function computeStyleSheetsHash(styleSheets) {
50+
let hash = FNV_OFFSET_BASIS;
4951

50-
if (!root?.host) {
52+
for (const sheet of styleSheets) {
53+
if (sheetHashes.has(sheet)) {
54+
hash ^= sheetHashes.get(sheet);
55+
continue;
56+
}
57+
58+
let localHash = FNV_OFFSET_BASIS;
59+
try {
60+
for (const rule of sheet.cssRules) {
61+
const text = rule.cssText;
62+
for (let i = 0; i < text.length; i++) {
63+
localHash ^= text.charCodeAt(i);
64+
localHash += (localHash << 1) + (localHash << 4) + (localHash << 7) + (localHash << 8) + (localHash << 24);
65+
}
66+
}
67+
} catch (_) {
68+
// ignore
69+
}
70+
71+
localHash >>>= 0;
72+
sheetHashes.set(sheet, localHash);
73+
hash ^= localHash;
74+
}
75+
76+
return hash >>> 0;
77+
}
78+
79+
const styleSheetHashes = new WeakMap();
80+
81+
export function addShadowDomStyles($element) {
82+
if (!config().copyStylesToShadowDom) {
5183
return;
5284
}
5385

86+
const el = $element.get(0);
87+
const root = el.getRootNode?.();
88+
if (!root?.host) return;
89+
5490
if (!ownerDocumentStyleSheet) {
5591
ownerDocumentStyleSheet = createConstructedStyleSheet(root);
56-
5792
processRules(ownerDocumentStyleSheet, el.ownerDocument.styleSheets, false);
5893
}
5994

60-
const currentShadowDomStyleSheet = createConstructedStyleSheet(root);
95+
const localHash = computeStyleSheetsHash(root.styleSheets);
96+
if (styleSheetHashes.get(root) === localHash) return;
97+
98+
styleSheetHashes.set(root, localHash);
6199

100+
const currentShadowDomStyleSheet = createConstructedStyleSheet(root);
62101
processRules(currentShadowDomStyleSheet, root.styleSheets, true);
63102

64103
root.adoptedStyleSheets = [ownerDocumentStyleSheet, currentShadowDomStyleSheet];
@@ -103,11 +142,9 @@ export function getShadowElementsFromPoint(x, y, root) {
103142

104143
for (let i = 0; i < el.childNodes.length; i++) {
105144
const childNode = el.childNodes[i];
106-
107-
// eslint-disable-next-line no-undef
108145
if (childNode.nodeType === Node.ELEMENT_NODE
109146
&& isPositionInElementRectangle(childNode, x, y)
110-
// eslint-disable-next-line no-undef
147+
111148
&& getComputedStyle(childNode).pointerEvents !== 'none'
112149
) {
113150
elementQueue.push(childNode);

packages/devextreme/js/common.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,12 @@ export type GlobalConfig = {
298298
* @public
299299
*/
300300
defaultUseCurrencyAccountingStyle?: boolean;
301+
/**
302+
* @docid
303+
* @default true
304+
* @public
305+
*/
306+
copyStylesToShadowDom?: boolean;
301307
/**
302308
* @docid
303309
* @default undefined
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import $ from 'jquery';
2+
import { computeStyleSheetsHash, addShadowDomStyles } from '__internal/core/utils/m_shadow_dom';
3+
4+
QUnit.module('computeStyleSheetsHash', () => {
5+
QUnit.test('Returns consistent hash for same content', function(assert) {
6+
const mockStyles = '.a { color: red; }';
7+
const styleSheet1 = new CSSStyleSheet();
8+
const styleSheet2 = new CSSStyleSheet();
9+
styleSheet1.replaceSync(mockStyles);
10+
styleSheet2.replaceSync(mockStyles);
11+
12+
const hash1 = computeStyleSheetsHash([styleSheet1]);
13+
const hash2 = computeStyleSheetsHash([styleSheet2]);
14+
15+
assert.equal(hash1, hash2, 'Hashes are equal for identical stylesheets');
16+
});
17+
18+
QUnit.test('Returns different hash for different content', function(assert) {
19+
const styleSheet1 = new CSSStyleSheet();
20+
const styleSheet2 = new CSSStyleSheet();
21+
styleSheet1.replaceSync('.a { color: red; }');
22+
styleSheet2.replaceSync('.a { color: blue; }');
23+
24+
const hash1 = computeStyleSheetsHash([styleSheet1]);
25+
const hash2 = computeStyleSheetsHash([styleSheet2]);
26+
27+
assert.notEqual(hash1, hash2, 'Hashes differ for different stylesheets');
28+
});
29+
});
30+
31+
QUnit.module('addShadowDomStyles', () => {
32+
QUnit.test('Does not duplicate stylesheets on repeated calls', async function(assert) {
33+
const done = assert.async();
34+
const container = document.createElement('div');
35+
document.body.appendChild(container);
36+
const shadow = container.attachShadow({ mode: 'open' });
37+
38+
const div = document.createElement('div');
39+
shadow.appendChild(div);
40+
const $div = $(div);
41+
42+
addShadowDomStyles($div);
43+
const firstSheets = [...shadow.adoptedStyleSheets];
44+
const firstRules = firstSheets.map(sheet => sheet.cssRules.length);
45+
46+
addShadowDomStyles($div);
47+
const secondSheets = [...shadow.adoptedStyleSheets];
48+
const secondRules = secondSheets.map(sheet => sheet.cssRules.length);
49+
50+
assert.equal(firstSheets.length, secondSheets.length, 'Stylesheets count unchanged after repeated call');
51+
52+
for(let i = 0; i < firstRules.length; i++) {
53+
assert.equal(firstRules[i], secondRules[i], `Sheet[${i}] cssRules count unchanged`);
54+
}
55+
done();
56+
});
57+
});

packages/devextreme/ts/dx.all.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,10 @@ declare module DevExpress.common {
10921092
* [descr:GlobalConfig.defaultUseCurrencyAccountingStyle]
10931093
*/
10941094
defaultUseCurrencyAccountingStyle?: boolean;
1095+
/**
1096+
* [descr:GlobalConfig.copyStylesToShadowDom]
1097+
*/
1098+
copyStylesToShadowDom?: boolean;
10951099
/**
10961100
* [descr:GlobalConfig.editorStylingMode]
10971101
*/

0 commit comments

Comments
 (0)