Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions e2e/testcafe-devextreme/tests/common/shadowDOM.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ClientFunction } from 'testcafe';
import url from '../../helpers/getPageUrl';

fixture.disablePageReloads`Shadow DOM - Adopted DX css styles`
.page(url(__dirname, '../container.html'));

const dxWidgetHostStyles = '.dx-widget-shadow { font-size: 20px; }';
const dxWidgetShadowStyles = '.dx-widget-shadow { font-size: 40px; }';

const setupShadowDomTest = (copyStylesToShadowDom) => ClientFunction((copyStyles) => {
if (!copyStyles) {
(window as any).DevExpress.config({ copyStylesToShadowDom: copyStyles });
}

const container = document.createElement('div');
container.id = 'shadow-host';
document.body.appendChild(container);

const hostStyleElement = document.createElement('style');
hostStyleElement.innerHTML = dxWidgetHostStyles;
document.head.appendChild(hostStyleElement);

const shadowRoot = container.attachShadow({ mode: 'open' });

const shadowStyleElement = shadowRoot.ownerDocument.createElement('style');
shadowStyleElement.innerHTML = dxWidgetShadowStyles;
shadowRoot.appendChild(shadowStyleElement);

const shadowContainerElement = document.createElement('div');
shadowContainerElement.id = 'shadow-container';
shadowRoot.appendChild(shadowContainerElement);

(window as any).testShadowRoot = shadowRoot;

// eslint-disable-next-line new-cap, no-new
new (window as any).DevExpress.ui.dxButton(shadowContainerElement, {
text: 'Test button',
});
}, {
dependencies: {
dxWidgetHostStyles,
dxWidgetShadowStyles,
},
})(copyStylesToShadowDom);

const getAdoptedStyleSheets = ClientFunction(() => {
const shadowRoot = (window as any).testShadowRoot;
const { adoptedStyleSheets } = shadowRoot;

const results: { [key: string]: string[] | null } = {
firstSheetRules: null,
secondSheetRules: null,
};

if (adoptedStyleSheets.length > 1) {
results.firstSheetRules = Array
.from(adoptedStyleSheets[0].cssRules).map((rule) => (rule as CSSRule).cssText);

results.secondSheetRules = Array
.from(adoptedStyleSheets[1].cssRules).map((rule) => (rule as CSSRule).cssText);
}

return results;
});

test('Copies DX css styles from the host to the shadow root when rendering a DX widget', async (t) => {
await setupShadowDomTest(true);

const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets();

const hasHostStyle = firstSheetRules?.some((rule) => rule === dxWidgetHostStyles);
await t.expect(hasHostStyle).ok('First adopted stylesheet contains host styles');

const hasShadowStyle = secondSheetRules?.some((rule) => rule === dxWidgetShadowStyles);
await t.expect(hasShadowStyle).ok('Second adopted stylesheet contains shadow styles');
});

