Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EuiWindowProvider for multi-window support #7782

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c303002
Add EuiWindowProvider for EuiPortal and EuiWindowEvent
stil May 21, 2024
77f5704
Formatting
stil May 21, 2024
7d1d391
Formatting
stil May 21, 2024
cf06fb6
Add Flyout, FocusTrap, OutsideClickDetector, findElement support for …
stil May 22, 2024
4fb655c
Formatting
stil May 22, 2024
26433d0
Fix effect
stil May 22, 2024
0bf4e8e
Fix effects
stil May 22, 2024
c0b29cf
Declare context
stil May 22, 2024
96f438a
Use context declaration
stil May 22, 2024
6555c1a
Delete unused import
stil May 22, 2024
cfbedf2
Make default window object resolved at runtime to fix tests
stil May 22, 2024
a46fea6
Fix for SSR scenario
stil May 22, 2024
99ed11c
Fix problems in Flyout
stil May 22, 2024
2d39642
Update i18ntokens.json
stil May 22, 2024
4ce72c6
Merge remote-tracking branch 'upstream/main' into add-window-provider…
stil Aug 20, 2024
a9a2388
Add build-publish for yalc
stil Sep 5, 2024
6ec4cf0
Merge remote-tracking branch 'upstream/main' into add-window-provider…
stil Sep 5, 2024
d90a8b5
Make tooltip component use window provider
stil Sep 19, 2024
8fcf539
Missed one window ref
stil Sep 19, 2024
adf4ca7
Make popover component use window provider
stil Sep 20, 2024
3f5a871
Use current window for Popover and Portal
stil Sep 20, 2024
11b93e4
Apply formatting
stil Sep 21, 2024
cdffdc8
Apply formatting
stil Sep 21, 2024
0d48da6
Fix import
stil Sep 21, 2024
9e7faeb
Fix hooks
stil Sep 21, 2024
af610db
Fix EuiOverlayMask
stil Sep 23, 2024
69f3c93
Merge remote-tracking branch 'upstream/main' into add-window-provider
stil Sep 24, 2024
41e633c
Fix tests
stil Sep 24, 2024
3b73f6c
Remove custom publish script
stil Sep 24, 2024
ba42379
Merge branch 'main' into add-window-provider
stil Sep 24, 2024
b27eb73
Merge remote-tracking branch 'upstream/main' into add-window-provider
stil Oct 5, 2024
b3d6990
Add EuiWindowProvider documentation page
stil Oct 6, 2024
b703c40
Add changelog entry
stil Oct 6, 2024
adae23d
Add props descriptions
stil Oct 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/eui/changelogs/upcoming/7782.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- Added `EuiWindowProvider` that fixes interactivity of some components in scenarios where rendering is done via React Portals into another browser window or iframe. ([#7782](https://github.com/elastic/eui/pull/7782))

**Dependency updates**

- Updated `react-focus-on` to vX.Y.Z (TODO)
24 changes: 12 additions & 12 deletions packages/eui/i18ntokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -4811,14 +4811,14 @@
"highlighting": "string",
"loc": {
"start": {
"line": 336,
"line": 339,
"column": 14,
"index": 10903
"index": 11143
},
"end": {
"line": 339,
"line": 342,
"column": 16,
"index": 11118
"index": 11358
}
},
"filepath": "src/components/flyout/flyout.tsx"
Expand All @@ -4829,14 +4829,14 @@
"highlighting": "string",
"loc": {
"start": {
"line": 341,
"line": 344,
"column": 14,
"index": 11151
"index": 11391
},
"end": {
"line": 344,
"line": 347,
"column": 16,
"index": 11329
"index": 11569
}
},
"filepath": "src/components/flyout/flyout.tsx"
Expand All @@ -4847,14 +4847,14 @@
"highlighting": "string",
"loc": {
"start": {
"line": 347,
"line": 350,
"column": 14,
"index": 11406
"index": 11646
},
"end": {
"line": 350,
"line": 353,
"column": 16,
"index": 11599
"index": 11839
}
},
"filepath": "src/components/flyout/flyout.tsx"
Expand Down
3 changes: 3 additions & 0 deletions packages/eui/src-docs/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ import { TourExample } from './views/tour/tour_example';

import { WindowEventExample } from './views/window_event/window_event_example';

import { WindowProviderExample } from './views/window_provider/window_provider_example';

import { Changelog } from './views/package/changelog';

import { I18nTokens } from './views/package/i18n_tokens';
Expand Down Expand Up @@ -667,6 +669,7 @@ const navigation = [
].map((example) => createExample(example)),
createTabbedPage(TextTruncateExample),
createExample(WindowEventExample),
createExample(WindowProviderExample),
],
},
{
Expand Down
9 changes: 9 additions & 0 deletions packages/eui/src-docs/src/views/window_provider/props.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { FunctionComponent } from 'react';

import { EuiWindowProviderProps } from '../../../../src/services/window_provider/provider';

export const useEuiWindowProviderProps: FunctionComponent<
EuiWindowProviderProps
> = () => {
return <div />;
};
167 changes: 167 additions & 0 deletions packages/eui/src-docs/src/views/window_provider/window_provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import {
EuiButton,
EuiButtonEmpty,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiHorizontalRule,
EuiLink,
EuiPanel,
EuiPopover,
EuiText,
EuiTitle,
EuiToolTip,
} from '../../../../src/components';
import { EuiWindowProvider } from '../../../../src/services';
import createCache, { EmotionCache } from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { WindowProvider } from 'react-style-singleton';

export default () => {
const [childWindow, setChildWindow] = useState<
| {
type: 'open';
window: Window;
emotionCache: EmotionCache;
}
| { type: 'closed' }
>({ type: 'closed' });

const openWindow = () => {
const newWindow = window.open('', '_blank', `width=800,height=600`);

if (!newWindow) {
throw new Error('Could not open the window.');
}

newWindow.onbeforeunload = () => {
setChildWindow({ type: 'closed' });
};

copyStyles(newWindow);

const emotionCache = createCache({
key: 'child-window',
container: newWindow.document.head,
});

setChildWindow({ type: 'open', window: newWindow, emotionCache });
};

return (
<div>
<EuiButton onClick={openWindow}>Open new window</EuiButton>
{childWindow.type === 'open' && (
<EuiWindowProvider window={childWindow.window}>
<WindowProvider window={childWindow.window}>
<CacheProvider value={childWindow.emotionCache}>
<NewWindow windowHandle={childWindow.window}>
<WindowContents />
</NewWindow>
</CacheProvider>
</WindowProvider>
</EuiWindowProvider>
)}
</div>
);
};

const NewWindow = ({
windowHandle,
children,
}: {
windowHandle: Window;
children: React.ReactNode;
}) => {
return createPortal(children, windowHandle.document.body);
};

const WindowContents = () => {
const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);

return (
<>
{isFlyoutOpen ? (
<EuiFlyout
onClose={() => {
setIsFlyoutOpen(false);
}}
paddingSize="m"
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>Flyout header</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>Flyout contents.</EuiText>
</EuiFlyoutBody>
</EuiFlyout>
) : null}

<EuiPanel hasBorder style={{ margin: '1rem' }}>
<EuiText>
<h2>Interactivity example</h2>
</EuiText>

<EuiHorizontalRule />

<EuiButton
onClick={() => {
setIsFlyoutOpen(true);
}}
>
Open flyout
</EuiButton>

<EuiHorizontalRule />

<EuiPopover
button={
<EuiButtonEmpty
iconType="documentation"
iconSide="right"
onClick={() => {
setIsPopoverOpen(true);
}}
>
Popover example (click to open)
</EuiButtonEmpty>
}
isOpen={isPopoverOpen}
closePopover={() => {
setIsPopoverOpen(false);
}}
>
<EuiText style={{ width: 300 }}>
<p>Popover content that&rsquo;s wider than the default width</p>
</EuiText>
</EuiPopover>

<EuiHorizontalRule />

<EuiToolTip position="top" content="Here is some tooltip text">
<EuiLink href="#">Tooltip example (hover to open)</EuiLink>
</EuiToolTip>
</EuiPanel>
</>
);
};

