diff --git a/web/packages/design/src/Modal/Modal.jsx b/web/packages/design/src/Modal/Modal.jsx
deleted file mode 100644
index 35c3cf250b37b..0000000000000
--- a/web/packages/design/src/Modal/Modal.jsx
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- * 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 styled from 'styled-components';
-import PropTypes from 'prop-types';
-
-import { ownerDocument } from './../utils';
-import Portal from './Portal';
-import RootRef from './RootRef';
-
-export default class Modal extends React.Component {
- mounted = false;
-
- componentDidMount() {
- this.mounted = true;
- if (this.props.open) {
- this.handleOpen();
- }
- }
-
- componentDidUpdate(prevProps) {
- if (prevProps.open && !this.props.open) {
- this.handleClose();
- } else if (!prevProps.open && this.props.open) {
- this.lastFocus = ownerDocument(this.mountNode).activeElement;
- this.handleOpen();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- if (this.props.open) {
- this.handleClose();
- }
- }
-
- handleOpen = () => {
- const doc = ownerDocument(this.mountNode);
- doc.addEventListener('keydown', this.handleDocumentKeyDown);
- doc.addEventListener('focus', this.enforceFocus, true);
-
- if (this.dialogRef) {
- this.handleOpened();
- }
- };
-
- handleOpened = () => {
- this.autoFocus();
- // Fix a bug on Chrome where the scroll isn't initially 0.
- this.modalRef.scrollTop = 0;
- };
-
- handleClose = () => {
- const doc = ownerDocument(this.mountNode);
- doc.removeEventListener('keydown', this.handleDocumentKeyDown);
- doc.removeEventListener('focus', this.enforceFocus, true);
-
- this.restoreLastFocus();
- };
-
- handleBackdropClick = event => {
- if (event.target !== event.currentTarget) {
- return;
- }
-
- if (this.props.onBackdropClick) {
- this.props.onBackdropClick(event);
- }
-
- if (!this.props.disableBackdropClick && this.props.onClose) {
- this.props.onClose(event, 'backdropClick');
- }
- };
-
- handleRendered = () => {
- if (this.props.onRendered) {
- this.props.onRendered();
- }
- };
-
- handleDocumentKeyDown = event => {
- const ESC = 'Escape';
-
- // Ignore events that have been `event.preventDefault()` marked.
- if (event.key !== ESC || event.defaultPrevented) {
- return;
- }
-
- if (this.props.onEscapeKeyDown) {
- this.props.onEscapeKeyDown(event);
- }
-
- if (!this.props.disableEscapeKeyDown && this.props.onClose) {
- this.props.onClose(event, 'escapeKeyDown');
- }
- };
-
- enforceFocus = () => {
- // The Modal might not already be mounted.
- if (this.props.disableEnforceFocus || !this.mounted || !this.dialogRef) {
- return;
- }
-
- const currentActiveElement = ownerDocument(this.mountNode).activeElement;
-
- 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.dialogRef) {
- return;
- }
-
- const currentActiveElement = ownerDocument(this.mountNode).activeElement;
-
- if (!this.dialogRef.contains(currentActiveElement)) {
- if (!this.dialogRef.hasAttribute('tabIndex')) {
- this.dialogRef.setAttribute('tabIndex', -1);
- }
-
- this.lastFocus = currentActiveElement;
- this.dialogRef.focus();
- }
- }
-
- restoreLastFocus() {
- if (this.props.disableRestoreFocus || !this.lastFocus) {
- return;
- }
-
- // Not all elements in IE 11 have a focus method.
- // Because IE 11 market share is low, we accept the restore focus being broken
- // and we silent the issue.
- if (this.lastFocus.focus) {
- this.lastFocus.focus();
- }
-
- this.lastFocus = null;
- }
-
- render() {
- const {
- BackdropProps,
- children,
- container,
- disablePortal,
- modalCss,
- hideBackdrop,
- open,
- className,
- } = this.props;
-
- const childProps = {};
-
- if (!open) {
- return null;
- }
-
- return (
-
- e.stopPropagation()}
- >
- {!hideBackdrop && (
-
- )}
-
- {React.cloneElement(children, childProps)}
-
-
-
- );
- }
-}
-
-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,
- /**
- * A node, component instance, or function that returns either.
- * The `container` will have the portal children appended to it.
- */
- 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) {
- const { invisible, ...rest } = props;
- return (
-
- );
-}
-
-const StyledBackdrop = styled.div`
- z-index: -1;
- position: fixed;
- right: 0;
- bottom: 0;
- top: 0;
- left: 0;
- background-color: ${props =>
- props.invisible ? 'transparent' : `rgba(0, 0, 0, 0.5)`};
- opacity: 1;
- touch-action: none;
-`;
-
-const StyledModal = styled.div`
- position: fixed;
- z-index: 1200;
- right: 0;
- bottom: 0;
- top: 0;
- left: 0;
- ${props => props.modalCss && props.modalCss(props)}
-`;
diff --git a/web/packages/design/src/Modal/Modal.test.tsx b/web/packages/design/src/Modal/Modal.test.tsx
index f8785a5294bd8..5fba1c2e3e9d2 100644
--- a/web/packages/design/src/Modal/Modal.test.tsx
+++ b/web/packages/design/src/Modal/Modal.test.tsx
@@ -16,8 +16,9 @@
* along with this program. If not, see .
*/
-import React from 'react';
+import { useState } from 'react';
import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { render, fireEvent } from 'design/utils/testing';
@@ -141,4 +142,43 @@ describe('design/Modal', () => {
'background-color': 'transparent',
});
});
+
+ it('restores focus on close', async () => {
+ const user = userEvent.setup();
+ render();
+ const toggleModalButton = screen.getByRole('button', { name: 'Toggle' });
+
+ await user.click(toggleModalButton);
+ // Type in the input inside the modal to shift focus into another element.
+ const input = screen.getByLabelText('Input in modal');
+ await user.type(input, 'a');
+
+ const closeModal = screen.getByRole('button', { name: 'Close modal' });
+ await user.click(closeModal);
+
+ expect(toggleModalButton).toHaveFocus();
+ });
});
+
+const ToggleableModal = () => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
diff --git a/web/packages/design/src/Modal/Modal.tsx b/web/packages/design/src/Modal/Modal.tsx
new file mode 100644
index 0000000000000..7aff835580c1d
--- /dev/null
+++ b/web/packages/design/src/Modal/Modal.tsx
@@ -0,0 +1,270 @@
+/*
+ * 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, { createRef, cloneElement } from 'react';
+import styled, { StyleFunction } from 'styled-components';
+import { createPortal } from 'react-dom';
+
+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. Modal used to call methods such as focus and
+ * setAttribute on the outermost element. This is no longer the case, so technically this type can
+ * be adjusted if needed.
+ */
+ children?: React.ReactElement;
+
+ /**
+ * Properties applied to the Backdrop element.
+ */
+ BackdropProps?: BackdropProps;
+
+ /**
+ * If `true`, clicking the backdrop will not fire any callback.
+ */
+ disableBackdropClick?: boolean;
+
+ /**
+ * 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();
+ mounted = false;
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.props.open) {
+ this.handleOpen();
+ }
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.open && !this.props.open) {
+ this.handleClose();
+ } else if (!prevProps.open && this.props.open) {
+ this.lastFocus = document.activeElement as HTMLElement;
+ this.handleOpen();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ if (this.props.open) {
+ this.handleClose();
+ }
+ }
+
+ 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);
+
+ if (this.dialogEl()) {
+ this.handleOpened();
+ }
+ };
+
+ handleOpened = () => {
+ // Fix a bug on Chrome where the scroll isn't initially 0.
+ this.modalRef.current.scrollTop = 0;
+ };
+
+ handleClose = () => {
+ document.removeEventListener('keydown', this.handleDocumentKeyDown);
+
+ this.restoreLastFocus();
+ };
+
+ handleBackdropClick = (event: React.MouseEvent) => {
+ if (event.target !== event.currentTarget) {
+ return;
+ }
+
+ if (this.props.onBackdropClick) {
+ this.props.onBackdropClick(event);
+ }
+
+ if (!this.props.disableBackdropClick && this.props.onClose) {
+ this.props.onClose(event, 'backdropClick');
+ }
+ };
+
+ handleDocumentKeyDown = (event: KeyboardEvent) => {
+ const ESC = 'Escape';
+
+ // Ignore events that have been `event.preventDefault()` marked.
+ if (event.key !== ESC || event.defaultPrevented) {
+ return;
+ }
+
+ if (this.props.onEscapeKeyDown) {
+ this.props.onEscapeKeyDown(event);
+ }
+
+ if (!this.props.disableEscapeKeyDown && this.props.onClose) {
+ this.props.onClose(event, 'escapeKeyDown');
+ }
+ };
+
+ restoreLastFocus() {
+ if (this.props.disableRestoreFocus || !this.lastFocus) {
+ return;
+ }
+
+ // Not all elements in IE 11 have a focus method.
+ // Because IE 11 market share is low, we accept the restore focus being broken
+ // and we silent the issue.
+ if (this.lastFocus.focus) {
+ this.lastFocus.focus();
+ }
+
+ this.lastFocus = null;
+ }
+
+ render() {
+ const { BackdropProps, children, modalCss, hideBackdrop, open, className } =
+ this.props;
+
+ if (!open) {
+ return null;
+ }
+
+ return createPortal(
+ e.stopPropagation()}
+ >
+ {!hideBackdrop && (
+
+ )}
+ {cloneElement(children, {})}
+ ,
+ document.body
+ );
+ }
+}
+
+type BackdropProps = {
+ /**
+ * Allows backdrop to keep bg color of parent eg: popup menu
+ */
+ invisible: boolean;
+ [prop: string]: any;
+};
+
+function Backdrop(props: BackdropProps) {
+ const { invisible, ...rest } = props;
+ return (
+
+ );
+}
+
+const StyledBackdrop = styled.div`
+ z-index: -1;
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ left: 0;
+ background-color: ${props =>
+ props.invisible ? 'transparent' : `rgba(0, 0, 0, 0.5)`};
+ opacity: 1;
+ touch-action: none;
+`;
+
+const StyledModal = styled.div<{
+ modalCss: StyleFunction;
+ ref: React.ForwardedRef;
+}>`
+ position: fixed;
+ z-index: 1200;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ left: 0;
+ ${props => props.modalCss?.(props)}
+`;
diff --git a/web/packages/design/src/Modal/Portal.jsx b/web/packages/design/src/Modal/Portal.jsx
deleted file mode 100644
index 1e11f0a295309..0000000000000
--- a/web/packages/design/src/Modal/Portal.jsx
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 5c2a0b0129185..0000000000000
--- a/web/packages/design/src/Modal/Portal.test.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * 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(
-
-
-
hello
-
-
- );
-}
diff --git a/web/packages/design/src/Modal/RootRef.jsx b/web/packages/design/src/Modal/RootRef.jsx
deleted file mode 100644
index b4f49a6e280dd..0000000000000
--- a/web/packages/design/src/Modal/RootRef.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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/Modal/index.js b/web/packages/design/src/Modal/index.ts
similarity index 100%
rename from web/packages/design/src/Modal/index.js
rename to web/packages/design/src/Modal/index.ts
diff --git a/web/packages/design/src/Popover/Popover.jsx b/web/packages/design/src/Popover/Popover.jsx
index 00d2857593178..0a4403b775d3b 100644
--- a/web/packages/design/src/Popover/Popover.jsx
+++ b/web/packages/design/src/Popover/Popover.jsx
@@ -40,12 +40,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
-import React from 'react';
+import React, { createRef } 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';
@@ -103,6 +101,7 @@ function getAnchorEl(anchorEl) {
}
export default class Popover extends React.Component {
+ paperRef = createRef();
handleGetOffsetTop = getOffsetTop;
handleGetOffsetLeft = getOffsetLeft;
@@ -118,7 +117,7 @@ export default class Popover extends React.Component {
return;
}
- this.setPositioningStyles(this.paperRef);
+ this.setPositioningStyles(this.paperRef.current);
};
}
}
@@ -153,7 +152,7 @@ export default class Popover extends React.Component {
};
getPositioningStyle = element => {
- const { anchorEl, anchorReference, marginThreshold } = this.props;
+ const { anchorReference, marginThreshold } = this.props;
// Check if the parent has requested anchoring on an inner content node
const contentAnchorOffset = this.getContentAnchorOffset(element);
@@ -189,12 +188,9 @@ 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 = containerWindow.innerHeight - marginThreshold;
- const widthThreshold = containerWindow.innerWidth - marginThreshold;
+ const heightThreshold = window.innerHeight - marginThreshold;
+ const widthThreshold = window.innerWidth - marginThreshold;
// Check if the vertical axis needs shifting
if (top < marginThreshold) {
@@ -224,8 +220,8 @@ export default class Popover extends React.Component {
return {
top: `${top}px`,
left: `${left}px`,
- bottom: `${containerWindow.innerHeight - bottom}px`,
- right: `${containerWindow.innerWidth - right}px`,
+ bottom: `${window.innerHeight - bottom}px`,
+ right: `${window.innerWidth - right}px`,
transformOrigin: getTransformOriginValue(transformOrigin),
};
};
@@ -236,8 +232,7 @@ 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) || ownerDocument(this.paperRef).body;
+ const anchorElement = getAnchorEl(anchorEl) || document.body;
const anchorRect = anchorElement.getBoundingClientRect();
@@ -301,25 +296,10 @@ export default class Popover extends React.Component {
};
render() {
- 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);
+ const { children, open, popoverCss, ...other } = this.props;
return (
{
- this.paperRef = ReactDOM.findDOMNode(ref);
- }}
+ ref={this.paperRef}
>
{children}
@@ -398,15 +376,6 @@ 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
deleted file mode 100644
index 6cac5e7bddc2b..0000000000000
--- a/web/packages/design/src/utils/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * 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;
-}