diff --git a/public/chat_header_button.test.tsx b/public/chat_header_button.test.tsx
index f6ebae40..a26f5fbd 100644
--- a/public/chat_header_button.test.tsx
+++ b/public/chat_header_button.test.tsx
@@ -74,6 +74,9 @@ jest.spyOn(coreContextExports, 'useCore').mockReturnValue({
element.style.display = 'block';
}
},
+ getSidecarConfig$: () => {
+ return new BehaviorSubject(undefined);
+ },
};
},
},
diff --git a/public/chat_header_button.tsx b/public/chat_header_button.tsx
index 18e610bb..773363f4 100644
--- a/public/chat_header_button.tsx
+++ b/public/chat_header_button.tsx
@@ -25,6 +25,7 @@ import {
} from './utils/constants';
import { useCore } from './contexts/core_context';
import { MountPointPortal } from '../../../src/plugins/opensearch_dashboards_react/public';
+import { usePatchFixedStyle } from './hooks/use_patch_fixed_style';
interface HeaderChatButtonProps {
application: ApplicationStart;
@@ -53,6 +54,7 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => {
const core = useCore();
const flyoutFullScreen = sidecarDockedMode === SIDECAR_DOCKED_MODE.TAKEOVER;
const flyoutMountPoint = useRef(null);
+ usePatchFixedStyle();
useEffectOnce(() => {
const subscription = props.application.currentAppId$.subscribe((id) => setAppId(id));
diff --git a/public/hooks/__snapshots__/use_patch_fixed_style.test.ts.snap b/public/hooks/__snapshots__/use_patch_fixed_style.test.ts.snap
new file mode 100644
index 00000000..63cbfe94
--- /dev/null
+++ b/public/hooks/__snapshots__/use_patch_fixed_style.test.ts.snap
@@ -0,0 +1,175 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`usePatchFixedStyle hook should not subscribe update after unmount 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`usePatchFixedStyle hook should not subscribe update after unmount 2`] = `
+
+
+
+
+
+`;
+
+exports[`usePatchFixedStyle hook should patch corresponding left style when sidecarConfig$ pipe 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`usePatchFixedStyle hook should patch corresponding right style when sidecarConfig$ pipe 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`usePatchFixedStyle hook should patch empty style when dockedMode of sidecarConfig$ is takeover 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`usePatchFixedStyle hook should patch empty style when isHidden of sidecarConfig$ is true 1`] = `
+
+
+
+
+
+
+`;
diff --git a/public/hooks/use_patch_fixed_style.test.ts b/public/hooks/use_patch_fixed_style.test.ts
new file mode 100644
index 00000000..ff275505
--- /dev/null
+++ b/public/hooks/use_patch_fixed_style.test.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { renderHook, act } from '@testing-library/react-hooks';
+
+import { usePatchFixedStyle } from './use_patch_fixed_style';
+import * as coreHookExports from '../contexts/core_context';
+import { BehaviorSubject } from 'rxjs';
+import { ISidecarConfig, SIDECAR_DOCKED_MODE } from '../../../../src/core/public';
+
+describe('usePatchFixedStyle hook', () => {
+ const sidecarConfig$ = new BehaviorSubject({
+ dockedMode: SIDECAR_DOCKED_MODE.RIGHT,
+ paddingSize: 300,
+ });
+
+ beforeEach(() => {
+ jest.spyOn(coreHookExports, 'useCore').mockReturnValue({
+ overlays: {
+ // @ts-ignore
+ sidecar: () => {
+ return {
+ getSidecarConfig$: () => {
+ return sidecarConfig$;
+ },
+ };
+ },
+ },
+ });
+ jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ window.requestAnimationFrame.mockRestore();
+ });
+
+ it('should patch corresponding left style when sidecarConfig$ pipe', async () => {
+ renderHook(() => usePatchFixedStyle());
+ act(() =>
+ sidecarConfig$.next({
+ dockedMode: SIDECAR_DOCKED_MODE.LEFT,
+ paddingSize: 300,
+ })
+ );
+
+ expect(document.head).toMatchSnapshot();
+ });
+
+ it('should patch corresponding right style when sidecarConfig$ pipe', async () => {
+ renderHook(() => usePatchFixedStyle());
+ act(() =>
+ sidecarConfig$.next({
+ dockedMode: SIDECAR_DOCKED_MODE.RIGHT,
+ paddingSize: 300,
+ })
+ );
+
+ expect(document.head).toMatchSnapshot();
+ });
+
+ it('should patch empty style when isHidden of sidecarConfig$ is true', async () => {
+ renderHook(() => usePatchFixedStyle());
+ act(() =>
+ sidecarConfig$.next({
+ dockedMode: SIDECAR_DOCKED_MODE.LEFT,
+ paddingSize: 300,
+ isHidden: true,
+ })
+ );
+
+ expect(document.head).toMatchSnapshot();
+ });
+
+ it('should patch empty style when dockedMode of sidecarConfig$ is takeover', async () => {
+ renderHook(() => usePatchFixedStyle());
+ act(() =>
+ sidecarConfig$.next({
+ dockedMode: SIDECAR_DOCKED_MODE.TAKEOVER,
+ paddingSize: 300,
+ })
+ );
+
+ expect(document.head).toMatchSnapshot();
+ });
+
+ it('should not subscribe update after unmount', async () => {
+ const { unmount } = renderHook(() => usePatchFixedStyle());
+ act(() =>
+ sidecarConfig$.next({
+ dockedMode: SIDECAR_DOCKED_MODE.RIGHT,
+ paddingSize: 300,
+ })
+ );
+ expect(document.head).toMatchSnapshot();
+
+ unmount();
+
+ act(() =>
+ sidecarConfig$.next({
+ dockedMode: SIDECAR_DOCKED_MODE.LEFT,
+ paddingSize: 500,
+ })
+ );
+ expect(document.head).toMatchSnapshot();
+ });
+});
diff --git a/public/hooks/use_patch_fixed_style.ts b/public/hooks/use_patch_fixed_style.ts
new file mode 100644
index 00000000..ea9930e5
--- /dev/null
+++ b/public/hooks/use_patch_fixed_style.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { useEffect } from 'react';
+
+import { useCore } from '../contexts/core_context';
+import { ISidecarConfig, SIDECAR_DOCKED_MODE } from '../../../../src/core/public';
+
+// There are some UI components from library whose position is fixed and are not compatible with each other and also not compatible with sidecar container.
+// There is currently no way to provide config for these components at runtime.
+// This hook patches a style for all these already known components to make them compatible with sidecar.
+// TODO: Use config provider from UI library to make this more reasonable.
+export const usePatchFixedStyle = () => {
+ const core = useCore();
+ const sidecarConfig$ = core.overlays.sidecar().getSidecarConfig$();
+
+ useEffect(() => {
+ const css = '';
+ const style = document.createElement('style');
+ const text = document.createTextNode(css);
+ document.head.appendChild(style);
+ style.appendChild(text);
+
+ const subscription = sidecarConfig$.subscribe((config) => {
+ if (!config) return;
+ updateHeadStyle(config, text);
+ });
+ return () => {
+ subscription.unsubscribe();
+ document.head.removeChild(style);
+ style.removeChild(text);
+ };
+ }, [sidecarConfig$]);
+};
+
+function updateHeadStyle(config: ISidecarConfig, text?: Text) {
+ let css: string;
+ if (!text) return;
+
+ if (
+ // When sidecar is opened and docked position is left or right, we should patch style.
+ config?.isHidden !== true &&
+ config.paddingSize &&
+ (config.dockedMode === SIDECAR_DOCKED_MODE.LEFT ||
+ config.dockedMode === SIDECAR_DOCKED_MODE.RIGHT)
+ ) {
+ const { dockedMode, paddingSize } = config;
+ if (dockedMode === SIDECAR_DOCKED_MODE.RIGHT) {
+ // Current applied components include flyout and bottomBar.
+ // Although the class names of actual rendered component start with eui. We will also apply oui for fallback.
+ css = `
+ .euiFlyout:not(.euiFlyout--left) {
+ right: ${paddingSize}px
+ }
+ .ouiFlyout:not(.ouiFlyout--left) {
+ right: ${paddingSize}px
+ }
+ .euiBottomBar{
+ padding-right: ${paddingSize}px
+ }
+ .ouiBottomBar{
+ padding-right: ${paddingSize}px
+ }
+ `;
+ } else if (dockedMode === SIDECAR_DOCKED_MODE.LEFT) {
+ css = `
+ .euiFlyout--left {
+ left: ${paddingSize}px
+ }
+ .ouiFlyout--left {
+ left: ${paddingSize}px
+ }
+ .euiBottomBar{
+ padding-left: ${paddingSize}px
+ }
+ .ouiBottomBar{
+ padding-left: ${paddingSize}px
+ }
+ `;
+ }
+ } else {
+ // If sidecar closes/hides or docked mode changes to takeover, we will set css empty to remove patch style and update.
+ css = '';
+ }
+
+ requestAnimationFrame(() => {
+ text.textContent = css;
+ });
+}