mirror of
https://github.com/apache/superset.git
synced 2026-05-31 21:29:19 +00:00
fix: Popovers in Explore not attached to the fields they are triggered by (#19139)
* fix: Popovers in Explore not attached to the fields they are triggered by * fix * PR comment * remove unused import
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import ControlPopover, { PopoverProps } from './ControlPopover';
|
||||
|
||||
const createProps = (): Partial<PopoverProps> => ({
|
||||
trigger: 'click',
|
||||
title: 'Control Popover Test',
|
||||
content: <span data-test="control-popover-content">Information</span>,
|
||||
});
|
||||
|
||||
const setupTest = (props: Partial<PopoverProps> = createProps()) => {
|
||||
const setStateMock = jest.fn();
|
||||
jest
|
||||
.spyOn(React, 'useState')
|
||||
.mockImplementation(((state: any) => [
|
||||
state,
|
||||
state === 'right' ? setStateMock : jest.fn(),
|
||||
]) as any);
|
||||
|
||||
const { container } = render(
|
||||
<div data-test="outer-container">
|
||||
<div id="controlSections">
|
||||
<ControlPopover {...props}>
|
||||
<span data-test="control-popover">Click me</span>
|
||||
</ControlPopover>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
return {
|
||||
props,
|
||||
container,
|
||||
setStateMock,
|
||||
};
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('Should render', () => {
|
||||
setupTest();
|
||||
expect(screen.getByTestId('control-popover')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId('control-popover'));
|
||||
expect(screen.getByText('Control Popover Test')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('control-popover-content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should lock the vertical scroll when the popup is visible', () => {
|
||||
setupTest();
|
||||
expect(screen.getByTestId('control-popover')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('outer-container')).not.toHaveStyle(
|
||||
'overflowY: hidden',
|
||||
);
|
||||
userEvent.click(screen.getByTestId('control-popover'));
|
||||
expect(screen.getByTestId('outer-container')).toHaveStyle(
|
||||
'overflowY: hidden',
|
||||
);
|
||||
userEvent.click(document.body);
|
||||
expect(screen.getByTestId('outer-container')).not.toHaveStyle(
|
||||
'overflowY: hidden',
|
||||
);
|
||||
});
|
||||
|
||||
test('Should place popup at the top', async () => {
|
||||
const { setStateMock } = setupTest({
|
||||
...createProps(),
|
||||
getVisibilityRatio: () => 0.2,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('control-popover')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId('control-popover'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setStateMock).toHaveBeenCalledWith('rightTop');
|
||||
});
|
||||
});
|
||||
|
||||
test('Should place popup at the center', async () => {
|
||||
const { setStateMock } = setupTest({
|
||||
...createProps(),
|
||||
getVisibilityRatio: () => 0.5,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('control-popover')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId('control-popover'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setStateMock).toHaveBeenCalledWith('right');
|
||||
});
|
||||
});
|
||||
|
||||
test('Should place popup at the bottom', async () => {
|
||||
const { setStateMock } = setupTest({
|
||||
...createProps(),
|
||||
getVisibilityRatio: () => 0.7,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('control-popover')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId('control-popover'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setStateMock).toHaveBeenCalledWith('rightBottom');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 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 React, { useCallback, useRef, useEffect } from 'react';
|
||||
|
||||
import Popover, {
|
||||
PopoverProps as BasePopoverProps,
|
||||
TooltipPlacement,
|
||||
} from 'src/components/Popover';
|
||||
|
||||
const sectionContainerId = 'controlSections';
|
||||
const getSectionContainerElement = () =>
|
||||
document.getElementById(sectionContainerId)?.parentElement;
|
||||
|
||||
const getElementYVisibilityRatioOnContainer = (node: HTMLElement) => {
|
||||
const containerHeight = window?.innerHeight;
|
||||
const nodePositionInViewport = node.getBoundingClientRect()?.top;
|
||||
if (!containerHeight || !nodePositionInViewport) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return nodePositionInViewport / containerHeight;
|
||||
};
|
||||
|
||||
export type PopoverProps = BasePopoverProps & {
|
||||
getVisibilityRatio?: typeof getElementYVisibilityRatioOnContainer;
|
||||
};
|
||||
|
||||
const ControlPopover: React.FC<PopoverProps> = ({
|
||||
getPopupContainer,
|
||||
getVisibilityRatio = getElementYVisibilityRatioOnContainer,
|
||||
...props
|
||||
}) => {
|
||||
const triggerElementRef = useRef<HTMLElement>();
|
||||
const [placement, setPlacement] = React.useState<TooltipPlacement>('right');
|
||||
|
||||
const calculatePlacement = useCallback(() => {
|
||||
const visibilityRatio = getVisibilityRatio(triggerElementRef.current!);
|
||||
|
||||
if (visibilityRatio < 0.35) {
|
||||
setPlacement('rightTop');
|
||||
} else if (visibilityRatio > 0.65) {
|
||||
setPlacement('rightBottom');
|
||||
} else {
|
||||
setPlacement('right');
|
||||
}
|
||||
}, [getVisibilityRatio]);
|
||||
|
||||
const changeContainerScrollStatus = useCallback(
|
||||
visible => {
|
||||
if (triggerElementRef.current && visible) {
|
||||
calculatePlacement();
|
||||
}
|
||||
|
||||
const element = getSectionContainerElement();
|
||||
if (element) {
|
||||
element.style.overflowY = visible ? 'hidden' : 'auto';
|
||||
}
|
||||
},
|
||||
[calculatePlacement],
|
||||
);
|
||||
|
||||
const handleGetPopupContainer = useCallback(
|
||||
(triggerNode: HTMLElement) => {
|
||||
triggerElementRef.current = triggerNode;
|
||||
setTimeout(() => {
|
||||
calculatePlacement();
|
||||
}, 0);
|
||||
|
||||
return getPopupContainer?.(triggerNode) || document.body;
|
||||
},
|
||||
[calculatePlacement, getPopupContainer],
|
||||
);
|
||||
|
||||
const handleOnVisibleChange = useCallback(
|
||||
(visible: boolean) => {
|
||||
if (props.visible === undefined) {
|
||||
changeContainerScrollStatus(visible);
|
||||
}
|
||||
|
||||
props.onVisibleChange?.(visible);
|
||||
},
|
||||
[props, changeContainerScrollStatus],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.visible !== undefined) {
|
||||
changeContainerScrollStatus(props.visible);
|
||||
}
|
||||
}, [props.visible, changeContainerScrollStatus]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
{...props}
|
||||
arrowPointAtCenter
|
||||
placement={placement}
|
||||
onVisibleChange={handleOnVisibleChange}
|
||||
getPopupContainer={handleGetPopupContainer}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlPopover;
|
||||
Reference in New Issue
Block a user