diff --git a/packages/react-docs/config/sidebar-routes.js b/packages/react-docs/config/sidebar-routes.js index 7532419034..e2fdd07494 100644 --- a/packages/react-docs/config/sidebar-routes.js +++ b/packages/react-docs/config/sidebar-routes.js @@ -10,6 +10,7 @@ import { MigrateSuccessIcon, RocketIcon, SVGIcon, + ToolsConfigurationIcon, UserTeamIcon, WidgetsIcon, WorkspaceIcon, @@ -61,6 +62,15 @@ export const routes = [ { title: 'React Icons', path: 'contributing/react-icons' }, ], }, + { + title: 'Customization', + icon: (props) => ( + + ), + routes: [ + { title: 'Shadow DOM', path: 'customization/shadow-dom' }, + ], + }, { title: 'Migrations', icon: (props) => ( diff --git a/packages/react-docs/pages/customization/shadow-dom.js b/packages/react-docs/pages/customization/shadow-dom.js new file mode 100644 index 0000000000..e374e0e594 --- /dev/null +++ b/packages/react-docs/pages/customization/shadow-dom.js @@ -0,0 +1,402 @@ +import createCache from '@emotion/cache'; +import { CacheProvider } from '@emotion/react'; +import { + Box, + Button, + Divider, + Drawer, + DrawerOverlay, + DrawerContent, + DrawerHeader, + DrawerBody, + DrawerFooter, + Flex, + Grid, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + PortalManager, + Skeleton, + Stack, + Text, + Toast, + ToastManager, + TonicProvider, + Tooltip, + createTheme, + useColorMode, + usePortalManager, + useToastManager, +} 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 DrawerComponent = ({ onClose }) => { + return ( + + + + + Drawer + + + + + + + + + + + + + + + + + ); +}; + +const ModalComponent = ({ onClose }) => { + return ( + + + + + Modal + + + + + + + + + + + + + + + + + ); +}; + +const ShadowDOMContainer = ({ children, colorMode, ...rest }) => { + const containerRef = useRef(); + const shadowRootElementRef = useRef(); + + useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + + const shadowContainer = container.shadowRoot ?? container.attachShadow({ mode: 'open' }); + const shadowRootElement = document.createElement('div'); + shadowContainer.appendChild(shadowRootElement); + shadowRootElementRef.current = shadowRootElement; + + return () => { + // Empty the shadow DOM content + if (shadowContainer) { + shadowContainer.innerHTML = ''; + } + + shadowRootElementRef.current = null; + }; + }, []); // Run only once on mount + + useEffect(() => { + const shadowRootElement = shadowRootElementRef.current; + const shadowContainer = shadowRootElement.parentNode; + const root = createRoot(shadowRootElement); + const cache = createCache({ + key: 'tonic-ui-shadow', + nonce: NONCE, // Needed to comply with Content Security Policy (CSP) for inline execution + prepend: true, + container: shadowContainer, + }); + const shadowTheme = createTheme({ + cssVariables: { + prefix: 'tonic-shadow', + rootSelector: ':host', + }, + components: { + Drawer: { + defaultProps: { + portalProps: { + containerRef: shadowRootElementRef, + }, + }, + }, + Modal: { + defaultProps: { + portalProps: { + containerRef: shadowRootElementRef, + }, + }, + }, + Popper: { + defaultProps: { + portalProps: { + containerRef: shadowRootElementRef, + }, + }, + }, + }, + }); + + root.render( + + + &:first-of-type': { + mt: '4x', // the space to the top edge of the screen + }, + '[data-toast-placement^="bottom"] > &:last-of-type': { + mb: '4x', // the space to the bottom edge of the screen + }, + '[data-toast-placement$="left"] > &': { + ml: '4x', // the space to the left edge of the screen + }, + '[data-toast-placement$="right"] > &': { + mr: '4x', // the space to the right edge of the screen + }, + }, + }} + containerRef={shadowRootElementRef} + > + + {children} + + + + + ); + }, [children, colorMode]); + + return ( +
+ ); +}; + +const InsideShadowDOMComponent = () => { + const portal = usePortalManager(); + const toast = useToastManager(); + const handleClickDrawer = () => { + portal((close) => ); + }; + const handleClickModal = () => { + portal((close) => ); + }; + const handleClickToast = () => { + const render = ({ id, onClose, placement }) => { + const isTop = placement.includes('top'); + const toastSpacingKey = isTop ? 'pb' : 'pt'; + const styleProps = { + [toastSpacingKey]: '2x', + width: 320, + }; + return ( + + + This is a toast notification + + + ); + }; + const options = { + duration: 5000, + }; + toast(render, options); + }; + + return ( + + + Inside Shadow DOM + + + + + + + + + + + + ); +}; + +const OutsideShadowDOMComponent = () => { + const portal = usePortalManager(); + const toast = useToastManager(); + const handleClickDrawer = () => { + portal((close) => ); + }; + const handleClickModal = () => { + portal((close) => ); + }; + const handleClickToast = () => { + const render = ({ id, onClose, placement }) => { + const isTop = placement.includes('top'); + const toastSpacingKey = isTop ? 'pb' : 'pt'; + const styleProps = { + [toastSpacingKey]: '2x', + width: 320, + }; + return ( + + + This is a toast notification + + + ); + }; + const options = { + duration: 5000, + }; + toast(render, options); + }; + + return ( + + + Outside Shadow DOM + + + + + + + + + + + + ); +}; + +const App = () => { + const [colorMode] = useColorMode(); + + return ( + + + + + + + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/customization/shadow-dom.page.mdx b/packages/react-docs/pages/customization/shadow-dom.page.mdx new file mode 100644 index 0000000000..d75793a76e --- /dev/null +++ b/packages/react-docs/pages/customization/shadow-dom.page.mdx @@ -0,0 +1,140 @@ +# Shadow DOM + +The shadow DOM allows you to encapsulate parts of your application, isolating them from global styles and preventing interference with the regular DOM tree. + +## Shadow DOM Integration + +To enable styling within the shadow DOM, start by importing `createCache` and `CacheProvider`: + +```js +import createCache from '@emotion/cache'; +import { CacheProvider } from '@emotion/react'; +``` + +### 1. Applying styles inside the shadow DOM + +The shadow DOM creates an isolated DOM tree attached to a host element, helping prevent style conflicts across components. Below is an example of how to create and style a shadow DOM: + +```js +const container = document.querySelector('#root'); +const shadowContainer = container.shadowRoot || container.attachShadow({ mode: 'open' }); +const shadowRootElement = document.createElement('div'); +shadowContainer.appendChild(shadowRootElement); + +// Consider using `const shadowRootElementRef = useRef();` if working within a functional component +const shadowRootElementRef = { + current: shadowRootElement, +}; + +const cache = createCache({ + key: 'tonic-ui-shadow', // or 'css' + prepend: true, + container: shadowContainer, + nonce, // [optional] to comply with Content Security Policy (CSP) for inline execution +}); +``` + +### 2. Theming components inside the shadow DOM + +Components such as `Drawer`, `Modal`, and `Popper` from Tonic UI often render outside the main DOM hierarchy using a `Portal`. By default, these portals render in `document.body`. When using the shadow DOM, they need to render within the shadow container: + +```js +const shadowTheme = createTheme({ + components: { + Drawer: { + defaultProps: { + portalProps: { + containerRef: shadowRootElementRef, + }, + }, + }, + Modal: { + defaultProps: { + portalProps: { + containerRef: shadowRootElementRef, + }, + }, + }, + Popper: { + defaultProps: { + portalProps: { + containerRef: shadowRootElementRef, + }, + }, + }, + }, +}); +``` + +### 3. Using CSS theme variables (optional) + +To use CSS theme variables within the shadow DOM, specify the root selector for generating the CSS variables: + +```js +const shadowTheme = createTheme({ + cssVariables: { + prefix: 'tonic-shadow', + rootSelector: ':host', + }, + components: { + // Same as the above step + }, +}); +``` + +### 4. Rendering components inside the shadow DOM + +The following code snippet illustrates how to render a React application inside the shadow DOM: + +```jsx +React.createRoot(shadowRootElement).render( + + + &:first-of-type': { + mt: '4x', // the space to the top edge of the screen + }, + '[data-toast-placement^="bottom"] > &:last-of-type': { + mb: '4x', // the space to the bottom edge of the screen + }, + '[data-toast-placement$="left"] > &': { + ml: '4x', // the space to the left edge of the screen + }, + '[data-toast-placement$="right"] > &': { + mr: '4x', // the space to the right edge of the screen + }, + }, + }} + containerRef={shadowRootElementRef} + > + + + + + + +); +``` + +## Demo + +This example applies a global button style. The button outside the shadow DOM inherits the global styling, while the button inside the shadow DOM remains unaffected: + +```jsx +sx={{ + 'button': { + opacity: '.65 !important', + }, +}} +``` + +{render('./shadow-dom')}