Skip to content

Commit

Permalink
docs: include Shadow DOM in the Customization section
Browse files Browse the repository at this point in the history
  • Loading branch information
cheton committed Dec 2, 2024
1 parent fbb0229 commit 43bb419
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 0 deletions.
10 changes: 10 additions & 0 deletions packages/react-docs/config/sidebar-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MigrateSuccessIcon,
RocketIcon,
SVGIcon,
ToolsConfigurationIcon,
UserTeamIcon,
WidgetsIcon,
WorkspaceIcon,
Expand Down Expand Up @@ -61,6 +62,15 @@ export const routes = [
{ title: 'React Icons', path: 'contributing/react-icons' },
],
},
{
title: 'Customization',
icon: (props) => (
<ToolsConfigurationIcon size="4x" {...props} />
),
routes: [
{ title: 'Shadow DOM', path: 'customization/shadow-dom' },
],
},
{
title: 'Migrations',
icon: (props) => (
Expand Down
14 changes: 14 additions & 0 deletions packages/react-docs/pages/customization/index.page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useRouter } from 'next/router';
import { useOnce } from '@tonic-ui/react-hooks';

const Root = () => {
const router = useRouter();

useOnce(() => {
router.push('/customization/shadow-dom');
});

return null;
};

export default Root;
188 changes: 188 additions & 0 deletions packages/react-docs/pages/customization/shadow-dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import {
Box,
Button,
Flex,
Grid,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
PortalManager,
Stack,
Text,
TonicProvider,
createTheme,
useColorMode,
usePortalManager,
} from '@tonic-ui/react';
import BorderedBox from '@/components/BorderedBox';
import React, { useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';

const NONCE = process.env.NONCE ?? '';

const TestModal = ({ onClose }) => {
return (
<Modal
closeOnEsc
closeOnOutsideClick
isClosable
isOpen
onClose={onClose}
size="sm"
>
<ModalOverlay />
<ModalContent>
<ModalHeader>
Modal Header
</ModalHeader>
<ModalBody>
Modal Body
</ModalBody>
<ModalFooter>
<Grid templateColumns="repeat(2, 1fr)" columnGap="2x">
<Button variant="primary" onClick={onClose}>
OK
</Button>
<Button variant="default" onClick={onClose}>
Cancel
</Button>
</Grid>
</ModalFooter>
</ModalContent>
</Modal>
);
};

const ShadowDOMContainer = ({ children, colorMode, ...rest }) => {
const shadowContainerRef = useRef();
const rootRef = useRef(); // React root
const cacheRef = useRef(); // Emotion cache

useEffect(() => {
const shadowContainer = shadowContainerRef.current;
if (!shadowContainer) {
return;
}

let shadowRoot = shadowContainer.shadowRoot;
if (!shadowRoot) {
shadowRoot = shadowContainer.attachShadow({ mode: 'open' });
}

cacheRef.current = createCache({
key: 'tonic-ui-shadow',
nonce: NONCE, // Needed to comply with Content Security Policy (CSP) for inline execution
prepend: true,
container: shadowRoot,
});

rootRef.current = createRoot(shadowRoot);

return () => {
// Cleanup: unmount root when component unmounts
if (rootRef.current) {
rootRef.current.unmount();
}
};
}, []); // Run only once on mount

useEffect(() => {
// Re-render when children changes
const root = rootRef.current;
const cache = cacheRef.current;
const shadowTheme = createTheme({
cssVariables: {
prefix: 'tonic-shadow',
rootSelector: ':host',
},
});

root.render(
<CacheProvider value={cache}>
<TonicProvider
colorMode={{
value: colorMode,
}}
theme={shadowTheme}
>
<PortalManager>
{children}
</PortalManager>
</TonicProvider>
</CacheProvider>
);
}, [children, colorMode]);

return (
<Box
ref={shadowContainerRef}
{...rest}
/>
);
};

const ShadowDOMComponent = () => {
const portal = usePortalManager();

return (
<BorderedBox py="4x" px="6x">
<Stack
spacing="2x"
sx={{ textAlign: 'center' }}
>
<Text>Shadow DOM</Text>
<Button
variant="primary"
onClick={() => {
portal((close) => <TestModal onClose={close} />);
}}
>
Open Modal
</Button>
</Stack>
</BorderedBox>
);
};

const NativeDOMComponent = () => {
const portal = usePortalManager();

return (
<BorderedBox py="4x" px="6x">
<Stack
spacing="2x"
sx={{ textAlign: 'center' }}
>
<Text>Native DOM</Text>
<Button
variant="primary"
onClick={() => {
portal((close) => <TestModal onClose={close} />);
}}
>
Open Modal
</Button>
</Stack>
</BorderedBox>
);
};

const App = () => {
const [colorMode] = useColorMode();

return (
<Flex columnGap="8x">
<ShadowDOMContainer colorMode={colorMode}>
<ShadowDOMComponent />
</ShadowDOMContainer>
<NativeDOMComponent />
</Flex>
);
};

export default App;
3 changes: 3 additions & 0 deletions packages/react-docs/pages/customization/shadow-dom.page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Shadow DOM

{render('./shadow-dom')}

0 comments on commit 43bb419

Please sign in to comment.