test('Does not copy DX css styles when copyStylesToShadowDom is disabled', async (t) => {
await setupShadowDomTest(false);

const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets();
await t.expect(firstSheetRules === null && secondSheetRules === null)
.ok('No adopted stylesheets should be created when copyStylesToShadowDom is disabled');
});
1 change: 1 addition & 0 deletions packages/devextreme/js/__internal/core/m_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const config = {
editorStylingMode: undefined,
useLegacyVisibleIndex: false,
versionAssertions: [],
copyStylesToShadowDom: true,

floatingActionButtonConfig: {
icon: 'add',
Expand Down
57 changes: 47 additions & 10 deletions packages/devextreme/js/__internal/core/utils/m_shadow_dom.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import config from '@js/core/config';

const DX_RULE_PREFIX = 'dx-';

let ownerDocumentStyleSheet = null;

function createConstructedStyleSheet(rootNode) {
try {
// eslint-disable-next-line no-undef
return new CSSStyleSheet();
} catch (err) {
const styleElement = rootNode.ownerDocument.createElement('style');
Expand Down Expand Up @@ -43,22 +44,60 @@ function insertRule(targetStyleSheet, rule, needApplyAllStyles) {
}
}

export function addShadowDomStyles($element) {
const el = $element.get(0);
const root = el.getRootNode?.();
const FNV_OFFSET_BASIS = 2166136261;
const sheetHashes = new WeakMap();
export function computeStyleSheetsHash(styleSheets) {
let hash = FNV_OFFSET_BASIS;

if (!root?.host) {
for (const sheet of styleSheets) {
if (sheetHashes.has(sheet)) {
hash ^= sheetHashes.get(sheet);
continue;
}

let localHash = FNV_OFFSET_BASIS;
try {
for (const rule of sheet.cssRules) {
const text = rule.cssText;
for (let i = 0; i < text.length; i++) {
localHash ^= text.charCodeAt(i);
localHash += (localHash << 1) + (localHash << 4) + (localHash << 7) + (localHash << 8) + (localHash << 24);
}
}
} catch (_) {
// ignore
}

localHash >>>= 0;
sheetHashes.set(sheet, localHash);
hash ^= localHash;
}

return hash >>> 0;
}

const styleSheetHashes = new WeakMap();

export function addShadowDomStyles($element) {
if (!config().copyStylesToShadowDom) {
return;
}

const el = $element.get(0);
const root = el.getRootNode?.();
if (!root?.host) return;

if (!ownerDocumentStyleSheet) {
ownerDocumentStyleSheet = createConstructedStyleSheet(root);

processRules(ownerDocumentStyleSheet, el.ownerDocument.styleSheets, false);
}

const currentShadowDomStyleSheet = createConstructedStyleSheet(root);
const localHash = computeStyleSheetsHash(root.styleSheets);
if (styleSheetHashes.get(root) === localHash) return;

styleSheetHashes.set(root, localHash);

const currentShadowDomStyleSheet = createConstructedStyleSheet(root);
processRules(currentShadowDomStyleSheet, root.styleSheets, true);

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

for (let i = 0; i < el.childNodes.length; i++) {
const childNode = el.childNodes[i];

// eslint-disable-next-line no-undef
if (childNode.nodeType === Node.ELEMENT_NODE
&& isPositionInElementRectangle(childNode, x, y)
// eslint-disable-next-line no-undef

&& getComputedStyle(childNode).pointerEvents !== 'none'
) {
elementQueue.push(childNode);
Expand Down
6 changes: 6 additions & 0 deletions packages/devextreme/js/common.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ export type GlobalConfig = {
* @public
*/
defaultUseCurrencyAccountingStyle?: boolean;
/**
* @docid
* @default true
* @public
*/
copyStylesToShadowDom?: boolean;
/**
* @docid
* @default undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import $ from 'jquery';
import { computeStyleSheetsHash, addShadowDomStyles } from '__internal/core/utils/m_shadow_dom';

QUnit.module('computeStyleSheetsHash', () => {
QUnit.test('Returns consistent hash for same content', function(assert) {
const mockStyles = '.a { color: red; }';
const styleSheet1 = new CSSStyleSheet();
const styleSheet2 = new CSSStyleSheet();
styleSheet1.replaceSync(mockStyles);
styleSheet2.replaceSync(mockStyles);

const hash1 = computeStyleSheetsHash([styleSheet1]);
const hash2 = computeStyleSheetsHash([styleSheet2]);

assert.equal(hash1, hash2, 'Hashes are equal for identical stylesheets');
});

QUnit.test('Returns different hash for different content', function(assert) {
const styleSheet1 = new CSSStyleSheet();
const styleSheet2 = new CSSStyleSheet();
styleSheet1.replaceSync('.a { color: red; }');
styleSheet2.replaceSync('.a { color: blue; }');

const hash1 = computeStyleSheetsHash([styleSheet1]);
const hash2 = computeStyleSheetsHash([styleSheet2]);

assert.notEqual(hash1, hash2, 'Hashes differ for different stylesheets');
});
});

QUnit.module('addShadowDomStyles', () => {
QUnit.test('Does not duplicate stylesheets on repeated calls', async function(assert) {
const done = assert.async();
const container = document.createElement('div');
document.body.appendChild(container);
const shadow = container.attachShadow({ mode: 'open' });

const div = document.createElement('div');
shadow.appendChild(div);

const $div = $(div);

addShadowDomStyles($div);
const firstSheets = [...shadow.adoptedStyleSheets];
const firstRules = firstSheets.map(sheet => sheet.cssRules.length);

addShadowDomStyles($div);
const secondSheets = [...shadow.adoptedStyleSheets];
const secondRules = secondSheets.map(sheet => sheet.cssRules.length);

assert.equal(firstSheets.length, secondSheets.length, 'Stylesheets count unchanged after repeated call');

for(let i = 0; i < firstRules.length; i++) {
assert.equal(firstRules[i], secondRules[i], `Sheet[${i}] cssRules count unchanged`);
}
done();
});
});
4 changes: 4 additions & 0 deletions packages/devextreme/ts/dx.all.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,10 @@ declare module DevExpress.common {
* [descr:GlobalConfig.defaultUseCurrencyAccountingStyle]
*/
defaultUseCurrencyAccountingStyle?: boolean;
/**
* [descr:GlobalConfig.copyStylesToShadowDom]
*/
copyStylesToShadowDom?: boolean;
/**
* [descr:GlobalConfig.editorStylingMode]
*/
Expand Down
Loading