From 3afafe78291bcca140c554fb741f9dd4d70a1bcc Mon Sep 17 00:00:00 2001 From: Mengdi Chen Date: Fri, 5 Jan 2024 17:07:28 -0500 Subject: [PATCH] feat #5246: add styleContainer option for PrimeReactContext (#5566) * feat #5246: add styleContainer option for useStyle * Pass `context.styleContainer` to `DomHandler.createInlineStyle` * Add docs for StyleContainer --- components/doc/common/apidoc/index.json | 17 ++++++++ components/doc/configuration/appendtodoc.js | 2 +- .../doc/configuration/stylecontainer.js | 39 +++++++++++++++++++ components/lib/api/PrimeReactContext.js | 3 ++ components/lib/api/api.d.ts | 11 ++++++ components/lib/carousel/Carousel.js | 2 +- components/lib/cascadeselect/CascadeSelect.js | 2 +- components/lib/contextmenu/ContextMenu.js | 2 +- components/lib/datatable/DataTable.js | 4 +- components/lib/dialog/Dialog.js | 2 +- components/lib/galleria/GalleriaThumbnails.js | 2 +- components/lib/hooks/useStyle.js | 6 ++- components/lib/megamenu/MegaMenu.js | 2 +- components/lib/orderlist/OrderList.js | 2 +- components/lib/overlaypanel/OverlayPanel.js | 2 +- components/lib/picklist/PickList.js | 2 +- components/lib/tieredmenu/TieredMenu.js | 2 +- components/lib/utils/DomHandler.js | 6 +-- components/lib/utils/utils.d.ts | 2 +- pages/configuration/index.js | 6 +++ 20 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 components/doc/configuration/stylecontainer.js diff --git a/components/doc/common/apidoc/index.json b/components/doc/common/apidoc/index.json index 4d5f529fc4..7f0b9b41ee 100644 --- a/components/doc/common/apidoc/index.json +++ b/components/doc/common/apidoc/index.json @@ -689,6 +689,13 @@ "type": "AppendToType", "description": "This option allows components with overlays like dropdowns or popups to be mounted into either the component or any DOM element, such as document body and self." }, + { + "name": "styleContainer", + "optional": true, + "readonly": false, + "type": "StyleContainerType", + "description": "This option allows `useStyle` to insert dynamic CSS styles into a specific container. This is useful when styles need to be scoped such as in a Shadow DOM." + }, { "name": "autoZIndex", "optional": true, @@ -787,6 +794,13 @@ "type": "Dispatch>", "description": "Sets the \"appendTo\" state of the context." }, + { + "name": "setStyleContainer", + "optional": true, + "readonly": false, + "type": "Dispatch>", + "description": "Sets the \"styleContainer\" state of the context." + }, { "name": "setAutoZIndex", "optional": true, @@ -3317,6 +3331,9 @@ }, "AppendToType": { "values": "\"self\" | HTMLElement | undefined | null" + }, + "StyleContainerType": { + "values": "ShadowRoot | HTMLElement | undefined | null" } } } diff --git a/components/doc/configuration/appendtodoc.js b/components/doc/configuration/appendtodoc.js index ffd30181fa..03dea8705c 100644 --- a/components/doc/configuration/appendtodoc.js +++ b/components/doc/configuration/appendtodoc.js @@ -27,7 +27,7 @@ export default function MyApp({ Component }) {

For components with an overlay like a dropdown, popups can be mounted either into the component or DOM element instance using this option. Valid values are any DOM Element like document body and self. By default all popups - are append to document body via Portals. + are appended to document body via Portals.