function copyStyles(targetWindow: Window) {
const collectedStyles: string[] = [];

const elements = [
...document.head.querySelectorAll('link[data-react-helmet="true"]'),
...document.head.getElementsByTagName('style'),
];

elements.forEach((element) => {
collectedStyles.push(element.outerHTML);
});

targetWindow.document.head.innerHTML += collectedStyles.join('\r\n');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { GuideSectionTypes } from '../../components';

import { EuiCode } from '../../../../src/components';
import WindowProviderDemo from './window_provider';
import { EuiWindowProvider } from '../../../../src/services';
const windowProviderSource = require('!!raw-loader!./window_provider');

export const WindowProviderExample = {
title: 'Window provider',
sections: [
{
source: [
{
type: GuideSectionTypes.TSX,
code: windowProviderSource,
},
],
text: (
<>
<p>
There might be situations when you need to render EUI components
inside iframes or in another window using React Portals. To ensure
that target components use the correct <EuiCode>window</EuiCode> and{' '}
<EuiCode>document</EuiCode> object, use the{' '}
<strong>EuiWindowProvider</strong> component.
</p>
</>
),
demo: <WindowProviderDemo />,
props: { EuiWindowProvider },
},
],
};
21 changes: 12 additions & 9 deletions packages/eui/src/components/flyout/flyout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
useIsWithinMinBreakpoint,
useEuiMemoizedStyles,
useGeneratedHtmlId,
useEuiWindow,
} from '../../services';
import { logicalStyle } from '../../global_styling';

