diff --git a/src/core/packages/chrome/layout/core-chrome-layout-components/header/layout_header.tsx b/src/core/packages/chrome/layout/core-chrome-layout-components/header/layout_header.tsx
index 20172d40e8acb..b9871184d03e6 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-components/header/layout_header.tsx
+++ b/src/core/packages/chrome/layout/core-chrome-layout-components/header/layout_header.tsx
@@ -8,6 +8,7 @@
*/
import React, { type ReactNode } from 'react';
+import { euiIncludeSelectorInFocusTrap } from '@kbn/core-chrome-layout-constants';
import { styles } from './layout_header.styles';
export interface LayoutHeaderProps {
@@ -22,7 +23,12 @@ export interface LayoutHeaderProps {
*/
export const LayoutHeader = ({ children }: LayoutHeaderProps) => {
return (
-
+
{children}
);
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-components/navigation/layout_navigation.tsx b/src/core/packages/chrome/layout/core-chrome-layout-components/navigation/layout_navigation.tsx
index 7ed2fb218403c..5a9429c12fe49 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-components/navigation/layout_navigation.tsx
+++ b/src/core/packages/chrome/layout/core-chrome-layout-components/navigation/layout_navigation.tsx
@@ -8,6 +8,7 @@
*/
import React, { type ReactNode } from 'react';
+import { euiIncludeSelectorInFocusTrap } from '@kbn/core-chrome-layout-constants';
import { styles } from './layout_navigation.styles';
export interface LayoutNavigationProps {
@@ -26,6 +27,7 @@ export const LayoutNavigation = ({ children }: LayoutNavigationProps) => {
css={styles.root}
className="kbnChromeLayoutNavigation"
data-test-subj="kbnChromeLayoutNavigation"
+ {...euiIncludeSelectorInFocusTrap.prop}
>
{children}
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-constants/index.ts b/src/core/packages/chrome/layout/core-chrome-layout-constants/index.ts
index a8a57c0bce7a3..503bceaa4bae0 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-constants/index.ts
+++ b/src/core/packages/chrome/layout/core-chrome-layout-constants/index.ts
@@ -30,3 +30,14 @@ export const MAIN_CONTENT_SELECTORS = [
'[role="main"]', // Fallback for plugins using deprecated EuiPageContent
'.kbnAppWrapper', // Last-ditch fallback for all plugins regardless of page template
];
+
+/**
+ * The selector for elements that should be included in the focus trap of a flyout.
+ * This will allow the flyout focus trap to include header and sidenav by default.
+ */
+export const euiIncludeSelectorInFocusTrap = {
+ prop: {
+ 'data-eui-includes-in-flyout-focus-trap': true,
+ },
+ selector: `[data-eui-includes-in-flyout-focus-trap="true"]`,
+};
diff --git a/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_global_app_style.tsx b/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_global_app_style.tsx
index 1b0ec07b85da2..b06406c3abaec 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_global_app_style.tsx
+++ b/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_global_app_style.tsx
@@ -15,11 +15,6 @@ import {
APP_MAIN_SCROLL_CONTAINER_ID,
} from '@kbn/core-chrome-layout-constants';
import { CommonGlobalAppStyles } from '../common/global_app_styles';
-import {
- useHackSyncPushFlyout,
- hackEuiPushFlyoutPaddingInlineEnd,
- hackEuiPushFlyoutPaddingInlineStart,
-} from './hack_use_sync_push_flyout';
const globalLayoutStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
:root {
@@ -107,8 +102,8 @@ const globalTempHackStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
// push flyout should be pushing the application area, instead of body
#${APP_MAIN_SCROLL_CONTAINER_ID} {
- ${logicalCSS('padding-right', `var(${hackEuiPushFlyoutPaddingInlineEnd}, 0px)`)};
- ${logicalCSS('padding-left', `var(${hackEuiPushFlyoutPaddingInlineStart}, 0px)`)};
+ ${logicalCSS('padding-right', `var(--euiPushFlyoutOffsetInlineEnd, 0px)`)};
+ ${logicalCSS('padding-left', `var(--euiPushFlyoutOffsetInlineStart, 0px)`)};
}
// this is a temporary hack to override EUI's body padding with push flyout
.kbnBody {
@@ -119,7 +114,6 @@ const globalTempHackStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
export const GridLayoutGlobalStyles = () => {
const { euiTheme } = useEuiTheme();
- useHackSyncPushFlyout();
return (
<>
diff --git a/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/hack_use_sync_push_flyout.tsx b/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/hack_use_sync_push_flyout.tsx
deleted file mode 100644
index 972992644b45f..0000000000000
--- a/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/hack_use_sync_push_flyout.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { useEffect } from 'react';
-import { useEuiThemeCSSVariables } from '@elastic/eui';
-
-export const hackEuiPushFlyoutPaddingInlineEnd = '--eui-push-flyout-padding-inline-end';
-export const hackEuiPushFlyoutPaddingInlineStart = '--eui-push-flyout-padding-inline-start';
-
-/**
- * This is definitely a hack for experimental purposes.
- * Currently, EUI push flyouts visually push the content to the right by adding a padding to the body
- * This hook listens to styles changes on the body and updates a CSS variable that is used to push the workspace content
- * https://github.com/elastic/eui/issues/8820
- */
-export function useHackSyncPushFlyout() {
- const { setGlobalCSSVariables } = useEuiThemeCSSVariables();
-
- useEffect(() => {
- const targetNode = document.body;
-
- const callback: MutationCallback = (mutationsList: MutationRecord[]) => {
- for (const mutation of mutationsList) {
- if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
- // const newPaddingInlineStart = window.getComputedStyle(targetNode).paddingInlineStart;
- const styleAttribute = targetNode.getAttribute('style') ?? '';
-
- function parseCSSDeclaration(styleAttr: string) {
- return styleAttr
- .trim()
- .split(';')
- .filter((s) => s !== '')
- .map((declaration) => {
- const colonIndex = declaration.indexOf(':');
- if (colonIndex === -1) {
- throw new Error('Invalid CSS declaration: no colon found.');
- }
-
- const property = declaration.slice(0, colonIndex).trim();
- const value = declaration.slice(colonIndex + 1).trim();
-
- return { property, value };
- });
- }
-
- const parsedStyle = parseCSSDeclaration(styleAttribute);
- const paddingInline = parsedStyle.find(
- (style) => style.property === 'padding-inline'
- )?.value;
- let paddingInlineStart = parsedStyle.find(
- (style) => style.property === 'padding-inline-start'
- )?.value;
- let paddingInlineEnd = parsedStyle.find(
- (style) => style.property === 'padding-inline-end'
- )?.value;
-
- const [start, end] = paddingInline?.split(' ') ?? ['', ''];
-
- paddingInlineStart = paddingInlineStart ?? start;
- paddingInlineEnd = paddingInlineEnd ?? end;
-
- if (paddingInlineStart) {
- setGlobalCSSVariables({
- [hackEuiPushFlyoutPaddingInlineStart]: paddingInlineStart,
- });
- } else {
- setGlobalCSSVariables({
- [hackEuiPushFlyoutPaddingInlineStart]: null,
- });
- }
-
- if (paddingInlineEnd) {
- setGlobalCSSVariables({
- [hackEuiPushFlyoutPaddingInlineEnd]: paddingInlineEnd,
- });
- } else {
- setGlobalCSSVariables({
- [hackEuiPushFlyoutPaddingInlineEnd]: null,
- });
- }
- }
- }
- };
-
- const observer = new MutationObserver(callback);
-
- observer.observe(targetNode, { attributes: true, attributeFilter: ['style'] });
-
- return () => {
- observer.disconnect();
- };
- }, [setGlobalCSSVariables]);
-}
diff --git a/src/platform/packages/shared/react/kibana_context/root/eui_provider.test.tsx b/src/platform/packages/shared/react/kibana_context/root/eui_provider.test.tsx
index 11d83f0affc41..774f86d8ed9bf 100644
--- a/src/platform/packages/shared/react/kibana_context/root/eui_provider.test.tsx
+++ b/src/platform/packages/shared/react/kibana_context/root/eui_provider.test.tsx
@@ -7,20 +7,29 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import type { ReactWrapper } from 'enzyme';
import type { FC } from 'react';
import React, { useEffect } from 'react';
import { act } from 'react-dom/test-utils';
+import { render, waitFor } from '@testing-library/react';
import { BehaviorSubject, of } from 'rxjs';
-import { useEuiTheme } from '@elastic/eui';
+import { useEuiTheme, EuiProvider } from '@elastic/eui';
import type { UserProfileService } from '@kbn/core-user-profile-browser';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import type { KibanaTheme } from '@kbn/react-kibana-context-common';
-import { mountWithIntl } from '@kbn/test-jest-helpers';
+import { euiIncludeSelectorInFocusTrap } from '@kbn/core-chrome-layout-constants';
import { KibanaEuiProvider } from './eui_provider';
+// Mock the EuiProvider component to capture its props
+jest.mock('@elastic/eui', () => {
+ const original = jest.requireActual('@elastic/eui');
+ return {
+ ...original,
+ EuiProvider: jest.fn(original.EuiProvider),
+ };
+});
+
describe('KibanaEuiProvider', () => {
let euiTheme: ReturnType | undefined;
let userProfile: UserProfileService;
@@ -30,18 +39,9 @@ describe('KibanaEuiProvider', () => {
euiTheme = undefined;
userProfile = userProfileServiceMock.createStart();
consoleWarnMock = jest.spyOn(global.console, 'warn').mockImplementation(() => {});
+ (EuiProvider as jest.Mock).mockImplementation(jest.requireActual('@elastic/eui').EuiProvider);
});
- const flushPromises = async () => {
- await new Promise(async (resolve, reject) => {
- try {
- setImmediate(() => resolve());
- } catch (error) {
- reject(error);
- }
- });
- };
-
const InnerComponent: FC = () => {
const theme = useEuiTheme();
useEffect(() => {
@@ -50,17 +50,10 @@ describe('KibanaEuiProvider', () => {
return foo
;
};
- const refresh = async (wrapper: ReactWrapper) => {
- await act(async () => {
- await flushPromises();
- wrapper.update();
- });
- };
-
it('exposes the EUI theme provider', async () => {
const coreTheme: KibanaTheme = { darkMode: true, name: 'amsterdam' };
- const wrapper = mountWithIntl(
+ render(
{
);
- await refresh(wrapper);
+ // Wait for the component to update
+ await waitFor(() => {
+ expect(euiTheme).toBeDefined();
+ });
expect(euiTheme!.colorMode).toEqual('DARK');
expect(euiTheme!.euiTheme.breakpoint.xxl).toEqual(1600);
@@ -80,23 +76,50 @@ describe('KibanaEuiProvider', () => {
it('propagates changes of the coreTheme observable', async () => {
const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' });
- const wrapper = mountWithIntl(
+ render(
);
- await refresh(wrapper);
+ // Wait for the component to update with initial theme
+ await waitFor(() => {
+ expect(euiTheme).toBeDefined();
+ });
expect(euiTheme!.colorMode).toEqual('DARK');
- await act(async () => {
+ // Update the theme
+ act(() => {
coreTheme$.next({ darkMode: false, name: 'amsterdam' });
});
- await refresh(wrapper);
+ // Wait for the component to update with new theme
+ await waitFor(() => {
+ expect(euiTheme!.colorMode).toEqual('LIGHT');
+ });
- expect(euiTheme!.colorMode).toEqual('LIGHT');
expect(consoleWarnMock).not.toBeCalled();
});
+
+ it('passes component defaults to EuiProvider', async () => {
+ const coreTheme: KibanaTheme = { darkMode: true, name: 'amsterdam' };
+
+ render(
+
+ test
+
+ );
+
+ expect(EuiProvider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ componentDefaults: {
+ EuiFlyout: {
+ includeSelectorInFocusTrap: euiIncludeSelectorInFocusTrap.selector,
+ },
+ },
+ }),
+ expect.anything()
+ );
+ });
});
diff --git a/src/platform/packages/shared/react/kibana_context/root/eui_provider.tsx b/src/platform/packages/shared/react/kibana_context/root/eui_provider.tsx
index 014453389581d..3b2e0e7d2df46 100644
--- a/src/platform/packages/shared/react/kibana_context/root/eui_provider.tsx
+++ b/src/platform/packages/shared/react/kibana_context/root/eui_provider.tsx
@@ -12,6 +12,10 @@ import React, { FC, PropsWithChildren, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import createCache from '@emotion/cache';
+// We can't use the import directly because the package isn't included in the shared bundle, so below the value is hardcoded.
+// However, we import this directly in the test to ensure our hardcoded selector is correct.
+// import { euiIncludeSelectorInFocusTrap } from '@kbn/core-chrome-layout-constants';
+
import { EuiProvider, EuiProviderProps, euiStylisPrefixer } from '@elastic/eui';
import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common';
import {
@@ -73,6 +77,12 @@ utilitiesCache.compat = true;
const cache = { default: emotionCache, global: globalCache, utility: utilitiesCache };
+const componentDefaults: EuiProviderProps['componentDefaults'] = {
+ EuiFlyout: {
+ includeSelectorInFocusTrap: `[data-eui-includes-in-flyout-focus-trap="true"]`,
+ },
+};
+
/**
* Prepares and returns a configured `EuiProvider` for use in Kibana roots. In most cases, this utility context
* should not be used. Instead, refer to `KibanaRootContextProvider` to set up the root of Kibana.
@@ -130,6 +140,7 @@ export const KibanaEuiProvider: FC> =
utilityClasses: globalStyles,
highContrastMode,
theme: _theme,
+ componentDefaults,
}}
>
{children}
diff --git a/src/platform/packages/shared/react/kibana_context/root/root_provider.test.tsx b/src/platform/packages/shared/react/kibana_context/root/root_provider.test.tsx
index 52af9d5654216..922640626c894 100644
--- a/src/platform/packages/shared/react/kibana_context/root/root_provider.test.tsx
+++ b/src/platform/packages/shared/react/kibana_context/root/root_provider.test.tsx
@@ -9,11 +9,10 @@
import React, { FC, useEffect } from 'react';
import { act } from 'react-dom/test-utils';
-import type { ReactWrapper } from 'enzyme';
+import { render, waitFor } from '@testing-library/react';
import { of, BehaviorSubject } from 'rxjs';
import { useEuiTheme } from '@elastic/eui';
import type { UseEuiTheme } from '@elastic/eui';
-import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { KibanaTheme } from '@kbn/react-kibana-context-common';
import type { ExecutionContextStart } from '@kbn/core-execution-context-browser';
import { executionContextServiceMock } from '@kbn/core-execution-context-browser-mocks';
@@ -36,16 +35,6 @@ describe('KibanaRootContextProvider', () => {
executionContext = executionContextServiceMock.createStartContract();
});
- const flushPromises = async () => {
- await new Promise(async (resolve, reject) => {
- try {
- setImmediate(() => resolve());
- } catch (error) {
- reject(error);
- }
- });
- };
-
const InnerComponent: FC = () => {
const theme = useEuiTheme();
useEffect(() => {
@@ -54,17 +43,10 @@ describe('KibanaRootContextProvider', () => {
return foo
;
};
- const refresh = async (wrapper: ReactWrapper) => {
- await act(async () => {
- await flushPromises();
- wrapper.update();
- });
- };
-
it('exposes the EUI theme provider', async () => {
const coreTheme: KibanaTheme = { darkMode: true, name: 'amsterdam' };
- const wrapper = mountWithIntl(
+ render(
{
);
- await refresh(wrapper);
+ await waitFor(() => {
+ expect(euiTheme).toBeDefined();
+ });
expect(euiTheme!.colorMode).toEqual('DARK');
});
@@ -83,7 +67,7 @@ describe('KibanaRootContextProvider', () => {
it('propagates changes of the coreTheme observable', async () => {
const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' });
- const wrapper = mountWithIntl(
+ render(
{
);
- await refresh(wrapper);
+ // Wait for initial render with dark theme
+ await waitFor(() => {
+ expect(euiTheme).toBeDefined();
+ });
expect(euiTheme!.colorMode).toEqual('DARK');
- await act(async () => {
+ // Update theme to light mode
+ act(() => {
coreTheme$.next({ darkMode: false, name: 'amsterdam' });
});
- await refresh(wrapper);
-
- expect(euiTheme!.colorMode).toEqual('LIGHT');
+ // Wait for the theme to update
+ await waitFor(() => {
+ expect(euiTheme!.colorMode).toEqual('LIGHT');
+ });
});
});
diff --git a/src/platform/packages/shared/react/kibana_context/root/tsconfig.json b/src/platform/packages/shared/react/kibana_context/root/tsconfig.json
index b934a5be91352..28b16b5ba6eb5 100644
--- a/src/platform/packages/shared/react/kibana_context/root/tsconfig.json
+++ b/src/platform/packages/shared/react/kibana_context/root/tsconfig.json
@@ -16,7 +16,6 @@
"target/**/*"
],
"kbn_references": [
- "@kbn/test-jest-helpers",
"@kbn/react-kibana-context-common",
"@kbn/core-i18n-browser-mocks",
"@kbn/core-i18n-browser",
@@ -26,6 +25,7 @@
"@kbn/core-user-profile-browser-mocks",
"@kbn/core-execution-context-browser",
"@kbn/core-execution-context-browser-mocks",
- "@kbn/shared-ux-router"
+ "@kbn/shared-ux-router",
+ "@kbn/core-chrome-layout-constants"
]
}