diff --git a/components/doc/configuration/stylecontainer.js b/components/doc/configuration/stylecontainer.js new file mode 100644 index 0000000000..f7e0735a0b --- /dev/null +++ b/components/doc/configuration/stylecontainer.js @@ -0,0 +1,39 @@ +import { DocSectionCode } from '@/components/doc/common/docsectioncode'; +import { DocSectionText } from '@/components/doc/common/docsectiontext'; + +export function StyleContainer(props) { + const code = { + basic: ` +//_app.js +import { PrimeReactProvider } from 'primereact/api'; + +root.attachShadow({ mode: 'open' }); // Open the shadowRoot +const mountHere = root.shadowRoot; + +const options = { appendTo: mountHere, styleContainer: mountHere}; + +ReactDOM.createRoot(mountHere).render( + + + + + +); +` + }; + + return ( + <> + +

+ This option allows useStyle to insert dynamic CSS styles into a specific container. This is useful when styles need to be scoped such as in a{' '} + + Shadow DOM + + . By default all dynamic styles are appended to document.head. +

+
+ + + ); +} diff --git a/components/lib/api/PrimeReactContext.js b/components/lib/api/PrimeReactContext.js index 35eb21722b..27667f77e4 100644 --- a/components/lib/api/PrimeReactContext.js +++ b/components/lib/api/PrimeReactContext.js @@ -11,6 +11,7 @@ export const PrimeReactProvider = (props) => { const [inputStyle, setInputStyle] = useState(propsValue.inputStyle || 'outlined'); const [locale, setLocale] = useState(propsValue.locale || 'en'); const [appendTo, setAppendTo] = useState(propsValue.appendTo || null); + const [styleContainer, setStyleContainer] = useState(propsValue.styleContainer || null); const [cssTransition, setCssTransition] = useState(propsValue.cssTransition || true); const [autoZIndex, setAutoZIndex] = useState(propsValue.autoZIndex || true); const [hideOverlaysOnDocumentScrolling, setHideOverlaysOnDocumentScrolling] = useState(propsValue.hideOverlaysOnDocumentScrolling || false); @@ -90,6 +91,8 @@ export const PrimeReactProvider = (props) => { setLocale, appendTo, setAppendTo, + styleContainer, + setStyleContainer, cssTransition, setCssTransition, autoZIndex, diff --git a/components/lib/api/api.d.ts b/components/lib/api/api.d.ts index 7108b391fa..4eb15b8f3c 100644 --- a/components/lib/api/api.d.ts +++ b/components/lib/api/api.d.ts @@ -131,6 +131,8 @@ export type InputStyleType = 'outlined' | 'filled'; export type AppendToType = 'self' | HTMLElement | undefined | null; +export type StyleContainerType = ShadowRoot | HTMLElement | undefined | null; + /** * Filter match modes for DataTable filter menus. */ @@ -157,6 +159,11 @@ export interface APIOptions { * This option allows components with overlays like dropdowns or popups to be mounted into either the component or any DOM element, such as document body and self. */ appendTo?: AppendToType; + /** + * This option allows `useStyle` to insert dynamic CSS styles into a specific container. This is useful when styles need to be scoped such as in a Shadow DOM. + * @defaultValue document.head + */ + styleContainer?: StyleContainerType; /** * ZIndexes are managed automatically to make sure layering of overlay components work seamlessly when combining multiple components. When autoZIndex is false, each group increments its zIndex within itself. */ @@ -229,6 +236,10 @@ export interface APIOptions { * Sets the "appendTo" state of the context. */ setAppendTo?: Dispatch>; + /** + * Sets the "styleContainer" state of the context. + */ + setStyleContainer?: Dispatch>; /** * Sets the "autoZIndex" state of the context. */ diff --git a/components/lib/carousel/Carousel.js b/components/lib/carousel/Carousel.js index 38ba5db9e9..e128a777a6 100644 --- a/components/lib/carousel/Carousel.js +++ b/components/lib/carousel/Carousel.js @@ -350,7 +350,7 @@ export const Carousel = React.memo( const createStyle = () => { if (!carouselStyle.current) { - carouselStyle.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + carouselStyle.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); } let innerHTML = ` diff --git a/components/lib/cascadeselect/CascadeSelect.js b/components/lib/cascadeselect/CascadeSelect.js index f95c24e27a..b7cc9e0f5e 100644 --- a/components/lib/cascadeselect/CascadeSelect.js +++ b/components/lib/cascadeselect/CascadeSelect.js @@ -218,7 +218,7 @@ export const CascadeSelect = React.memo( const createStyle = () => { if (!styleElementRef.current) { - styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); const selector = `${attributeSelectorState}_panel`; const innerHTML = ` diff --git a/components/lib/contextmenu/ContextMenu.js b/components/lib/contextmenu/ContextMenu.js index a933d8836d..bfdb3fe9f0 100644 --- a/components/lib/contextmenu/ContextMenu.js +++ b/components/lib/contextmenu/ContextMenu.js @@ -74,7 +74,7 @@ export const ContextMenu = React.memo( const createStyle = () => { if (!styleElementRef.current) { - styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); const selector = `${attributeSelectorState}`; const innerHTML = ` diff --git a/components/lib/datatable/DataTable.js b/components/lib/datatable/DataTable.js index efdcdf872b..d760c99fd0 100644 --- a/components/lib/datatable/DataTable.js +++ b/components/lib/datatable/DataTable.js @@ -791,12 +791,12 @@ export const DataTable = React.forwardRef((inProps, ref) => { }; const createStyleElement = () => { - styleElement.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + styleElement.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); }; const createResponsiveStyle = () => { if (!responsiveStyleElement.current) { - responsiveStyleElement.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + responsiveStyleElement.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); let tableSelector = `.p-datatable-wrapper ${isVirtualScrollerDisabled() ? '' : '> .p-virtualscroller'} > .p-datatable-table`; let selector = `.p-datatable[${attributeSelector.current}] > ${tableSelector}`; diff --git a/components/lib/dialog/Dialog.js b/components/lib/dialog/Dialog.js index c3cc5ff1ce..593748f237 100644 --- a/components/lib/dialog/Dialog.js +++ b/components/lib/dialog/Dialog.js @@ -389,7 +389,7 @@ export const Dialog = React.forwardRef((inProps, ref) => { }; const createStyle = () => { - styleElement.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + styleElement.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); let innerHTML = ''; diff --git a/components/lib/galleria/GalleriaThumbnails.js b/components/lib/galleria/GalleriaThumbnails.js index b6b73cec8b..68b13f1d7a 100644 --- a/components/lib/galleria/GalleriaThumbnails.js +++ b/components/lib/galleria/GalleriaThumbnails.js @@ -344,7 +344,7 @@ export const GalleriaThumbnails = React.memo( const createStyle = () => { if (!thumbnailsStyle.current) { - thumbnailsStyle.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + thumbnailsStyle.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); } let innerHTML = ` diff --git a/components/lib/hooks/useStyle.js b/components/lib/hooks/useStyle.js index c185b18ba1..2209e85fc1 100644 --- a/components/lib/hooks/useStyle.js +++ b/components/lib/hooks/useStyle.js @@ -19,7 +19,9 @@ export const useStyle = (css, options = {}) => { const load = () => { if (!document) return; - styleRef.current = document.querySelector(`style[data-primereact-style-id="${name}"]`) || document.getElementById(id) || document.createElement('style'); + const styleContainer = context?.styleContainer || document.head; + + styleRef.current = styleContainer.querySelector(`style[data-primereact-style-id="${name}"]`) || document.getElementById(id) || document.createElement('style'); if (!styleRef.current.isConnected) { styleRef.current.type = 'text/css'; @@ -27,7 +29,7 @@ export const useStyle = (css, options = {}) => { media && (styleRef.current.media = media); DomHandler.addNonce(styleRef.current, (context && context.nonce) || PrimeReact.nonce); - document.head.appendChild(styleRef.current); + styleContainer.appendChild(styleRef.current); name && styleRef.current.setAttribute('data-primereact-style-id', name); } diff --git a/components/lib/megamenu/MegaMenu.js b/components/lib/megamenu/MegaMenu.js index 76c51cf265..c1829882c8 100644 --- a/components/lib/megamenu/MegaMenu.js +++ b/components/lib/megamenu/MegaMenu.js @@ -992,7 +992,7 @@ export const MegaMenu = React.memo( const createStyle = () => { if (!styleElementRef.current) { - styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); const selector = `${attributeSelectorState}`; const innerHTML = ` diff --git a/components/lib/orderlist/OrderList.js b/components/lib/orderlist/OrderList.js index 897ca2338c..523338b927 100644 --- a/components/lib/orderlist/OrderList.js +++ b/components/lib/orderlist/OrderList.js @@ -321,7 +321,7 @@ export const OrderList = React.memo( const createStyle = () => { if (!styleElementRef.current) { - styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); let innerHTML = ` @media screen and (max-width: ${props.breakpoint}) { diff --git a/components/lib/overlaypanel/OverlayPanel.js b/components/lib/overlaypanel/OverlayPanel.js index b038bf496d..040576ac39 100644 --- a/components/lib/overlaypanel/OverlayPanel.js +++ b/components/lib/overlaypanel/OverlayPanel.js @@ -173,7 +173,7 @@ export const OverlayPanel = React.forwardRef((inProps, ref) => { const createStyle = () => { if (!styleElement.current) { - styleElement.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + styleElement.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); let innerHTML = ''; diff --git a/components/lib/picklist/PickList.js b/components/lib/picklist/PickList.js index a43695c6bc..38c5bc1776 100644 --- a/components/lib/picklist/PickList.js +++ b/components/lib/picklist/PickList.js @@ -222,7 +222,7 @@ export const PickList = React.memo( const createStyle = () => { if (!styleElementRef.current) { - styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); let innerHTML = ` @media screen and (max-width: ${props.breakpoint}) { diff --git a/components/lib/tieredmenu/TieredMenu.js b/components/lib/tieredmenu/TieredMenu.js index 86695a4e6b..dbd8bf0480 100644 --- a/components/lib/tieredmenu/TieredMenu.js +++ b/components/lib/tieredmenu/TieredMenu.js @@ -520,7 +520,7 @@ export const TieredMenu = React.memo( const createStyle = () => { if (!styleElementRef.current) { - styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce); + styleElementRef.current = DomHandler.createInlineStyle((context && context.nonce) || PrimeReact.nonce, context && context.styleContainer); const selector = `${attributeSelectorState}`; const innerHTML = ` diff --git a/components/lib/utils/DomHandler.js b/components/lib/utils/DomHandler.js index f891a83b63..9f53dc549b 100644 --- a/components/lib/utils/DomHandler.js +++ b/components/lib/utils/DomHandler.js @@ -1067,11 +1067,11 @@ export default class DomHandler { return false; } - static createInlineStyle(nonce) { + static createInlineStyle(nonce, styleContainer = document.head) { let styleElement = document.createElement('style'); DomHandler.addNonce(styleElement, nonce); - document.head.appendChild(styleElement); + styleContainer.appendChild(styleElement); return styleElement; } @@ -1079,7 +1079,7 @@ export default class DomHandler { static removeInlineStyle(styleElement) { if (this.isExist(styleElement)) { try { - document.head.removeChild(styleElement); + styleElement.parentNode.removeChild(styleElement); } catch (error) { // style element may have already been removed in a fast refresh } diff --git a/components/lib/utils/utils.d.ts b/components/lib/utils/utils.d.ts index 18a0f148dc..803e34ed51 100644 --- a/components/lib/utils/utils.d.ts +++ b/components/lib/utils/utils.d.ts @@ -80,7 +80,7 @@ export declare class DomHandler { static applyStyle(el: HTMLElement, style: any): void; static exportCSV(csv: any, filename: string): void; static saveAs(file: { name: string; url: any }): boolean; - static createInlineStyle(nonce?: string): HTMLStyleElement; + static createInlineStyle(nonce?: string, styleContainer?: ShadowRoot | HTMLElement): HTMLStyleElement; static removeInlineStyle(styleElement: HTMLStyleElement): HTMLStyleElement | null; static getTargetElement(target: any): HTMLElement | null; } diff --git a/pages/configuration/index.js b/pages/configuration/index.js index e9b44520c5..a0efc3c42f 100644 --- a/pages/configuration/index.js +++ b/pages/configuration/index.js @@ -1,5 +1,6 @@ import { DocComponent } from '@/components/doc/common/doccomponent'; import { AppendToDoc } from '@/components/doc/configuration/appendtodoc'; +import { StyleContainer } from '@/components/doc/configuration/stylecontainer'; import { CSSTransitionDoc } from '@/components/doc/configuration/csstransitiondoc'; import { FilterMatchModeDoc } from '@/components/doc/configuration/filtermatchmodedoc'; import { HideOverlaysDoc } from '@/components/doc/configuration/hideoverlaysdoc'; @@ -20,6 +21,11 @@ const InstallationPage = () => { label: 'AppendTo', component: AppendToDoc }, + { + id: 'stylecontainer', + label: 'StyleContainer', + component: StyleContainer + }, { id: 'csstransition', label: 'CSS Transition',