Expand Down Expand Up @@ -202,6 +203,8 @@ export const EuiFlyout = forwardRef(
) => {
const Element = as || defaultElement;
const maskRef = useRef<HTMLDivElement>(null);
const currentWindow = useEuiWindow();
const currentDocument = currentWindow?.document ?? document;

const windowIsLargeEnoughToPush =
useIsWithinMinBreakpoint(pushMinBreakpoint);
Expand All @@ -225,23 +228,23 @@ export const EuiFlyout = forwardRef(
const paddingSide =
side === 'left' ? 'paddingInlineStart' : 'paddingInlineEnd';

document.body.style[paddingSide] = `${width}px`;
currentDocument.body.style[paddingSide] = `${width}px`;
return () => {
document.body.style[paddingSide] = '';
currentDocument.body.style[paddingSide] = '';
};
}
}, [isPushed, side, width]);
}, [isPushed, side, width, currentDocument.body.style]);

/**
* This class doesn't actually do anything by EUI, but is nice to add for consumers (JIC)
*/
useEffect(() => {
document.body.classList.add('euiBody--hasFlyout');
currentDocument.body.classList.add('euiBody--hasFlyout');
return () => {
// Remove the hasFlyout class when the flyout is unmounted
document.body.classList.remove('euiBody--hasFlyout');
currentDocument.body.classList.remove('euiBody--hasFlyout');
};
}, []);
}, [currentDocument.body.classList]);

/**
* ESC key closes flyout (always?)
Expand Down Expand Up @@ -290,12 +293,12 @@ export const EuiFlyout = forwardRef(
* If not disabled, automatically add fixed EuiHeaders as shards
* to EuiFlyout focus traps, to prevent focus fighting
*/
const flyoutToggle = useRef<Element | null>(document.activeElement);
const flyoutToggle = useRef<Element | null>(currentDocument.activeElement);
const [fixedHeaders, setFixedHeaders] = useState<HTMLDivElement[]>([]);

useEffect(() => {
if (includeFixedHeadersInFocusTrap) {
const fixedHeaderEls = document.querySelectorAll<HTMLDivElement>(
const fixedHeaderEls = currentDocument.querySelectorAll<HTMLDivElement>(
'.euiHeader[data-fixed-header]'
);
setFixedHeaders(Array.from(fixedHeaderEls));
Expand All @@ -311,7 +314,7 @@ export const EuiFlyout = forwardRef(
// Clear existing headers if necessary, e.g. switching to `false`
setFixedHeaders((headers) => (headers.length ? [] : headers));
}
}, [includeFixedHeadersInFocusTrap, resizeRef]);
}, [includeFixedHeadersInFocusTrap, resizeRef, currentDocument]);

const focusTrapProps: EuiFlyoutProps['focusTrapProps'] = useMemo(
() => ({
Expand Down
Loading