blob: 75159b6c7c7d7c1cf67ed49b690406c5029bec0e [file] [log] [blame]
/**
* 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.
*/
// eslint-disable-next-line no-restricted-syntax -- whole React import is required for `ControlPopover.test.tsx` Jest test passing.
import React, { FC, useCallback, useRef, useEffect, useState } from 'react';
import {
Popover,
PopoverProps as BasePopoverProps,
} from '@superset-ui/core/components';
import { TooltipPlacement } from '@superset-ui/core/components/Tooltip/types';
const sectionContainerId = 'controlSections';
export const getSectionContainerElement = () =>
document.getElementById(sectionContainerId)?.lastElementChild as HTMLElement;
const getElementVisibilityRatio = (node?: HTMLElement) => {
const containerHeight = window?.innerHeight;
const containerWidth = window?.innerWidth;
const rect = node?.getBoundingClientRect();
if (!containerHeight || !containerWidth || !rect?.top) {
return { yRatio: 0, xRatio: 0 };
}
const yRatio = rect.top / containerHeight;
const xRatio = rect.left / containerWidth;
return { yRatio, xRatio };
};
export type PopoverProps = BasePopoverProps & {
getVisibilityRatio?: typeof getElementVisibilityRatio;
};
const ControlPopover: FC<PopoverProps> = ({
getPopupContainer,
getVisibilityRatio = getElementVisibilityRatio,
open: visibleProp,
destroyTooltipOnHide = false,
placement: initialPlacement = 'right',
...props
}) => {
const triggerElementRef = useRef<HTMLElement>();
const [visible, setVisible] = useState(
visibleProp === undefined ? props.defaultOpen : visibleProp,
);
const [placement, setPlacement] =
React.useState<TooltipPlacement>(initialPlacement);
const calculatePlacement = useCallback(() => {
if (!triggerElementRef.current) return;
const { yRatio, xRatio } = getVisibilityRatio(triggerElementRef.current);
const horizontalPlacement =
xRatio < 0.35 ? 'right' : xRatio > 0.65 ? 'left' : '';
const verticalPlacement = (() => {
if (yRatio < 0.35) return horizontalPlacement ? 'top' : 'bottom';
if (yRatio > 0.65) return horizontalPlacement ? 'bottom' : 'top';
return '';
})();
const newPlacement =
((horizontalPlacement
? horizontalPlacement +
verticalPlacement.charAt(0).toUpperCase() +
verticalPlacement.slice(1)
: verticalPlacement) as TooltipPlacement) || 'left';
if (newPlacement !== placement) {
setPlacement(newPlacement);
}
}, [getVisibilityRatio]);
const changeContainerScrollStatus = useCallback(
visible => {
const element = getSectionContainerElement();
if (element) {
element.style.setProperty(
'overflow-y',
visible ? 'hidden' : 'auto',
'important',
);
}
},
[calculatePlacement],
);
const handleGetPopupContainer = useCallback(
(triggerNode: HTMLElement) => {
triggerElementRef.current = triggerNode;
return getPopupContainer?.(triggerNode) || document.body;
},
[calculatePlacement, getPopupContainer],
);
const handleOnVisibleChange = useCallback(
(visible: boolean | undefined) => {
if (visible === undefined) {
changeContainerScrollStatus(visible);
}
setVisible(!!visible);
props.onOpenChange?.(!!visible);
},
[props, changeContainerScrollStatus],
);
const handleDocumentKeyDownListener = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Escape') {
setVisible(false);
props.onOpenChange?.(false);
}
},
[props],
);
useEffect(() => {
if (visibleProp !== undefined) {
setVisible(!!visibleProp);
}
}, [visibleProp]);
useEffect(() => {
if (visible !== undefined) {
changeContainerScrollStatus(visible);
}
}, [visible, changeContainerScrollStatus]);
useEffect(() => {
if (visible) {
document.addEventListener('keydown', handleDocumentKeyDownListener);
}
return () => {
document.removeEventListener('keydown', handleDocumentKeyDownListener);
};
}, [handleDocumentKeyDownListener, visible]);
useEffect(() => {
if (visible) {
calculatePlacement();
}
}, [visible, calculatePlacement]);
return (
<Popover
{...props}
open={visible}
arrow={{ pointAtCenter: true }}
placement={placement}
onOpenChange={handleOnVisibleChange}
getPopupContainer={handleGetPopupContainer}
destroyTooltipOnHide={destroyTooltipOnHide}
/>
);
};
export default ControlPopover;