className?: string;
closeOnClick?: boolean;
id: string;
+ isPortal?: boolean;
onOpen?: VoidFunction;
overlay: React.ReactNode;
placement?: PopupPlacement;
render() {
const { open } = this.state;
- const { allowResizing, className, closeOnClick = true, id, size = 'full', zLevel } = this.props;
+ const {
+ allowResizing,
+ className,
+ closeOnClick = true,
+ id,
+ isPortal,
+ size = 'full',
+ zLevel,
+ } = this.props;
const a11yAttrs: A11yAttrs = {
'aria-controls': `${id}-dropdown`,
'aria-expanded': open,
aria-labelledby={`${id}-trigger`}
className={className}
id={`${id}-dropdown`}
+ isPortal={isPortal}
onRequestClose={this.handleClose}
open={open}
overlay={
*/
import EscKeydownHandler from './EscKeydownHandler';
import OutsideClickHandler from './OutsideClickHandler';
-import { PortalPopup } from './popups';
+import { Popup } from './popups';
-type PopupProps = PortalPopup['props'];
+type PopupProps = Popup['props'];
interface Props extends PopupProps {
onRequestClose: VoidFunction;
const { children, open, onRequestClose, overlay, ...popupProps } = props;
return (
- <PortalPopup
+ <Popup
overlay={
open ? (
<OutsideClickHandler onClickOutside={onRequestClose}>
{...popupProps}
>
{children}
- </PortalPopup>
+ </Popup>
);
}
import { ButtonSecondary } from '../buttons';
import Dropdown, { ActionsDropdown } from '../Dropdown';
+describe('Dropdown with Portal Wrapper', () => {
+ it('renders', async () => {
+ const { user } = setupWithChildren();
+ expect(screen.getByRole('button')).toBeInTheDocument();
+
+ await user.click(screen.getByRole('button'));
+ expect(screen.getByRole('menu')).toBeInTheDocument();
+ });
+
+ it('toggles with render prop', async () => {
+ const { user } = setupWithChildren(({ onToggleClick }) => (
+ <ButtonSecondary onClick={onToggleClick} />
+ ));
+
+ await user.click(screen.getByRole('button'));
+ expect(screen.getByRole('menu')).toBeVisible();
+ });
+
+ function setupWithChildren(children?: Dropdown['props']['children']) {
+ return renderWithRouter(
+ <Dropdown id="test-menu" isPortal={true} overlay={<div id="overlay" />}>
+ {children ?? <ButtonSecondary />}
+ </Dropdown>
+ );
+ }
+});
+
describe('Dropdown', () => {
it('renders', async () => {
const { user } = setupWithChildren();
import { themeBorder, themeColor, themeContrast, themeShadow } from '../helpers/theme';
import ClickEventBoundary from './ClickEventBoundary';
-interface PopupProps {
+interface PopupBaseProps {
'aria-labelledby'?: string;
children?: React.ReactNode;
className?: string;
zLevel?: PopupZLevel;
}
-function PopupBase(props: PopupProps, ref: React.Ref<HTMLDivElement>) {
+function PopupBase(props: PopupBaseProps, ref: React.Ref<HTMLDivElement>) {
const {
children,
className,
const PopupWithRef = React.forwardRef(PopupBase);
PopupWithRef.displayName = 'Popup';
-export const Popup = PopupWithRef;
-
-interface PortalPopupProps extends Omit<PopupProps, 'style'> {
+interface PopupProps extends Omit<PopupBaseProps, 'style'> {
allowResizing?: boolean;
children: React.ReactNode;
+ isPortal?: boolean;
overlay: React.ReactNode;
}
return state.height !== undefined;
}
-export class PortalPopup extends React.PureComponent<PortalPopupProps, State> {
+export class Popup extends React.PureComponent<PopupProps, State> {
mounted = false;
popupNode = React.createRef<HTMLDivElement>();
throttledPositionTooltip: () => void;
- constructor(props: PortalPopupProps) {
+ constructor(props: PopupProps) {
super(props);
this.state = {};
this.throttledPositionTooltip = throttle(this.positionPopup, THROTTLE_SCROLL_DELAY);
this.mounted = true;
}
- componentDidUpdate(prevProps: PortalPopupProps) {
+ componentDidUpdate(prevProps: PopupProps) {
if (this.props.placement !== prevProps.placement || this.props.overlay !== prevProps.overlay) {
this.positionPopup();
}
return (
<>
{this.props.children}
- {this.props.overlay && (
- <PortalWrapper>
- <Popup placement={placement} ref={this.popupNode} style={style} {...popupProps}>
+ {this.props.overlay &&
+ (this.props.isPortal ? (
+ <PortalWrapper>
+ <PopupWithRef
+ placement={placement}
+ ref={this.popupNode}
+ style={style}
+ {...popupProps}
+ >
+ {overlay}
+ </PopupWithRef>
+ </PortalWrapper>
+ ) : (
+ <PopupWithRef placement={placement} ref={this.popupNode} style={style} {...popupProps}>
{overlay}
- </Popup>
- </PortalWrapper>
- )}
+ </PopupWithRef>
+ ))}
</>
);
}
InteractiveIcon,
INTERACTIVE_TOOLTIP_DELAY,
MenuSearchIcon,
+ Popup,
PopupZLevel,
- PortalPopup,
TextMuted,
} from 'design-system';
import { debounce, uniqBy } from 'lodash';
const search = (
<div role="search" className="sw-min-w-abs-200 sw-max-w-abs-350 sw-w-full">
- <PortalPopup
+ <Popup
allowResizing={true}
overlay={
open && (
searchInputAriaLabel={translate('search_verb')}
clearIconAriaLabel={translate('clear')}
/>
- </PortalPopup>
+ </Popup>
</div>
);
Link,
NavBarTabLink,
NavBarTabs,
+ PopupZLevel,
} from 'design-system';
import * as React from 'react';
import Tooltip from '../../../../components/controls/Tooltip';
data-test="administration"
id="component-navigation-admin"
size="auto"
+ zLevel={PopupZLevel.Global}
overlay={adminLinks}
>
{({ onToggleClick, open, a11yAttrs }) => (
data-test="extensions"
id="component-navigation-more"
size="auto"
+ zLevel={PopupZLevel.Global}
overlay={withoutSecurityExtension.map((e) => this.renderExtension(e, false, query))}
>
{({ onToggleClick, open, a11yAttrs }) => (
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ButtonSecondary, PopupPlacement, PopupZLevel, PortalPopup } from 'design-system';
+import { ButtonSecondary, Popup, PopupPlacement, PopupZLevel } from 'design-system';
import * as React from 'react';
import EscKeydownHandler from '../../../../../components/controls/EscKeydownHandler';
import OutsideClickHandler from '../../../../../components/controls/OutsideClickHandler';
setIsMenuOpen(false);
}}
>
- <PortalPopup
+ <Popup
allowResizing={true}
overlay={
isMenuOpen && (
>
{currentBranchLikeElement}
</ButtonSecondary>
- </PortalPopup>
+ </Popup>
</OutsideClickHandler>
</EscKeydownHandler>