Repository: mui/mui-x
Pull request #17548: [internal] Move loadStyleSheets to internals and use it in data grid and charts
Author: login: bernardobelchior
Labels: name: internal
description: Behind-the-scenes enhancement. Formerly called “core”.
name: scope: data grid
description: Changes related to the data grid.
name: type: enhancement
description: It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.
name: scope: charts
description: Changes related to the charts.
Created: 2025-04-25T09:58:53Z | Merged: 2025-04-28T08:16:11Z

Move `loadStyleSheets` to `@mui/x-internals` and use it in data grid and charts.


Diff:
diff --git a/packages/x-charts-pro/src/internals/plugins/useChartProExport/common.ts b/packages/x-charts-pro/src/internals/plugins/useChartProExport/common.ts
index f38c40f259387..ade6dffe04e16 100644
--- a/packages/x-charts-pro/src/internals/plugins/useChartProExport/common.ts
+++ b/packages/x-charts-pro/src/internals/plugins/useChartProExport/common.ts
@@ -1,5 +1,3 @@
-import ownerDocument from '@mui/utils/ownerDocument';
-
 export function createExportIframe(title?: string): HTMLIFrameElement {
   const iframeEl = document.createElement('iframe');
   iframeEl.style.position = 'absolute';
@@ -8,54 +6,3 @@ export function createExportIframe(title?: string): HTMLIFrameElement {
   iframeEl.title = title || document.title;
   return iframeEl;
 }
-
-export function loadStyleSheets(document: Document, element: HTMLElement | SVGElement) {
-  const stylesheetLoadPromises: Promise<void>[] = [];
-  const doc = ownerDocument(element);
-
-  const rootCandidate = element.getRootNode();
-  const root =
-    rootCandidate.constructor.name === 'ShadowRoot' ? (rootCandidate as ShadowRoot) : doc;
-  const headStyleElements = root!.querySelectorAll("style, link[rel='stylesheet']");
-
-  for (let i = 0; i < headStyleElements.length; i += 1) {
-    const node = headStyleElements[i];
-    if (node.tagName === 'STYLE') {
-      const newHeadStyleElements = document.createElement(node.tagName);
-      const sheet = (node as HTMLStyleElement).sheet;
-
-      if (sheet) {
-        let styleCSS = '';
-        // NOTE: for-of is not supported by IE
-        for (let j = 0; j < sheet.cssRules.length; j += 1) {
-          if (typeof sheet.cssRules[j].cssText === 'string') {
-            styleCSS += `${sheet.cssRules[j].cssText}\r\n`;
-          }
-        }
-        newHeadStyleElements.appendChild(document.createTextNode(styleCSS));
-        document.head.appendChild(newHeadStyleElements);
-      }
-    } else if (node.getAttribute('href')) {
-      // If `href` tag is empty, avoid loading these links
-
-      const newHeadStyleElements = document.createElement(node.tagName);
-
-      for (let j = 0; j < node.attributes.length; j += 1) {
-        const attr = node.attributes[j];
-        if (attr) {
-          newHeadStyleElements.setAttribute(attr.nodeName, attr.nodeValue || '');
-        }
-      }
-
-      stylesheetLoadPromises.push(
-        new Promise((resolve) => {
-          newHeadStyleElements.addEventListener('load', () => resolve());
-        }),
-      );
-
-      document.head.appendChild(newHeadStyleElements);
-    }
-  }
-
-  return Promise.all(stylesheetLoadPromises);
-}
diff --git a/packages/x-charts-pro/src/internals/plugins/useChartProExport/exportImage.ts b/packages/x-charts-pro/src/internals/plugins/useChartProExport/exportImage.ts
index d8c92a4a6f4b7..fa62366432f64 100644
--- a/packages/x-charts-pro/src/internals/plugins/useChartProExport/exportImage.ts
+++ b/packages/x-charts-pro/src/internals/plugins/useChartProExport/exportImage.ts
@@ -1,5 +1,6 @@
 import ownerDocument from '@mui/utils/ownerDocument';
-import { createExportIframe, loadStyleSheets } from './common';
+import { loadStyleSheets } from '@mui/x-internals/export';
+import { createExportIframe } from './common';
 import { ChartImageExportOptions } from './useChartProExport.types';
 
 export const getDrawDocument = async () => {
@@ -45,7 +46,11 @@ export async function exportImage(
     exportDoc.body.innerHTML = container.innerHTML;
     exportDoc.body.style.margin = '0px';
 
-    await loadStyleSheets(exportDoc, element);
+    const rootCandidate = element.getRootNode();
+    const root =
+      rootCandidate.constructor.name === 'ShadowRoot' ? (rootCandidate as ShadowRoot) : doc;
+
+    await Promise.all(loadStyleSheets(exportDoc, root));
 
     resolve();
   };
diff --git a/packages/x-charts-pro/src/internals/plugins/useChartProExport/print.ts b/packages/x-charts-pro/src/internals/plugins/useChartProExport/print.ts
index 7071364f6dab0..206eb0a183e02 100644
--- a/packages/x-charts-pro/src/internals/plugins/useChartProExport/print.ts
+++ b/packages/x-charts-pro/src/internals/plugins/useChartProExport/print.ts
@@ -1,5 +1,6 @@
 import ownerDocument from '@mui/utils/ownerDocument';
-import { createExportIframe, loadStyleSheets } from './common';
+import { loadStyleSheets } from '@mui/x-internals/export';
+import { createExportIframe } from './common';
 import { ChartPrintExportOptions } from './useChartProExport.types';
 
 export function printChart(
@@ -16,7 +17,11 @@ export function printChart(
     container.appendChild(elementClone);
     printDoc.body.innerHTML = container.innerHTML;
 
-    await loadStyleSheets(printDoc, element);
+    const rootCandidate = element.getRootNode();
+    const root =
+      rootCandidate.constructor.name === 'ShadowRoot' ? (rootCandidate as ShadowRoot) : doc;
+
+    await Promise.all(loadStyleSheets(printDoc, root));
 
     printWindow.contentWindow!.print();
 
diff --git a/packages/x-data-grid/src/hooks/features/export/useGridPrintExport.tsx b/packages/x-data-grid/src/hooks/features/export/useGridPrintExport.tsx
index e38fd1076456e..b984a9689089b 100644
--- a/packages/x-data-grid/src/hooks/features/export/useGridPrintExport.tsx
+++ b/packages/x-data-grid/src/hooks/features/export/useGridPrintExport.tsx
@@ -1,6 +1,7 @@
 import * as React from 'react';
 import { RefObject } from '@mui/x-internals/types';
 import { unstable_ownerDocument as ownerDocument } from '@mui/utils';
+import { loadStyleSheets } from '@mui/x-internals/export';
 import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity';
 import { GridPrintExportApi } from '../../../models/api/gridPrintExportApi';
 import { useGridLogger } from '../../utils/useGridLogger';
@@ -211,7 +212,7 @@ export const useGridPrintExport = (
         printDoc.body.classList.add(...normalizeOptions.bodyClassName.split(' '));
       }
 
-      const stylesheetLoadPromises: Promise<void>[] = [];
+      let stylesheetLoadPromises: Promise<void>[] = [];
 
       if (normalizeOptions.copyStyles) {
         const rootCandidate = gridRootElement!.getRootNode();
@@ -219,46 +220,8 @@ export const useGridPrintExport = (
           rootCandidate.constructor.name === 'ShadowRoot'
             ? (rootCandidate as ShadowRoot)
             : doc.current;
-        const headStyleElements = root!.querySelectorAll("style, link[rel='stylesheet']");
-
-        for (let i = 0; i < headStyleElements.length; i += 1) {
-          const node = headStyleElements[i];
-          if (node.tagName === 'STYLE') {
-            const newHeadStyleElements = printDoc.createElement(node.tagName);
-            const sheet = (node as HTMLStyleElement).sheet;
-
-            if (sheet) {
-              let styleCSS = '';
-              // NOTE: for-of is not supported by IE
-              for (let j = 0; j < sheet.cssRules.length; j += 1) {
-                if (typeof sheet.cssRules[j].cssText === 'string') {
-                  styleCSS += `${sheet.cssRules[j].cssText}\r\n`;
-                }
-              }
-              newHeadStyleElements.appendChild(printDoc.createTextNode(styleCSS));
-              printDoc.head.appendChild(newHeadStyleElements);
-            }
-          } else if (node.getAttribute('href')) {
-            // If `href` tag is empty, avoid loading these links
-
-            const newHeadStyleElements = printDoc.createElement(node.tagName);
-
-            for (let j = 0; j < node.attributes.length; j += 1) {
-              const attr = node.attributes[j];
-              if (attr) {
-                newHeadStyleElements.setAttribute(attr.nodeName, attr.nodeValue || '');
-              }
-            }
-
-            stylesheetLoadPromises.push(
-              new Promise((resolve) => {
-                newHeadStyleElements.addEventListener('load', () => resolve());
-              }),
-            );
 
-            printDoc.head.appendChild(newHeadStyleElements);
-          }
-        }
+        stylesheetLoadPromises = loadStyleSheets(printDoc, root!);
       }
 
       // Trigger print
diff --git a/packages/x-internals/src/export/index.ts b/packages/x-internals/src/export/index.ts
new file mode 100644
index 0000000000000..923031ca81020
--- /dev/null
+++ b/packages/x-internals/src/export/index.ts
@@ -0,0 +1 @@
+export { loadStyleSheets } from './loadStyleSheets';
diff --git a/packages/x-internals/src/export/loadStyleSheets.tsx b/packages/x-internals/src/export/loadStyleSheets.tsx
new file mode 100644
index 0000000000000..d73413f78bb3e
--- /dev/null
+++ b/packages/x-internals/src/export/loadStyleSheets.tsx
@@ -0,0 +1,50 @@
+/**
+ * Loads all stylesheets from the given root element into the document.
+ * @returns an array of promises that resolve when each stylesheet is loaded
+ * @param document Document to load stylesheets into
+ * @param root Document or ShadowRoot to load stylesheets from
+ */
+export function loadStyleSheets(document: Document, root: Document | ShadowRoot) {
+  const stylesheetLoadPromises: Promise<void>[] = [];
+  const headStyleElements = root.querySelectorAll("style, link[rel='stylesheet']");
+
+  for (let i = 0; i < headStyleElements.length; i += 1) {
+    const node = headStyleElements[i];
+    if (node.tagName === 'STYLE') {
+      const newHeadStyleElements = document.createElement(node.tagName);
+      const sheet = (node as HTMLStyleElement).sheet;
+
+      if (sheet) {
+        let styleCSS = '';
+        for (let j = 0; j < sheet.cssRules.length; j += 1) {
+          if (typeof sheet.cssRules[j].cssText === 'string') {
+            styleCSS += `${sheet.cssRules[j].cssText}\r\n`;
+          }
+        }
+        newHeadStyleElements.appendChild(document.createTextNode(styleCSS));
+        document.head.appendChild(newHeadStyleElements);
+      }
+    } else if (node.getAttribute('href')) {
+      // If `href` tag is empty, avoid loading these links
+
+      const newHeadStyleElements = document.createElement(node.tagName);
+
+      for (let j = 0; j < node.attributes.length; j += 1) {
+        const attr = node.attributes[j];
+        if (attr) {
+          newHeadStyleElements.setAttribute(attr.nodeName, attr.nodeValue || '');
+        }
+      }
+
+      stylesheetLoadPromises.push(
+        new Promise((resolve) => {
+          newHeadStyleElements.addEventListener('load', () => resolve());
+        }),
+      );
+
+      document.head.appendChild(newHeadStyleElements);
+    }
+  }
+
+  return stylesheetLoadPromises;
+}


Reviews:
user: login: cherniavskii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T13:10:52Z
user: login: cherniavskii
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-25T13:15:54Z
user: login: cherniavskii
body: LGTM!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-25T13:16:00Z
user: login: bernardobelchior
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T13:39:41Z
user: login: cherniavskii
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T15:08:59Z
user: login: bernardobelchior
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T15:50:22Z
user: login: alexfauquette
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-28T07:49:23Z

Review comments:
diff_hunk: @@ -0,0 +1,51 @@
+/**
+ * Loads all stylesheets from the given root element into the document.
+ * @returns an array of promises that resolve when each stylesheet is loaded
+ * @param document Document to load stylesheets into
+ * @param root Document or ShadowRoot to load stylesheets from
+ */
+export function loadStyleSheets(document: Document, root: Document | ShadowRoot) {
+  const stylesheetLoadPromises: Promise<void>[] = [];
+  const headStyleElements = root.querySelectorAll("style, link[rel='stylesheet']");
+
+  for (let i = 0; i < headStyleElements.length; i += 1) {
+    const node = headStyleElements[i];
+    if (node.tagName === 'STYLE') {
+      const newHeadStyleElements = document.createElement(node.tagName);
+      const sheet = (node as HTMLStyleElement).sheet;
+
+      if (sheet) {
+        let styleCSS = '';
+        // NOTE: for-of is not supported by IE
path: packages/x-internals/src/export/loadStyleSheets.tsx
user: login: cherniavskii
body: ```suggestion
```
Stale comment, we dropped IE support back in v7
created_at: 2025-04-25T13:10:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,51 @@
+/**
+ * Loads all stylesheets from the given root element into the document.
+ * @returns an array of promises that resolve when each stylesheet is loaded
+ * @param document Document to load stylesheets into
+ * @param root Document or ShadowRoot to load stylesheets from
+ */
+export function loadStyleSheets(document: Document, root: Document | ShadowRoot) {
+  const stylesheetLoadPromises: Promise<void>[] = [];
+  const headStyleElements = root.querySelectorAll("style, link[rel='stylesheet']");
+
+  for (let i = 0; i < headStyleElements.length; i += 1) {
+    const node = headStyleElements[i];
+    if (node.tagName === 'STYLE') {
+      const newHeadStyleElements = document.createElement(node.tagName);
+      const sheet = (node as HTMLStyleElement).sheet;
+
+      if (sheet) {
+        let styleCSS = '';
+        // NOTE: for-of is not supported by IE
path: packages/x-internals/src/export/loadStyleSheets.tsx
user: login: bernardobelchior
body: Yeah, I think I might change this into a `for...of` in a follow-up PR.
created_at: 2025-04-25T13:39:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,51 @@
+/**
+ * Loads all stylesheets from the given root element into the document.
+ * @returns an array of promises that resolve when each stylesheet is loaded
+ * @param document Document to load stylesheets into
+ * @param root Document or ShadowRoot to load stylesheets from
+ */
+export function loadStyleSheets(document: Document, root: Document | ShadowRoot) {
+  const stylesheetLoadPromises: Promise<void>[] = [];
+  const headStyleElements = root.querySelectorAll("style, link[rel='stylesheet']");
+
+  for (let i = 0; i < headStyleElements.length; i += 1) {
+    const node = headStyleElements[i];
+    if (node.tagName === 'STYLE') {
+      const newHeadStyleElements = document.createElement(node.tagName);
+      const sheet = (node as HTMLStyleElement).sheet;
+
+      if (sheet) {
+        let styleCSS = '';
+        // NOTE: for-of is not supported by IE
path: packages/x-internals/src/export/loadStyleSheets.tsx
user: login: cherniavskii
body: `for...of` doesn't seem to be any better than a regular `for` loop in this case though
created_at: 2025-04-25T15:08:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,51 @@
+/**
+ * Loads all stylesheets from the given root element into the document.
+ * @returns an array of promises that resolve when each stylesheet is loaded
+ * @param document Document to load stylesheets into
+ * @param root Document or ShadowRoot to load stylesheets from
+ */
+export function loadStyleSheets(document: Document, root: Document | ShadowRoot) {
+  const stylesheetLoadPromises: Promise<void>[] = [];
+  const headStyleElements = root.querySelectorAll("style, link[rel='stylesheet']");
+
+  for (let i = 0; i < headStyleElements.length; i += 1) {
+    const node = headStyleElements[i];
+    if (node.tagName === 'STYLE') {
+      const newHeadStyleElements = document.createElement(node.tagName);
+      const sheet = (node as HTMLStyleElement).sheet;
+
+      if (sheet) {
+        let styleCSS = '';
+        // NOTE: for-of is not supported by IE
path: packages/x-internals/src/export/loadStyleSheets.tsx
user: login: bernardobelchior
body: Yeah, it would be mostly for ergonomics. 
created_at: 2025-04-25T15:50:22Z
author_association: MEMBER

PR comments:
user: login: mui-bot
created_at: 2025-04-25T10:02:05Z
body: 
<!--
  0 failure: 
  0 warning: 
  
  1 markdown notices
  DangerID: danger-id-Danger;
-->



**Deploy preview:** <a href="https://deploy-preview-17548--material-ui-x.netlify.app/">https://deploy-preview-17548--material-ui-x.netlify.app/</a>
<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a> against 88e9b4c405558dcf8cf97cdb9ca9e30e78f34f60
</p>

author_association: NONE
user: login: codspeed-hq[bot]
created_at: 2025-04-25T10:06:34Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/mui/mui-x/branches/bernardobelchior%3Aload-style-sheets-internals)

### Merging #17548 will **not alter performance**

<sub>Comparing <code>bernardobelchior:load-style-sheets-internals</code> (88e9b4c) with <code>master</code> (cb55e58)</sub>



### Summary

`✅ 8` untouched benchmarks  




author_association: NONE