Skip to content

Commit

Permalink
Merge pull request #54 from CityOfDetroit/detroitmi.build-2
Browse files Browse the repository at this point in the history
Use Shadow DOM for style encapsulation
  • Loading branch information
jmcbroom authored Jan 21, 2025
2 parents 6399c80 + ad189fd commit 9917fd5
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 8 deletions.
120 changes: 120 additions & 0 deletions src/ShadowDOMWrapper.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { Theme } from "@radix-ui/themes";
import 'maplibre-gl/dist/maplibre-gl.css';
import './styles/index.css'; // Import app styles (including Tailwind)
import "@radix-ui/themes/styles.css";
import "@radix-ui/colors/blue-alpha.css";
import "@radix-ui/colors/blue.css";
import "@radix-ui/colors/green-alpha.css";
import "@radix-ui/colors/green.css";
import "@radix-ui/colors/gray-alpha.css";
import "@radix-ui/colors/gray.css";

const ShadowDOMWrapper = () => {
const hostRef = useRef(null);
const shadowRootRef = useRef(null);
const rootRef = useRef(null);

useEffect(() => {
if (hostRef.current && !shadowRootRef.current) {
// Create shadow root
shadowRootRef.current = hostRef.current.attachShadow({ mode: 'open' });

// Create a container for React content
const container = document.createElement('div');
container.id = 'shadow-root';
shadowRootRef.current.appendChild(container);

const copyRootVariables = () => {
const link = shadowRootRef.current.querySelector('#bundled-styles');

if (link) {
const waitForSheet = new Promise((resolve) => {
if (link.sheet) {
resolve(link.sheet);
return;
}
link.onload = () => resolve(link.sheet);
});

waitForSheet.then(sheet => {
let hostStyles = '';

Array.from(sheet.cssRules).forEach(rule => {
// Handle regular style rules
if (rule.type === CSSRule.STYLE_RULE && rule.selectorText.includes(':root')) {
const newSelector = rule.selectorText.replace(/:root/g, ':host');
hostStyles += `${newSelector} { ${rule.style.cssText} }\n`;
}
// Handle media queries and supports rules
else if (rule.type === CSSRule.MEDIA_RULE || rule.type === CSSRule.SUPPORTS_RULE) {
let mediaRules = '';
Array.from(rule.cssRules).forEach(nestedRule => {
if (nestedRule.selectorText && nestedRule.selectorText.includes(':root')) {
const newSelector = nestedRule.selectorText.replace(/:root/g, ':host');
mediaRules += `${newSelector} { ${nestedRule.style.cssText} }\n`;
}
});
if (mediaRules) {
hostStyles += `${rule.conditionText} {\n${mediaRules}}\n`;
}
}
});

// Create a style block with the transformed rules
const variablesStyle = document.createElement('style');
variablesStyle.textContent = hostStyles;

shadowRootRef.current.insertBefore(variablesStyle, shadowRootRef.current.firstChild);
});
}
};

// Add stylesheets
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = './base-unit-tools.css';
link.id = 'bundled-styles';
shadowRootRef.current.appendChild(link);

// Wait for stylesheet to load before copying variables
link.onload = copyRootVariables;

// Create root and render app
rootRef.current = createRoot(container);
rootRef.current.render(
<React.StrictMode>
<Theme accentColor="green" grayColor="sand" radius="small" scaling="100%">
<App mode={'embedded'} basePath={''}
/>
</Theme>
</React.StrictMode>
);
}

return () => {
if (rootRef.current) {
rootRef.current.unmount();
}
};
});

return <div ref={hostRef} className="base-unit-tools-container" />;
};

// Modified initialization function
export const initEmbeddedBaseUnitTools = (elementId) => {
const container = document.getElementById(elementId);
if (!container) {
console.error(`Container with id "${elementId}" not found`);
return;
}

const root = createRoot(container);
root.render(<ShadowDOMWrapper />);
};

// Export for use in embedded-index.js
export default initEmbeddedBaseUnitTools;
8 changes: 4 additions & 4 deletions src/embedded-index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { initBaseUnitTools } from './index.jsx';
import { initEmbeddedBaseUnitTools } from './ShadowDOMWrapper';

initBaseUnitTools('root', {
mode: 'embedded',
});
if (document.getElementById('root')) {
initEmbeddedBaseUnitTools('root');
}
8 changes: 4 additions & 4 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import "@radix-ui/themes/styles.css";
import { Theme } from "@radix-ui/themes";

// Function to initialize the app
export function initBaseUnitTools(elementId, options = {}) {
function initStandaloneBaseUnitTools(elementId) {
const root = createRoot(document.getElementById(elementId));
root.render(
<React.StrictMode>
<Theme accentColor="green" grayColor="sand" radius="small" scaling="100%">
<App
mode={options.mode || 'embedded'}
basePath={options.basePath || ''}
mode={'standalone'}
basePath={''}
/>
</Theme>
</React.StrictMode>
Expand All @@ -23,5 +23,5 @@ export function initBaseUnitTools(elementId, options = {}) {

// If running standalone
if (document.getElementById('root')) {
initBaseUnitTools('root', { mode: 'standalone' });
initStandaloneBaseUnitTools('root');
}
8 changes: 8 additions & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ const embeddedConfig = {
output: {
format: 'es',
entryFileNames: 'base-unit-tools.js',
assetFileNames: (assetInfo) => {
// Ensure CSS files are named predictably
if (assetInfo.name.endsWith('.css')) {
return 'base-unit-tools.css';
}
// Other assets can use a hash
return 'assets/[name]-[hash][extname]';
}
}
},
cssCodeSplit: false
Expand Down

0 comments on commit 9917fd5

Please sign in to comment.