From 679aac08b8ed45a24a1dc375a3b4d6abecaeb124 Mon Sep 17 00:00:00 2001 From: Ivan Runets Date: Tue, 10 Feb 2026 17:10:15 +0300 Subject: [PATCH 1/3] feat: add support for Shadow DOM for DropdownContainer - Updated `UuiEnhancedApp` to accept a `shadowRootHost` for Shadow DOM integration. - Introduced a new example demonstrating UUI context usage within a Shadow DOM. - Updated documentation and type definitions to reflect the new `shadowRootHost` property. - fixed `autoFocus` always being `true` even when `false` is passed through params. --- app/build-config/craco.config.js | 2 + .../UseUuiServicesShadowDOM.example.tsx | 85 +++++++++++++++++++ .../docs/pages/contexts/contextProvider.json | 3 +- app/src/helpers/appRootUtils.ts | 1 + app/src/index.tsx | 1 + changelog.md | 2 + .../contexts-UseUuiServicesShadowDOM.json | 10 +++ .../src/overlays/DropdownContainer.tsx | 6 +- uui-core/src/hooks/useUuiServices.ts | 5 +- uui-core/src/types/contexts.ts | 2 + 10 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 app/src/docs/_examples/contexts/UseUuiServicesShadowDOM.example.tsx create mode 100644 public/docs/content/contexts-UseUuiServicesShadowDOM.json diff --git a/app/build-config/craco.config.js b/app/build-config/craco.config.js index 6aa47bfda0..144bba4abb 100644 --- a/app/build-config/craco.config.js +++ b/app/build-config/craco.config.js @@ -165,6 +165,8 @@ function configureWebpack(config, { paths }) { changePluginByName(config, 'HtmlWebpackPlugin', (plugin) => { plugin.userOptions.isWrapUuiAppInShadowDom = isWrapUuiAppInShadowDom; + // Template receives plugin.options, not userOptions (options is a copy made at plugin construction time) + plugin.options.isWrapUuiAppInShadowDom = isWrapUuiAppInShadowDom; }); changePluginByName(config, 'ForkTsCheckerWebpackPlugin', (plugin) => { // custom formatter can be removed when next bug is fixed: diff --git a/app/src/docs/_examples/contexts/UseUuiServicesShadowDOM.example.tsx b/app/src/docs/_examples/contexts/UseUuiServicesShadowDOM.example.tsx new file mode 100644 index 0000000000..1c73604319 --- /dev/null +++ b/app/src/docs/_examples/contexts/UseUuiServicesShadowDOM.example.tsx @@ -0,0 +1,85 @@ +// Note: please remove @ts-nocheck comment in real app, it's here only because it's our local code example. +// @ts-nocheck +import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { UuiContext, HistoryAdaptedRouter, useUuiServices, DragGhost, IProcessRequest } from '@epam/uui-core'; +import { Modals, Snackbar } from '@epam/uui-components'; +import { ErrorHandler } from '@epam/promo'; +import { createBrowserHistory } from 'history'; +import { svc } from '../../../services'; +import { Router } from 'react-router'; + +const history = createBrowserHistory(); +const router = new HistoryAdaptedRouter(history); + +function getAppRootNode() { + const root = document.getElementById('root'); + root.attachShadow({ mode: 'open' }); + + const host = document.createElement('div'); + host.className = 'uui-theme-promo'; + root.shadowRoot!.appendChild(host); + + return host; +} + +/** + * API definition example + */ +type TApi = ReturnType; +function apiDefinition(processRequest: IProcessRequest) { + return { + loadDataExample() { + return processRequest('url goes here', 'GET'); + }, + loadAppContextData() { + return processRequest('url goes here', 'GET'); + }, + // ... other api are defined here + }; +} + +function UuiEnhancedApp({ container }: { container: HTMLElement }) { + const shadowRootHost = useMemo(() => { + const rootNode = container.getRootNode(); + return rootNode instanceof ShadowRoot ? rootNode : null; + }, [container]); + + const [isLoaded, setIsLoaded] = React.useState(false); + const { services } = useUuiServices({ + apiDefinition, + router, + shadowRootHost, // you can provide shadow root here, if not provided, FocusLock will use document.activeElement as the active element. + }); + + React.useEffect(() => { + Object.assign(svc, services); + // app context is loaded here + loadAppContext().then((appCtx) => { + services.uuiApp = appCtx; + setIsLoaded(true); + }); + }, [services]); + + if (isLoaded) { + return ( + ( + + + + Your App component + + + + + + + ) + ); + } + return null; +} + +const container = getAppRootNode(); +const root = createRoot(container); +root.render(); diff --git a/app/src/docs/pages/contexts/contextProvider.json b/app/src/docs/pages/contexts/contextProvider.json index 660b3563c1..92ed0b6eff 100644 --- a/app/src/docs/pages/contexts/contextProvider.json +++ b/app/src/docs/pages/contexts/contextProvider.json @@ -7,7 +7,8 @@ { "name": "Initialization", "componentPath": "contexts/UseUuiServices.example.tsx", "onlyCode": true }, { "name": "Usage", "componentPath": "contexts/UuiServicesUsage.example.tsx", "onlyCode": true }, { "name": "Advanced setup", "componentPath": "contexts/UseUuiServicesAdvanced.example.tsx", "onlyCode": true }, - { "name": "With react-router v.6", "componentPath": "contexts/UseUuiServicesRR6.example.tsx", "onlyCode": true } + { "name": "With react-router v.6", "componentPath": "contexts/UseUuiServicesRR6.example.tsx", "onlyCode": true }, + { "name": "With app hosted in Shadow DOM", "componentPath": "contexts/UseUuiServicesShadowDOM.example.tsx", "onlyCode": true } ], "tags": ["contexts"] } \ No newline at end of file diff --git a/app/src/helpers/appRootUtils.ts b/app/src/helpers/appRootUtils.ts index 4d9a73ff1d..638e9e49eb 100644 --- a/app/src/helpers/appRootUtils.ts +++ b/app/src/helpers/appRootUtils.ts @@ -16,6 +16,7 @@ export function getAppRootNode() { div = document.createElement('div'); div.id = SHADOW_ROOT_ID; div.className = document.body.className; + div.style.height = '100%'; document.body.className = ''; defaultRoot.shadowRoot.appendChild(div); } diff --git a/app/src/index.tsx b/app/src/index.tsx index ba67d95666..7be3afc7f0 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -83,6 +83,7 @@ function UuiEnhancedApp() { router, apiReloginPath: 'api/auth/login', apiPingPath: 'api/auth/ping', + shadowRootHost: document.getElementById('root')?.shadowRoot, }); useEffect(() => { diff --git a/changelog.md b/changelog.md index daa8e848c2..67a1e95835 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,7 @@ # 6.4.4 - xx.xx.2026 **What's New** +* [DropdownContainer]: added Shadow DOM support. * [Blocker]: added `inset` prop (`BlockerInset`: top, bottom, left, right in px) to control the blocker's coverage area. * Added Cursor AI integration with skills and developer documentation to improve AI-assisted development workflow. * [Dropdown]: added `fallbackPlacements` prop to customize alternative placements when preferred placement doesn't fit @@ -17,6 +18,7 @@ **What's Fixed** +* [DropdownContainer]: fixed `autoFocus` always being `true` even when `false` is passed through params * [DataPickerBody]: empty search results are announced via a **polite** off-screen `role="status"` region [#1506](https://github.com/epam/UUI/issues/1506) Case №9. * [VirtualList]: fixed loading `Blocker` not fully covering the visible area. * [FiltersPanel]: fixed filters being centered instead of left-aligned inside dropdown popups ([#3065](https://github.com/epam/UUI/issues/3065)) diff --git a/public/docs/content/contexts-UseUuiServicesShadowDOM.json b/public/docs/content/contexts-UseUuiServicesShadowDOM.json new file mode 100644 index 0000000000..8233a709b6 --- /dev/null +++ b/public/docs/content/contexts-UseUuiServicesShadowDOM.json @@ -0,0 +1,10 @@ +[ + { + "type": "paragraph", + "children": [ + { + "text": "Example how to assemble UUI context with app hosted in Shadow DOM." + } + ] + } +] \ No newline at end of file diff --git a/uui-components/src/overlays/DropdownContainer.tsx b/uui-components/src/overlays/DropdownContainer.tsx index b2a9df9916..efb6e99293 100644 --- a/uui-components/src/overlays/DropdownContainer.tsx +++ b/uui-components/src/overlays/DropdownContainer.tsx @@ -3,6 +3,7 @@ import FocusLock from 'react-focus-lock'; import { uuiElement, IHasCX, IHasChildren, cx, IHasRawProps, uuiMarkers, IHasForwardedRef, IDropdownBodyProps, IHasStyleAttrs, + useUuiContext, } from '@epam/uui-core'; import { VPanel } from '../layout/flexItems/VPanel'; import PopoverArrow from './PopoverArrow'; @@ -57,6 +58,8 @@ export const DropdownContainer = React.forwardRef((props: DropdownContainerProps persistentFocus = true, } = props; + const { shadowRootHost } = useUuiContext(); + function renderDropdownContainer() { return ( {renderDropdownContainer()} diff --git a/uui-core/src/hooks/useUuiServices.ts b/uui-core/src/hooks/useUuiServices.ts index 879f725b14..5a7aa09b7a 100644 --- a/uui-core/src/hooks/useUuiServices.ts +++ b/uui-core/src/hooks/useUuiServices.ts @@ -22,11 +22,13 @@ export interface UseUuiServicesProps extends UuiServicesProps appContext?: TAppContext; /** Instance of the router */ router: IRouterContext; + /** Shadow root host. If not provided, FocusLock will use document.activeElement as the active element. */ + shadowRootHost?: ShadowRoot; } function createServices(props: UseUuiServicesProps) { const { - router, appContext, apiDefinition, apiReloginPath, apiServerUrl, apiPingPath, fetch, + router, appContext, apiDefinition, apiReloginPath, apiServerUrl, apiPingPath, fetch, shadowRootHost, } = props; const uuiLayout = new LayoutContext(); @@ -57,6 +59,7 @@ function createServices(props: UseUuiServicesProps extends UuiContexts { uuiApp: TAppContext; /** React router history instance */ history?: IHistory4; + /** Shadow root host. If not provided, FocusLock will use document.activeElement as the active element. */ + shadowRootHost?: ShadowRoot; } From 374707a2f9a423ca667d7abb4a70fe0fc2a0395e Mon Sep 17 00:00:00 2001 From: Ivan Runets Date: Thu, 12 Feb 2026 10:56:44 +0300 Subject: [PATCH 2/3] refactor: remove shadowRootHost support and update DropdownContainer focus handling - Removed `shadowRootHost` from `useUuiServices` and related components. - Updated `DropdownContainer` to manage focus lock correctly without relying on `shadowRootHost`. - Deleted example demonstrating Shadow DOM usage as it is no longer supported. - Updated changelog to reflect changes in focus handling for `DropdownContainer`. --- .../UseUuiServicesShadowDOM.example.tsx | 85 ------------------- .../docs/pages/contexts/contextProvider.json | 3 +- app/src/index.tsx | 1 - .../contexts-UseUuiServicesShadowDOM.json | 10 --- .../src/overlays/DropdownContainer.tsx | 11 ++- uui-core/src/hooks/useUuiServices.ts | 5 +- uui-core/src/types/contexts.ts | 2 - 7 files changed, 10 insertions(+), 107 deletions(-) delete mode 100644 app/src/docs/_examples/contexts/UseUuiServicesShadowDOM.example.tsx delete mode 100644 public/docs/content/contexts-UseUuiServicesShadowDOM.json diff --git a/app/src/docs/_examples/contexts/UseUuiServicesShadowDOM.example.tsx b/app/src/docs/_examples/contexts/UseUuiServicesShadowDOM.example.tsx deleted file mode 100644 index 1c73604319..0000000000 --- a/app/src/docs/_examples/contexts/UseUuiServicesShadowDOM.example.tsx +++ /dev/null @@ -1,85 +0,0 @@ -// Note: please remove @ts-nocheck comment in real app, it's here only because it's our local code example. -// @ts-nocheck -import * as React from 'react'; -import { createRoot } from 'react-dom/client'; -import { UuiContext, HistoryAdaptedRouter, useUuiServices, DragGhost, IProcessRequest } from '@epam/uui-core'; -import { Modals, Snackbar } from '@epam/uui-components'; -import { ErrorHandler } from '@epam/promo'; -import { createBrowserHistory } from 'history'; -import { svc } from '../../../services'; -import { Router } from 'react-router'; - -const history = createBrowserHistory(); -const router = new HistoryAdaptedRouter(history); - -function getAppRootNode() { - const root = document.getElementById('root'); - root.attachShadow({ mode: 'open' }); - - const host = document.createElement('div'); - host.className = 'uui-theme-promo'; - root.shadowRoot!.appendChild(host); - - return host; -} - -/** - * API definition example - */ -type TApi = ReturnType; -function apiDefinition(processRequest: IProcessRequest) { - return { - loadDataExample() { - return processRequest('url goes here', 'GET'); - }, - loadAppContextData() { - return processRequest('url goes here', 'GET'); - }, - // ... other api are defined here - }; -} - -function UuiEnhancedApp({ container }: { container: HTMLElement }) { - const shadowRootHost = useMemo(() => { - const rootNode = container.getRootNode(); - return rootNode instanceof ShadowRoot ? rootNode : null; - }, [container]); - - const [isLoaded, setIsLoaded] = React.useState(false); - const { services } = useUuiServices({ - apiDefinition, - router, - shadowRootHost, // you can provide shadow root here, if not provided, FocusLock will use document.activeElement as the active element. - }); - - React.useEffect(() => { - Object.assign(svc, services); - // app context is loaded here - loadAppContext().then((appCtx) => { - services.uuiApp = appCtx; - setIsLoaded(true); - }); - }, [services]); - - if (isLoaded) { - return ( - ( - - - - Your App component - - - - - - - ) - ); - } - return null; -} - -const container = getAppRootNode(); -const root = createRoot(container); -root.render(); diff --git a/app/src/docs/pages/contexts/contextProvider.json b/app/src/docs/pages/contexts/contextProvider.json index 92ed0b6eff..660b3563c1 100644 --- a/app/src/docs/pages/contexts/contextProvider.json +++ b/app/src/docs/pages/contexts/contextProvider.json @@ -7,8 +7,7 @@ { "name": "Initialization", "componentPath": "contexts/UseUuiServices.example.tsx", "onlyCode": true }, { "name": "Usage", "componentPath": "contexts/UuiServicesUsage.example.tsx", "onlyCode": true }, { "name": "Advanced setup", "componentPath": "contexts/UseUuiServicesAdvanced.example.tsx", "onlyCode": true }, - { "name": "With react-router v.6", "componentPath": "contexts/UseUuiServicesRR6.example.tsx", "onlyCode": true }, - { "name": "With app hosted in Shadow DOM", "componentPath": "contexts/UseUuiServicesShadowDOM.example.tsx", "onlyCode": true } + { "name": "With react-router v.6", "componentPath": "contexts/UseUuiServicesRR6.example.tsx", "onlyCode": true } ], "tags": ["contexts"] } \ No newline at end of file diff --git a/app/src/index.tsx b/app/src/index.tsx index 7be3afc7f0..ba67d95666 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -83,7 +83,6 @@ function UuiEnhancedApp() { router, apiReloginPath: 'api/auth/login', apiPingPath: 'api/auth/ping', - shadowRootHost: document.getElementById('root')?.shadowRoot, }); useEffect(() => { diff --git a/public/docs/content/contexts-UseUuiServicesShadowDOM.json b/public/docs/content/contexts-UseUuiServicesShadowDOM.json deleted file mode 100644 index 8233a709b6..0000000000 --- a/public/docs/content/contexts-UseUuiServicesShadowDOM.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "type": "paragraph", - "children": [ - { - "text": "Example how to assemble UUI context with app hosted in Shadow DOM." - } - ] - } -] \ No newline at end of file diff --git a/uui-components/src/overlays/DropdownContainer.tsx b/uui-components/src/overlays/DropdownContainer.tsx index efb6e99293..58df6fee4a 100644 --- a/uui-components/src/overlays/DropdownContainer.tsx +++ b/uui-components/src/overlays/DropdownContainer.tsx @@ -3,11 +3,11 @@ import FocusLock from 'react-focus-lock'; import { uuiElement, IHasCX, IHasChildren, cx, IHasRawProps, uuiMarkers, IHasForwardedRef, IDropdownBodyProps, IHasStyleAttrs, - useUuiContext, } from '@epam/uui-core'; import { VPanel } from '../layout/flexItems/VPanel'; import PopoverArrow from './PopoverArrow'; import { ReactFocusLockProps } from 'react-focus-lock'; +import { useCallback, useState } from 'react'; export interface DropdownContainerProps extends IHasCX, @@ -57,13 +57,18 @@ export const DropdownContainer = React.forwardRef((props: DropdownContainerProps returnFocus = true, persistentFocus = true, } = props; + const [shadowRootHost, setShadowRootHost] = useState(null); - const { shadowRootHost } = useUuiContext(); + const saveShadowRootHost = useCallback((node: HTMLElement | null) => { + if (!node) return; + const root = node.getRootNode(); + setShadowRootHost(root instanceof ShadowRoot ? root : null); + }, []); function renderDropdownContainer() { return ( : undefined } + forwardedRef={ !focusLock ? (ref as React.ForwardedRef) : saveShadowRootHost } cx={ cx(uuiElement.dropdownBody, uuiMarkers.lockFocus, props.cx) } style={ { ...props.style, diff --git a/uui-core/src/hooks/useUuiServices.ts b/uui-core/src/hooks/useUuiServices.ts index 5a7aa09b7a..879f725b14 100644 --- a/uui-core/src/hooks/useUuiServices.ts +++ b/uui-core/src/hooks/useUuiServices.ts @@ -22,13 +22,11 @@ export interface UseUuiServicesProps extends UuiServicesProps appContext?: TAppContext; /** Instance of the router */ router: IRouterContext; - /** Shadow root host. If not provided, FocusLock will use document.activeElement as the active element. */ - shadowRootHost?: ShadowRoot; } function createServices(props: UseUuiServicesProps) { const { - router, appContext, apiDefinition, apiReloginPath, apiServerUrl, apiPingPath, fetch, shadowRootHost, + router, appContext, apiDefinition, apiReloginPath, apiServerUrl, apiPingPath, fetch, } = props; const uuiLayout = new LayoutContext(); @@ -59,7 +57,6 @@ function createServices(props: UseUuiServicesProps extends UuiContexts { uuiApp: TAppContext; /** React router history instance */ history?: IHistory4; - /** Shadow root host. If not provided, FocusLock will use document.activeElement as the active element. */ - shadowRootHost?: ShadowRoot; } From 0aa3de61855e9353140208b8b9baf4eb6d6225c7 Mon Sep 17 00:00:00 2001 From: Ivan Runets Date: Tue, 31 Mar 2026 13:26:56 +0300 Subject: [PATCH 3/3] feat: add Shadow DOM support for dropdowns --- .../content/unitTestingGuide-cookbook.json | 6 +- .../unitTestingGuide-getting-started.json | 2 +- public/docs/docsGenOutput/docsGenOutput.json | 24 ++-- test-utils/src/jsdom/setupJsDom.js | 4 +- uui-components/package.json | 2 +- .../src/navigation/MainMenu/Burger/Burger.tsx | 2 +- uui-components/src/overlays/Dropdown.tsx | 2 +- .../src/overlays/DropdownContainer.tsx | 14 +-- uui-components/src/overlays/ModalBlocker.tsx | 2 +- .../navigation/MainMenu/MainMenuDropdown.tsx | 2 +- .../MainMenuDropdown.test.tsx.snap | 70 +++++------ uui/components/pickers/DataPickerBody.tsx | 4 +- uui/components/pickers/PickerModal.tsx | 2 +- uui/package.json | 2 +- yarn.lock | 110 ++++++++---------- 15 files changed, 104 insertions(+), 144 deletions(-) diff --git a/public/docs/content/unitTestingGuide-cookbook.json b/public/docs/content/unitTestingGuide-cookbook.json index c5d94f9d46..4802d31ebe 100644 --- a/public/docs/content/unitTestingGuide-cookbook.json +++ b/public/docs/content/unitTestingGuide-cookbook.json @@ -609,7 +609,7 @@ "type": "paragraph", "children": [ { - "text": "react-focus-lock", + "text": "@epam/uui-react-focus-lock-fork", "uui-richTextEditor-bold": true } ] @@ -679,7 +679,7 @@ "text": "(" }, { - "text": "'react-focus-lock'", + "text": "'@epam/uui-react-focus-lock-fork'", "uui-richTextEditor-bold": true }, { @@ -693,7 +693,7 @@ "text": "(" }, { - "text": "'react-focus-lock'", + "text": "'@epam/uui-react-focus-lock-fork'", "uui-richTextEditor-bold": true }, { diff --git a/public/docs/content/unitTestingGuide-getting-started.json b/public/docs/content/unitTestingGuide-getting-started.json index a406b92e5c..c347180475 100644 --- a/public/docs/content/unitTestingGuide-getting-started.json +++ b/public/docs/content/unitTestingGuide-getting-started.json @@ -78,7 +78,7 @@ "text": ", " }, { - "text": "react-focus-lock", + "text": "@epam/uui-react-focus-lock-fork", "uui-richTextEditor-code": true }, { diff --git a/public/docs/docsGenOutput/docsGenOutput.json b/public/docs/docsGenOutput/docsGenOutput.json index 506dcc909b..85db282151 100644 --- a/public/docs/docsGenOutput/docsGenOutput.json +++ b/public/docs/docsGenOutput/docsGenOutput.json @@ -51371,7 +51371,7 @@ "editor": { "type": "bool" }, - "from": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps", + "from": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps", "required": false }, { @@ -51386,7 +51386,7 @@ "raw": "string | React.ComponentClass & { children: React.ReactNode; }, any> | React.FunctionComponent & { children: React.ReactNode; }>", "html": "string | React.ComponentClass<Record<string, any> & { children: React.ReactNode; }, any> | React.FunctionComponent<Record<string, any> & { children: React.ReactNode; }>" }, - "from": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps", + "from": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps", "required": false }, { @@ -51401,7 +51401,7 @@ "raw": "(HTMLElement | React.RefObject)[]", "html": "(HTMLElement | React.RefObject<any>)[]" }, - "from": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps", + "from": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps", "required": false }, { @@ -51421,7 +51421,7 @@ "raw": "boolean | FocusOptions | (returnTo: Element) => boolean | FocusOptions", "html": "boolean | FocusOptions | (returnTo: Element) => boolean | FocusOptions" }, - "from": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps", + "from": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps", "required": false } ], @@ -78218,7 +78218,7 @@ "editor": { "type": "bool" }, - "from": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps", + "from": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps", "required": false }, { @@ -78233,7 +78233,7 @@ "raw": "string | React.ComponentClass & { children: React.ReactNode; }, any> | React.FunctionComponent & { children: React.ReactNode; }>", "html": "string | React.ComponentClass<Record<string, any> & { children: React.ReactNode; }, any> | React.FunctionComponent<Record<string, any> & { children: React.ReactNode; }>" }, - "from": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps", + "from": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps", "required": false }, { @@ -78248,7 +78248,7 @@ "raw": "(HTMLElement | React.RefObject)[]", "html": "(HTMLElement | React.RefObject<any>)[]" }, - "from": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps", + "from": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps", "required": false }, { @@ -78268,7 +78268,7 @@ "raw": "boolean | FocusOptions | (returnTo: Element) => boolean | FocusOptions", "html": "boolean | FocusOptions | (returnTo: Element) => boolean | FocusOptions" }, - "from": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps", + "from": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps", "required": false }, { @@ -81954,7 +81954,7 @@ "raw": "(HTMLElement | React.RefObject)[]", "html": "(HTMLElement | React.RefObject<any>)[]" }, - "from": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps", + "from": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps", "required": false } ], @@ -121330,14 +121330,14 @@ "exported": false } }, - "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts:ReactFocusLockProps": { + "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts:ReactFocusLockProps": { "summary": { - "module": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts", + "module": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts", "typeName": { "name": "ReactFocusLockProps", "nameFull": "ReactFocusLockProps" }, - "src": "node_modules/react-focus-lock/dist/cjs/interfaces.d.ts", + "src": "node_modules/@epam/uui-react-focus-lock-fork/dist/cjs/interfaces.d.ts", "exported": false } }, diff --git a/test-utils/src/jsdom/setupJsDom.js b/test-utils/src/jsdom/setupJsDom.js index c2e26fdd7f..f2a044b360 100644 --- a/test-utils/src/jsdom/setupJsDom.js +++ b/test-utils/src/jsdom/setupJsDom.js @@ -90,8 +90,8 @@ function enableMockForCommon3rdPartyDeps() { }; }); - testRunner.mock('react-focus-lock', () => ({ - ...testRunner.requireActual('react-focus-lock'), + testRunner.mock('@epam/uui-react-focus-lock-fork', () => ({ + ...testRunner.requireActual('@epam/uui-react-focus-lock-fork'), __esModule: true, /** * @param {object} props - Component's props diff --git a/uui-components/package.json b/uui-components/package.json index 4de0fd378d..40b87eaa30 100644 --- a/uui-components/package.json +++ b/uui-components/package.json @@ -20,7 +20,7 @@ "classnames": "2.2.6", "dayjs": "1.11.12", "react-fast-compare": "^3.2.2", - "react-focus-lock": "2.13.5", + "@epam/uui-react-focus-lock-fork": "2.13.8", "react-transition-group": "4.4.5" }, "peerDependencies": { diff --git a/uui-components/src/navigation/MainMenu/Burger/Burger.tsx b/uui-components/src/navigation/MainMenu/Burger/Burger.tsx index bc6637b205..c6ad5901d8 100644 --- a/uui-components/src/navigation/MainMenu/Burger/Burger.tsx +++ b/uui-components/src/navigation/MainMenu/Burger/Burger.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import cx from 'classnames'; -import FocusLock from 'react-focus-lock'; +import FocusLock from '@epam/uui-react-focus-lock-fork'; import { IHasCX, Icon, IHasRawProps, IHasForwardedRef, } from '@epam/uui-core'; diff --git a/uui-components/src/overlays/Dropdown.tsx b/uui-components/src/overlays/Dropdown.tsx index 67a200882b..7dc0ac9556 100644 --- a/uui-components/src/overlays/Dropdown.tsx +++ b/uui-components/src/overlays/Dropdown.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useContext, us import { useFloating, autoUpdate, flip, shift, useMergeRefs, hide, arrow, useDismiss, } from '@floating-ui/react'; -import { FreeFocusInside } from 'react-focus-lock'; +import { FreeFocusInside } from '@epam/uui-react-focus-lock-fork'; import { isEventTargetInsideClickable, UuiContext } from '@epam/uui-core'; import type { LayoutLayer, DropdownProps } from '@epam/uui-core'; import { getFallbackPlacements } from '../helpers'; diff --git a/uui-components/src/overlays/DropdownContainer.tsx b/uui-components/src/overlays/DropdownContainer.tsx index 58df6fee4a..3710c7c3ce 100644 --- a/uui-components/src/overlays/DropdownContainer.tsx +++ b/uui-components/src/overlays/DropdownContainer.tsx @@ -1,13 +1,11 @@ import * as React from 'react'; -import FocusLock from 'react-focus-lock'; +import FocusLock, { ReactFocusLockProps } from '@epam/uui-react-focus-lock-fork'; import { uuiElement, IHasCX, IHasChildren, cx, IHasRawProps, uuiMarkers, IHasForwardedRef, IDropdownBodyProps, IHasStyleAttrs, } from '@epam/uui-core'; import { VPanel } from '../layout/flexItems/VPanel'; import PopoverArrow from './PopoverArrow'; -import { ReactFocusLockProps } from 'react-focus-lock'; -import { useCallback, useState } from 'react'; export interface DropdownContainerProps extends IHasCX, @@ -57,18 +55,11 @@ export const DropdownContainer = React.forwardRef((props: DropdownContainerProps returnFocus = true, persistentFocus = true, } = props; - const [shadowRootHost, setShadowRootHost] = useState(null); - - const saveShadowRootHost = useCallback((node: HTMLElement | null) => { - if (!node) return; - const root = node.getRootNode(); - setShadowRootHost(root instanceof ShadowRoot ? root : null); - }, []); function renderDropdownContainer() { return ( ) : saveShadowRootHost } + forwardedRef={ !focusLock ? (ref as React.ForwardedRef) : undefined } cx={ cx(uuiElement.dropdownBody, uuiMarkers.lockFocus, props.cx) } style={ { ...props.style, @@ -101,7 +92,6 @@ export const DropdownContainer = React.forwardRef((props: DropdownContainerProps shards={ props.shards } autoFocus={ props.autoFocus ?? true } as={ props.as } - shadowRootHost={ shadowRootHost } > {renderDropdownContainer()} diff --git a/uui-components/src/overlays/ModalBlocker.tsx b/uui-components/src/overlays/ModalBlocker.tsx index 45ab1dd6f1..be9ccd3d0b 100644 --- a/uui-components/src/overlays/ModalBlocker.tsx +++ b/uui-components/src/overlays/ModalBlocker.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect } from 'react'; -import FocusLock from 'react-focus-lock'; +import FocusLock from '@epam/uui-react-focus-lock-fork'; import css from './ModalBlocker.module.scss'; import { ModalBlockerProps, UuiContext, cx, uuiElement } from '@epam/uui-core'; diff --git a/uui/components/navigation/MainMenu/MainMenuDropdown.tsx b/uui/components/navigation/MainMenu/MainMenuDropdown.tsx index 91075d70d5..f082370d27 100644 --- a/uui/components/navigation/MainMenu/MainMenuDropdown.tsx +++ b/uui/components/navigation/MainMenu/MainMenuDropdown.tsx @@ -1,5 +1,5 @@ import React, { KeyboardEvent } from 'react'; -import FocusLock from 'react-focus-lock'; +import FocusLock from '@epam/uui-react-focus-lock-fork'; import cx from 'classnames'; import { Dropdown, MainMenuDropdownProps } from '@epam/uui-components'; import { MainMenuButton } from './MainMenuButton'; diff --git a/uui/components/navigation/MainMenu/__tests__/__snapshots__/MainMenuDropdown.test.tsx.snap b/uui/components/navigation/MainMenu/__tests__/__snapshots__/MainMenuDropdown.test.tsx.snap index 1fccc9fda0..4a85a2a090 100644 --- a/uui/components/navigation/MainMenu/__tests__/__snapshots__/MainMenuDropdown.test.tsx.snap +++ b/uui/components/navigation/MainMenu/__tests__/__snapshots__/MainMenuDropdown.test.tsx.snap @@ -32,54 +32,40 @@ exports[`MainMenuDropdown should be rendered correctly in opened state 1`] = ` style="position: fixed; top: 0px; left: 0px; z-index: 2000;" >
-