/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import * as React from "react"; import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { Popover, PopoverPosition, PopoverProps } from "@patternfly/react-core/dist/js/components/Popover"; import { useBoxedExpressionEditor } from "../../BoxedExpressionEditorContext"; import { NavigationKeysUtils } from "../../keysUtils/keyUtils"; import { generateUuid } from "../../api"; import "./PopoverMenu.css"; export interface PopoverMenuProps { /** Optional children element to be considered for triggering the popover */ children?: React.ReactElement; /** A function which returns the HTMLElement where the popover's arrow should be placed */ arrowPlacement?: () => HTMLElement; /** The content of the popover itself */ body: React.ReactNode; /** The node where to append the popover content */ appendTo?: HTMLElement | ((ref?: HTMLElement) => HTMLElement); /** Additional classname to be used for the popover */ className?: string; /** True to have width automatically computed */ hasAutoWidth?: boolean; /** Popover min width */ minWidth?: string; /** * True to show the popover programmatically. */ isVisible?: boolean | null; /** * Lifecycle function invoked when the popover has fully transitioned out, called when the user click outside the popover. */ onHidden?: () => void; /** * Lifecycle function invoked when the popover has fully transitioned out, called when the user press "Esc" key. */ onCancel?: (event?: MouseEvent | KeyboardEvent) => void; /** * Lifecycle function invoked when the popover has fully transitioned in. */ onShown?: () => void; position?: PopoverPosition; distance?: number; } export interface PopoverMenuRef { /** * set the visibility of the popover */ setIsVisible: (isVisible: boolean) => void; } const POPUP_DROP_DOWN_RESERVED_HEIGHT = 200; const POPUP_DEFAULT_HEIGHT = 200; export const PopoverMenu = React.forwardRef( ( { children, arrowPlacement, body, position, distance, appendTo, className, hasAutoWidth, minWidth = `var(--pf-v5-c-popover--MinWidth)`, onHidden = () => {}, onCancel = () => {}, onShown = () => {}, }: PopoverMenuProps, ref ) => { const { currentlyOpenContextMenu, setCurrentlyOpenContextMenu } = useBoxedExpressionEditor(); const [isPopoverVisible, setIsPopoverVisible] = useState(false); const id = useMemo(() => generateUuid(), []); useEffect(() => { setIsPopoverVisible(currentlyOpenContextMenu == id); }, [id, currentlyOpenContextMenu]); const onPopoverShown = useCallback(() => { setCurrentlyOpenContextMenu(id); onShown(); }, [setCurrentlyOpenContextMenu, id, onShown]); const shouldOpen: PopoverProps["shouldOpen"] = useCallback((_event, showFunction) => { showFunction?.(); }, []); const shouldClose: PopoverProps["shouldClose"] = useCallback( (event, hideFunction): void => { if (event instanceof KeyboardEvent && NavigationKeysUtils.isEsc(event.key)) { onCancel(event); } else { hideFunction?.(); } }, [onCancel] ); const onHiddenCallback: PopoverProps["onHidden"] = useCallback((): void => { // This validation is to prevent this code of being called twice, because if the user clicks outside the // Boxed Expression component the onHidden() is called again by the Popover which is listen to clicks // on the document to close all opened popups. if (currentlyOpenContextMenu) { onHidden(); setCurrentlyOpenContextMenu(undefined); } }, [currentlyOpenContextMenu, onHidden, setCurrentlyOpenContextMenu]); useImperativeHandle( ref, (): PopoverMenuRef => ({ setIsVisible: (isVisible: boolean) => { setCurrentlyOpenContextMenu(isVisible ? id : undefined); }, }) ); const appendElement = useMemo(() => { if (appendTo instanceof HTMLElement) { return appendTo; } else if (appendTo) { return appendTo(); } }, [appendTo]); const yPos = appendElement?.getBoundingClientRect().top ?? 0; const popupPosition = useMemo(() => { if (appendElement) { const availableHeight = document.documentElement.clientHeight; if (POPUP_DEFAULT_HEIGHT + yPos + POPUP_DROP_DOWN_RESERVED_HEIGHT > availableHeight) { return PopoverPosition.right; } } return PopoverPosition.bottom; }, [appendElement, yPos]); return ( } bodyContent={body} isVisible={isPopoverVisible} onShown={onPopoverShown} onHidden={onHiddenCallback} shouldClose={shouldClose} shouldOpen={shouldOpen} flipBehavior={["bottom-start", "bottom", "bottom-end", "right-start", "left-start", "right-end", "left-end"]} > {children} ); } );