diff --git a/web/packages/design/src/Modal/Modal.tsx b/web/packages/design/src/Modal/Modal.jsx
similarity index 52%
rename from web/packages/design/src/Modal/Modal.tsx
rename to web/packages/design/src/Modal/Modal.jsx
index 9a93b583f8beb..35c3cf250b37b 100644
--- a/web/packages/design/src/Modal/Modal.tsx
+++ b/web/packages/design/src/Modal/Modal.jsx
@@ -16,99 +16,15 @@
* along with this program. If not, see .
*/
-import React, { createRef, cloneElement } from 'react';
-import styled, { StyleFunction } from 'styled-components';
-import { createPortal } from 'react-dom';
+import React from 'react';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
-type Props = {
- /**
- * If `true`, the modal is open.
- */
- open: boolean;
-
- className?: string;
-
- /**
- * Styles passed to the modal, the parent of the children.
- */
- // TODO(ravicious): The type for modalCss might need some work after we migrate the components
- // that use to TypeScript.
- modalCss?: StyleFunction;
-
- /**
- * The child must be a single HTML element, as Modal calls methods such as focus and setAttribute
- * on it.
- */
- children?: React.ReactElement;
-
- /**
- * Properties applied to the Backdrop element.
- */
- BackdropProps?: BackdropProps;
-
- /**
- * If `true`, the modal will not automatically shift focus to itself when it opens, and
- * replace it to the last focused element when it closes.
- * This also works correctly with any modal children that have the `disableAutoFocus` prop.
- *
- * Generally this should never be set to `true` as it makes the modal less
- * accessible to assistive technologies, like screen readers.
- */
- disableAutoFocus?: boolean;
-
- /**
- * If `true`, clicking the backdrop will not fire any callback.
- */
- disableBackdropClick?: boolean;
-
- /**
- * If `true`, the modal will not prevent focus from leaving the modal while open.
- *
- * Generally this should never be set to `true` as it makes the modal less
- * accessible to assistive technologies, like screen readers.
- */
- disableEnforceFocus?: boolean;
+import { ownerDocument } from './../utils';
+import Portal from './Portal';
+import RootRef from './RootRef';
- /**
- * If `true`, hitting escape will not fire any callback.
- */
- disableEscapeKeyDown?: boolean;
-
- /**
- * If `true`, the modal will not restore focus to previously focused element once
- * modal is hidden.
- */
- disableRestoreFocus?: boolean;
-
- /**
- * If `true`, the backdrop is not rendered.
- */
- hideBackdrop?: boolean;
-
- /**
- * Callback fired when the backdrop is clicked.
- */
- onBackdropClick?: (event: React.MouseEvent) => void;
-
- /**
- * Callback fired when the component requests to be closed.
- * The `reason` parameter can optionally be used to control the response to `onClose`.
- */
- onClose?: (
- event: KeyboardEvent | React.MouseEvent,
- reason: 'escapeKeyDown' | 'backdropClick'
- ) => void;
-
- /**
- * Callback fired when the escape key is pressed,
- * `disableEscapeKeyDown` is false and the modal is in focus.
- */
- onEscapeKeyDown?: (event: KeyboardEvent) => void;
-};
-
-export default class Modal extends React.Component {
- lastFocus: HTMLElement | undefined;
- modalRef = createRef();
+export default class Modal extends React.Component {
mounted = false;
componentDidMount() {
@@ -118,11 +34,11 @@ export default class Modal extends React.Component {
}
}
- componentDidUpdate(prevProps: Props) {
+ componentDidUpdate(prevProps) {
if (prevProps.open && !this.props.open) {
this.handleClose();
} else if (!prevProps.open && this.props.open) {
- this.lastFocus = document.activeElement as HTMLElement;
+ this.lastFocus = ownerDocument(this.mountNode).activeElement;
this.handleOpen();
}
}
@@ -134,26 +50,12 @@ export default class Modal extends React.Component {
}
}
- dialogEl = (): Element => {
- const modalEl = this.modalRef.current;
- if (!modalEl) {
- return;
- }
-
- const isBackdropRenderedFirst = !this.props.hideBackdrop;
-
- if (isBackdropRenderedFirst) {
- return modalEl.children[1];
- }
-
- return modalEl.firstElementChild;
- };
-
handleOpen = () => {
- document.addEventListener('keydown', this.handleDocumentKeyDown);
- document.addEventListener('focus', this.enforceFocus, true);
+ const doc = ownerDocument(this.mountNode);
+ doc.addEventListener('keydown', this.handleDocumentKeyDown);
+ doc.addEventListener('focus', this.enforceFocus, true);
- if (this.dialogEl()) {
+ if (this.dialogRef) {
this.handleOpened();
}
};
@@ -161,17 +63,18 @@ export default class Modal extends React.Component {
handleOpened = () => {
this.autoFocus();
// Fix a bug on Chrome where the scroll isn't initially 0.
- this.modalRef.current.scrollTop = 0;
+ this.modalRef.scrollTop = 0;
};
handleClose = () => {
- document.removeEventListener('keydown', this.handleDocumentKeyDown);
- document.removeEventListener('focus', this.enforceFocus, true);
+ const doc = ownerDocument(this.mountNode);
+ doc.removeEventListener('keydown', this.handleDocumentKeyDown);
+ doc.removeEventListener('focus', this.enforceFocus, true);
this.restoreLastFocus();
};
- handleBackdropClick = (event: React.MouseEvent) => {
+ handleBackdropClick = event => {
if (event.target !== event.currentTarget) {
return;
}
@@ -185,7 +88,13 @@ export default class Modal extends React.Component {
}
};
- handleDocumentKeyDown = (event: KeyboardEvent) => {
+ handleRendered = () => {
+ if (this.props.onRendered) {
+ this.props.onRendered();
+ }
+ };
+
+ handleDocumentKeyDown = event => {
const ESC = 'Escape';
// Ignore events that have been `event.preventDefault()` marked.
@@ -204,33 +113,44 @@ export default class Modal extends React.Component {
enforceFocus = () => {
// The Modal might not already be mounted.
- if (this.props.disableEnforceFocus || !this.mounted || !this.dialogEl()) {
+ if (this.props.disableEnforceFocus || !this.mounted || !this.dialogRef) {
return;
}
- const currentActiveElement = document.activeElement;
+ const currentActiveElement = ownerDocument(this.mountNode).activeElement;
- if (!this.dialogEl().contains(currentActiveElement)) {
- // Technically, dialogEl can be something else than an HTML element with the focus method.
- this.dialogEl()['focus']?.();
+ if (!this.dialogRef.contains(currentActiveElement)) {
+ this.dialogRef.focus();
}
};
+ handlePortalRef = ref => {
+ this.mountNode = ref ? ref.getMountNode() : ref;
+ };
+
+ handleModalRef = ref => {
+ this.modalRef = ref;
+ };
+
+ onRootRef = ref => {
+ this.dialogRef = ref;
+ };
+
autoFocus() {
// We might render an empty child.
- if (this.props.disableAutoFocus || !this.dialogEl()) {
+ if (this.props.disableAutoFocus || !this.dialogRef) {
return;
}
- const currentActiveElement = document.activeElement as HTMLElement;
+ const currentActiveElement = ownerDocument(this.mountNode).activeElement;
- if (!this.dialogEl().contains(currentActiveElement)) {
- if (!this.dialogEl().hasAttribute('tabIndex')) {
- this.dialogEl().setAttribute('tabIndex', '-1');
+ if (!this.dialogRef.contains(currentActiveElement)) {
+ if (!this.dialogRef.hasAttribute('tabIndex')) {
+ this.dialogRef.setAttribute('tabIndex', -1);
}
this.lastFocus = currentActiveElement;
- this.dialogEl()['focus']?.();
+ this.dialogRef.focus();
}
}
@@ -250,40 +170,144 @@ export default class Modal extends React.Component {
}
render() {
- const { BackdropProps, children, modalCss, hideBackdrop, open, className } =
- this.props;
+ const {
+ BackdropProps,
+ children,
+ container,
+ disablePortal,
+ modalCss,
+ hideBackdrop,
+ open,
+ className,
+ } = this.props;
+
+ const childProps = {};
if (!open) {
return null;
}
- return createPortal(
- e.stopPropagation()}
+ return (
+
- {!hideBackdrop && (
-
- )}
- {cloneElement(children, {})}
- ,
- document.body
+ e.stopPropagation()}
+ >
+ {!hideBackdrop && (
+
+ )}
+
+ {React.cloneElement(children, childProps)}
+
+
+
);
}
}
-type BackdropProps = {
+Modal.propTypes = {
+ /**
+ * Properties applied to the [`Backdrop`](/api/backdrop/) element.
+ *
+ * invisible: Boolean - allows backdrop to keep bg color of parent eg: popup menu
+ */
+ BackdropProps: PropTypes.object,
+ /**
+ * A single child content element.
+ */
+ children: PropTypes.element,
/**
- * Allows backdrop to keep bg color of parent eg: popup menu
+ * A node, component instance, or function that returns either.
+ * The `container` will have the portal children appended to it.
*/
- invisible: boolean;
- [prop: string]: any;
+ container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ /**
+ * If `true`, the modal will not automatically shift focus to itself when it opens, and
+ * replace it to the last focused element when it closes.
+ * This also works correctly with any modal children that have the `disableAutoFocus` prop.
+ *
+ * Generally this should never be set to `true` as it makes the modal less
+ * accessible to assistive technologies, like screen readers.
+ */
+ disableAutoFocus: PropTypes.bool,
+ /**
+ * If `true`, clicking the backdrop will not fire any callback.
+ */
+ disableBackdropClick: PropTypes.bool,
+ /**
+ * If `true`, the modal will not prevent focus from leaving the modal while open.
+ *
+ * Generally this should never be set to `true` as it makes the modal less
+ * accessible to assistive technologies, like screen readers.
+ */
+ disableEnforceFocus: PropTypes.bool,
+ /**
+ * If `true`, hitting escape will not fire any callback.
+ */
+ disableEscapeKeyDown: PropTypes.bool,
+ /**
+ * Disable the portal behavior.
+ * The children stay within it's parent DOM hierarchy.
+ */
+ disablePortal: PropTypes.bool,
+ /**
+ * If `true`, the modal will not restore focus to previously focused element once
+ * modal is hidden.
+ */
+ disableRestoreFocus: PropTypes.bool,
+ /**
+ * If `true`, the backdrop is not rendered.
+ */
+ hideBackdrop: PropTypes.bool,
+ /**
+ * Callback fired when the backdrop is clicked.
+ */
+ onBackdropClick: PropTypes.func,
+ /**
+ * Callback fired when the component requests to be closed.
+ * The `reason` parameter can optionally be used to control the response to `onClose`.
+ *
+ * @param {object} event The event source of the callback
+ * @param {string} reason Can be:`"escapeKeyDown"`, `"backdropClick"`
+ */
+ onClose: PropTypes.func,
+ /**
+ * Callback fired when the escape key is pressed,
+ * `disableEscapeKeyDown` is false and the modal is in focus.
+ */
+ onEscapeKeyDown: PropTypes.func,
+ /**
+ * Callback fired once the children has been mounted into the `container`.
+ * It signals that the `open={true}` property took effect.
+ */
+ onRendered: PropTypes.func,
+ /**
+ * If `true`, the modal is open.
+ */
+ open: PropTypes.bool.isRequired,
+ className: PropTypes.string,
+};
+
+Modal.defaultProps = {
+ disableAutoFocus: false,
+ disableBackdropClick: false,
+ disableEnforceFocus: false,
+ disableEscapeKeyDown: false,
+ disablePortal: false,
+ disableRestoreFocus: false,
+ hideBackdrop: false,
};
-function Backdrop(props: BackdropProps) {
+function Backdrop(props) {
const { invisible, ...rest } = props;
return (
`
+const StyledBackdrop = styled.div`
z-index: -1;
position: fixed;
right: 0;
@@ -308,15 +332,12 @@ const StyledBackdrop = styled.div`
touch-action: none;
`;
-const StyledModal = styled.div<{
- modalCss: StyleFunction;
- ref: React.ForwardedRef;
-}>`
+const StyledModal = styled.div`
position: fixed;
z-index: 1200;
right: 0;
bottom: 0;
top: 0;
left: 0;
- ${props => props.modalCss?.(props)}
+ ${props => props.modalCss && props.modalCss(props)}
`;
diff --git a/web/packages/design/src/Modal/Portal.jsx b/web/packages/design/src/Modal/Portal.jsx
new file mode 100644
index 0000000000000..1e11f0a295309
--- /dev/null
+++ b/web/packages/design/src/Modal/Portal.jsx
@@ -0,0 +1,121 @@
+/*
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+
+import { ownerDocument } from './../utils';
+
+/**
+ * Portals provide a first-class way to render children into a DOM node
+ * that exists outside the DOM hierarchy of the parent component.
+ */
+class Portal extends React.Component {
+ componentDidMount() {
+ this.setMountNode(this.props.container);
+
+ // Only rerender if needed
+ if (!this.props.disablePortal) {
+ // Portal initializes the container and mounts it to the DOM during
+ // first render. No children are rendered at this time.
+ // ForceUpdate is called to render children elements inside
+ // the container after it gets mounted.
+ this.forceUpdate();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (
+ prevProps.container !== this.props.container ||
+ prevProps.disablePortal !== this.props.disablePortal
+ ) {
+ this.setMountNode(this.props.container);
+
+ // Only rerender if needed
+ if (!this.props.disablePortal) {
+ this.forceUpdate();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ this.mountNode = null;
+ }
+
+ setMountNode(container) {
+ if (this.props.disablePortal) {
+ this.mountNode = ReactDOM.findDOMNode(this).parentElement;
+ } else {
+ this.mountNode = getContainer(container, getOwnerDocument(this).body);
+ }
+ }
+
+ /**
+ * @public
+ */
+ getMountNode = () => {
+ return this.mountNode;
+ };
+
+ render() {
+ const { children, disablePortal } = this.props;
+
+ if (disablePortal) {
+ return children;
+ }
+
+ return this.mountNode
+ ? ReactDOM.createPortal(children, this.mountNode)
+ : null;
+ }
+}
+
+Portal.propTypes = {
+ /**
+ * The children to render into the `container`.
+ */
+ children: PropTypes.node.isRequired,
+ /**
+ * A node, component instance, or function that returns either.
+ * The `container` will have the portal children appended to it.
+ * By default, it uses the body of the top-level document object,
+ * so it's simply `document.body` most of the time.
+ */
+ container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ /**
+ * Disable the portal behavior.
+ * The children stay within it's parent DOM hierarchy.
+ */
+ disablePortal: PropTypes.bool,
+};
+
+Portal.defaultProps = {
+ disablePortal: false,
+};
+
+function getContainer(container, defaultContainer) {
+ container = typeof container === 'function' ? container() : container;
+ return ReactDOM.findDOMNode(container) || defaultContainer;
+}
+
+function getOwnerDocument(element) {
+ return ownerDocument(ReactDOM.findDOMNode(element));
+}
+
+export default Portal;
diff --git a/web/packages/design/src/Modal/Portal.test.tsx b/web/packages/design/src/Modal/Portal.test.tsx
new file mode 100644
index 0000000000000..5c2a0b0129185
--- /dev/null
+++ b/web/packages/design/src/Modal/Portal.test.tsx
@@ -0,0 +1,55 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+import { screen } from '@testing-library/react';
+
+import { render } from 'design/utils/testing';
+
+import Portal from './Portal';
+
+describe('design/Modal/Portal', () => {
+ test('container to be attached to body element', () => {
+ const { container } = renderPortal({});
+ const content = screen.getByTestId('content');
+ expect(container).not.toContainElement(content);
+ expect(document.body).toContainElement(screen.getByTestId('parent'));
+ });
+
+ test('container to be attached to custom element', () => {
+ const customElement = document.createElement('div');
+ renderPortal({ container: customElement });
+ expect(screen.queryByTestId('content')).not.toBeInTheDocument();
+ expect(customElement).toHaveTextContent('hello');
+ });
+
+ test('disable the portal behavior', () => {
+ const { container } = renderPortal({ disablePortal: true });
+ expect(container).toContainElement(screen.getByTestId('content'));
+ });
+});
+
+function renderPortal(props) {
+ return render(
+
+ );
+}
diff --git a/web/packages/design/src/Modal/RootRef.jsx b/web/packages/design/src/Modal/RootRef.jsx
new file mode 100644
index 0000000000000..b4f49a6e280dd
--- /dev/null
+++ b/web/packages/design/src/Modal/RootRef.jsx
@@ -0,0 +1,72 @@
+/*
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+
+class RootRef extends React.Component {
+ componentDidMount() {
+ this.ref = ReactDOM.findDOMNode(this);
+ setRef(this.props.rootRef, this.ref);
+ }
+
+ componentDidUpdate(prevProps) {
+ const ref = ReactDOM.findDOMNode(this);
+
+ if (prevProps.rootRef !== this.props.rootRef || this.ref !== ref) {
+ if (prevProps.rootRef !== this.props.rootRef) {
+ setRef(prevProps.rootRef, null);
+ }
+
+ this.ref = ref;
+ setRef(this.props.rootRef, this.ref);
+ }
+ }
+
+ componentWillUnmount() {
+ this.ref = null;
+ setRef(this.props.rootRef, null);
+ }
+
+ render() {
+ return this.props.children;
+ }
+}
+
+function setRef(ref, value) {
+ if (typeof ref === 'function') {
+ ref(value);
+ } else if (ref) {
+ ref.current = value;
+ }
+}
+
+RootRef.propTypes = {
+ /**
+ * The wrapped element.
+ */
+ children: PropTypes.element.isRequired,
+ /**
+ * Provide a way to access the DOM node of the wrapped element.
+ * You can provide a callback ref or a `React.createRef()` ref.
+ */
+ rootRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
+};
+
+export default RootRef;
diff --git a/web/packages/design/src/Popover/Popover.jsx b/web/packages/design/src/Popover/Popover.jsx
index 0a4403b775d3b..00d2857593178 100644
--- a/web/packages/design/src/Popover/Popover.jsx
+++ b/web/packages/design/src/Popover/Popover.jsx
@@ -40,10 +40,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
-import React, { createRef } from 'react';
+import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
+import ReactDOM from 'react-dom';
+import { ownerWindow, ownerDocument } from '../utils';
import Modal from '../Modal';
import Transition from './Transition';
@@ -101,7 +103,6 @@ function getAnchorEl(anchorEl) {
}
export default class Popover extends React.Component {
- paperRef = createRef();
handleGetOffsetTop = getOffsetTop;
handleGetOffsetLeft = getOffsetLeft;
@@ -117,7 +118,7 @@ export default class Popover extends React.Component {
return;
}
- this.setPositioningStyles(this.paperRef.current);
+ this.setPositioningStyles(this.paperRef);
};
}
}
@@ -152,7 +153,7 @@ export default class Popover extends React.Component {
};
getPositioningStyle = element => {
- const { anchorReference, marginThreshold } = this.props;
+ const { anchorEl, anchorReference, marginThreshold } = this.props;
// Check if the parent has requested anchoring on an inner content node
const contentAnchorOffset = this.getContentAnchorOffset(element);
@@ -188,9 +189,12 @@ export default class Popover extends React.Component {
let bottom = top + elemRect.height;
let right = left + elemRect.width;
+ // Use the parent window of the anchorEl if provided
+ const containerWindow = ownerWindow(getAnchorEl(anchorEl));
+
// Window thresholds taking required margin into account
- const heightThreshold = window.innerHeight - marginThreshold;
- const widthThreshold = window.innerWidth - marginThreshold;
+ const heightThreshold = containerWindow.innerHeight - marginThreshold;
+ const widthThreshold = containerWindow.innerWidth - marginThreshold;
// Check if the vertical axis needs shifting
if (top < marginThreshold) {
@@ -220,8 +224,8 @@ export default class Popover extends React.Component {
return {
top: `${top}px`,
left: `${left}px`,
- bottom: `${window.innerHeight - bottom}px`,
- right: `${window.innerWidth - right}px`,
+ bottom: `${containerWindow.innerHeight - bottom}px`,
+ right: `${containerWindow.innerWidth - right}px`,
transformOrigin: getTransformOriginValue(transformOrigin),
};
};
@@ -232,7 +236,8 @@ export default class Popover extends React.Component {
const { anchorEl, anchorOrigin } = this.props;
// If an anchor element wasn't provided, just use the parent body element of this Popover
- const anchorElement = getAnchorEl(anchorEl) || document.body;
+ const anchorElement =
+ getAnchorEl(anchorEl) || ownerDocument(this.paperRef).body;
const anchorRect = anchorElement.getBoundingClientRect();
@@ -296,10 +301,25 @@ export default class Popover extends React.Component {
};
render() {
- const { children, open, popoverCss, ...other } = this.props;
+ const {
+ anchorEl,
+ children,
+ container: containerProp,
+ open,
+ popoverCss,
+ ...other
+ } = this.props;
+
+ // If the container prop is provided, use that
+ // If the anchorEl prop is provided, use its parent body element as the container
+ // If neither are provided let the Modal take care of choosing the container
+ const container =
+ containerProp ||
+ (anchorEl ? ownerDocument(getAnchorEl(anchorEl)).body : undefined);
return (
{
+ this.paperRef = ReactDOM.findDOMNode(ref);
+ }}
>
{children}
@@ -376,6 +398,15 @@ Popover.propTypes = {
* The content of the component.
*/
children: PropTypes.node,
+ /**
+ * A node, component instance, or function that returns either.
+ * The `container` will passed to the Modal component.
+ * By default, it uses the body of the anchorEl's top-level document object,
+ * so it's simply `document.body` most of the time.
+ */
+ container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ /**
+ */
/**
* This function is called in order to retrieve the content anchor element.
* It's the opposite of the `anchorEl` property.
diff --git a/web/packages/design/src/utils/index.ts b/web/packages/design/src/utils/index.ts
new file mode 100644
index 0000000000000..6cac5e7bddc2b
--- /dev/null
+++ b/web/packages/design/src/utils/index.ts
@@ -0,0 +1,26 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export function ownerDocument(node?: Element) {
+ return (node && node.ownerDocument) || document;
+}
+
+export function ownerWindow(node?: Element): Window {
+ const doc = ownerDocument(node);
+ return (doc && doc.defaultView) || window;
+}