refactor(Popover): Upgrade Popover to Antd5 (#31973)

Co-authored-by: Geido <60598000+geido@users.noreply.github.com>
This commit is contained in:
Alexandru Soare 2025-02-10 16:38:17 +02:00 committed by GitHub
parent 06f8f8e608
commit 0030f46d2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 157 additions and 228 deletions

View File

@ -161,7 +161,7 @@ describe('Horizontal FilterBar', () => {
cy.getBySel('filter-control-name') cy.getBySel('filter-control-name')
.contains('test_12') .contains('test_12')
.should('not.be.visible'); .should('not.be.visible');
cy.get('.ant-popover-inner-content').scrollTo('bottom'); cy.get('.antd5-popover-inner').scrollTo('bottom');
cy.getBySel('filter-control-name').contains('test_12').should('be.visible'); cy.getBySel('filter-control-name').contains('test_12').should('be.visible');
}); });
@ -226,7 +226,7 @@ describe('Horizontal FilterBar', () => {
cy.getBySel('slice-header').within(() => { cy.getBySel('slice-header').within(() => {
cy.get('.filter-counts').trigger('mouseover'); cy.get('.filter-counts').trigger('mouseover');
}); });
cy.get('.filterStatusPopover').contains('test_9').click(); cy.getBySel('filter-status-popover').contains('test_9').click();
cy.getBySel('dropdown-content').should('be.visible'); cy.getBySel('dropdown-content').should('be.visible');
cy.get('.ant-select-focused').should('be.visible'); cy.get('.ant-select-focused').should('be.visible');
}); });

View File

@ -456,19 +456,19 @@ export function applyAdvancedTimeRangeFilterOnDashboard(
endRange?: string, endRange?: string,
) { ) {
cy.get('.control-label').contains('RANGE TYPE').should('be.visible'); cy.get('.control-label').contains('RANGE TYPE').should('be.visible');
cy.get('.ant-popover-content .ant-select-selector') cy.get('.antd5-popover-content .ant-select-selector')
.should('be.visible') .should('be.visible')
.click(); .click();
cy.get(`[label="Advanced"]`).should('be.visible').click(); cy.get(`[label="Advanced"]`).should('be.visible').click();
cy.get('.section-title').contains('Advanced Time Range').should('be.visible'); cy.get('.section-title').contains('Advanced Time Range').should('be.visible');
if (startRange) { if (startRange) {
cy.get('.ant-popover-inner-content') cy.get('.antd5-popover-inner-content')
.find('[class^=ant-input]') .find('[class^=ant-input]')
.first() .first()
.type(`${startRange}`); .type(`${startRange}`);
} }
if (endRange) { if (endRange) {
cy.get('.ant-popover-inner-content') cy.get('.antd5-popover-inner-content')
.find('[class^=ant-input]') .find('[class^=ant-input]')
.last() .last()
.type(`${endRange}`); .type(`${endRange}`);

View File

@ -555,7 +555,7 @@ export const exploreView = {
timeSection: { timeSection: {
timeRangeFilter: dataTestLocator('time-range-trigger'), timeRangeFilter: dataTestLocator('time-range-trigger'),
timeRangeFilterModal: { timeRangeFilterModal: {
container: '.ant-popover-content', container: '.antd5-popover-content',
footer: '.footer', footer: '.footer',
cancelButton: dataTestLocator('cancel-button'), cancelButton: dataTestLocator('cancel-button'),
configureLastTimeRange: { configureLastTimeRange: {

View File

@ -17,9 +17,9 @@
* under the License. * under the License.
*/ */
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Popover } from 'antd'; import { Popover } from 'antd-v5';
import type ReactAce from 'react-ace'; import type ReactAce from 'react-ace';
import type { PopoverProps } from 'antd/lib/popover'; import type { PopoverProps } from 'antd-v5/lib/popover';
import { CalculatorOutlined } from '@ant-design/icons'; import { CalculatorOutlined } from '@ant-design/icons';
import { css, styled, useTheme, t } from '@superset-ui/core'; import { css, styled, useTheme, t } from '@superset-ui/core';
@ -72,7 +72,7 @@ export const SQLPopover = (props: PopoverProps & { sqlExpression: string }) => {
/> />
} }
placement="bottomLeft" placement="bottomLeft"
arrowPointAtCenter arrow={{ pointAtCenter: true }}
title={t('SQL expression')} title={t('SQL expression')}
{...props} {...props}
> >

View File

@ -39,14 +39,18 @@ export const GlobalStyles = () => (
.echarts-tooltip[style*='visibility: hidden'] { .echarts-tooltip[style*='visibility: hidden'] {
display: none !important; display: none !important;
} }
// Ant Design is applying inline z-index styles causing troubles // Ant Design is applying inline z-index styles causing troubles
// TODO: Remove z-indexes when Ant Design is fully upgraded to v5 // TODO: Remove z-indexes when Ant Design is fully upgraded to v5
// Prefer vanilla Ant Design z-indexes that should work out of the box // Prefer vanilla Ant Design z-indexes that should work out of the box
.ant-popover, .antd5-dropdown,
.ant-dropdown,
.ant-select-dropdown, .ant-select-dropdown,
.antd5-modal-wrap, .antd5-modal-wrap,
.antd5-modal-mask, .antd5-modal-mask,
.antd5-picker-dropdown { .antd5-picker-dropdown,
.ant-popover,
.antd5-popover {
z-index: ${theme.zIndex.max} !important; z-index: ${theme.zIndex.max} !important;
} }

View File

@ -137,7 +137,6 @@ const DropdownContainer = forwardRef(
const { current } = ref; const { current } = ref;
const [itemsWidth, setItemsWidth] = useState<number[]>([]); const [itemsWidth, setItemsWidth] = useState<number[]>([]);
const [popoverVisible, setPopoverVisible] = useState(false); const [popoverVisible, setPopoverVisible] = useState(false);
// We use React.useState to be able to mock the state in Jest // We use React.useState to be able to mock the state in Jest
const [overflowingIndex, setOverflowingIndex] = useState<number>(-1); const [overflowingIndex, setOverflowingIndex] = useState<number>(-1);
@ -181,11 +180,13 @@ const DropdownContainer = forwardRef(
); );
useLayoutEffect(() => { useLayoutEffect(() => {
if (popoverVisible) {
return;
}
const container = current?.children.item(0); const container = current?.children.item(0);
if (container) { if (container) {
const { children } = container; const { children } = container;
const childrenArray = Array.from(children); const childrenArray = Array.from(children);
// If items length change, add all items to the container // If items length change, add all items to the container
// and recalculate the widths // and recalculate the widths
if (itemsWidth.length !== items.length) { if (itemsWidth.length !== items.length) {
@ -341,11 +342,7 @@ const DropdownContainer = forwardRef(
<> <>
<Global <Global
styles={css` styles={css`
.ant-popover-inner-content { .antd5-popover-inner {
max-height: ${MAX_HEIGHT}px;
overflow: ${showOverflow ? 'auto' : 'visible'};
padding: ${theme.gridUnit * 3}px ${theme.gridUnit * 4}px;
// Some OS versions only show the scroll when hovering. // Some OS versions only show the scroll when hovering.
// These settings will make the scroll always visible. // These settings will make the scroll always visible.
::-webkit-scrollbar { ::-webkit-scrollbar {
@ -365,11 +362,16 @@ const DropdownContainer = forwardRef(
} }
`} `}
/> />
<Popover <Popover
overlayInnerStyle={{
maxHeight: `${MAX_HEIGHT}px`,
overflow: showOverflow ? 'auto' : 'visible',
}}
content={popoverContent} content={popoverContent}
trigger="click" trigger="click"
visible={popoverVisible} open={popoverVisible}
onVisibleChange={visible => setPopoverVisible(visible)} onOpenChange={visible => setPopoverVisible(visible)}
placement="bottom" placement="bottom"
forceRender={forceRender} forceRender={forceRender}
> >

View File

@ -17,8 +17,7 @@
* under the License. * under the License.
*/ */
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import { PopoverProps } from 'antd/lib/popover'; import Popover, { PopoverProps } from 'src/components/Popover';
import Popover from '.';
export default { export default {
title: 'Popover', title: 'Popover',
@ -66,6 +65,8 @@ const TRIGGERS = {
InteractivePopover.args = { InteractivePopover.args = {
content: 'Popover sample content', content: 'Popover sample content',
title: 'Popover title', title: 'Popover title',
arrow: true,
color: '#fff',
}; };
InteractivePopover.argTypes = { InteractivePopover.argTypes = {
@ -79,4 +80,14 @@ InteractivePopover.argTypes = {
control: { type: 'select' }, control: { type: 'select' },
options: TRIGGERS.options, options: TRIGGERS.options,
}, },
arrow: {
name: 'arrow',
control: { type: 'boolean' },
description: "Change arrow's visible state",
},
color: {
name: 'color',
control: { type: 'color' },
description: 'The background color of the popover.',
},
}; };

View File

@ -21,7 +21,7 @@ import userEvent from '@testing-library/user-event';
import { supersetTheme } from '@superset-ui/core'; import { supersetTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import Popover from '.'; import Popover from 'src/components/Popover';
test('should render', () => { test('should render', () => {
const { container } = render(<Popover />); const { container } = render(<Popover />);
@ -29,12 +29,12 @@ test('should render', () => {
}); });
test('should render a title when visible', () => { test('should render a title when visible', () => {
render(<Popover title="Popover title" visible />); render(<Popover title="Popover title" open />);
expect(screen.getByText('Popover title')).toBeInTheDocument(); expect(screen.getByText('Popover title')).toBeInTheDocument();
}); });
test('should render some content when visible', () => { test('should render some content when visible', () => {
render(<Popover content="Content sample" visible />); render(<Popover content="Content sample" open />);
expect(screen.getByText('Content sample')).toBeInTheDocument(); expect(screen.getByText('Content sample')).toBeInTheDocument();
}); });
@ -61,22 +61,22 @@ test('renders with icon child', async () => {
}); });
test('fires an event when visibility is changed', async () => { test('fires an event when visibility is changed', async () => {
const onVisibleChange = jest.fn(); const onOpenChange = jest.fn();
render( render(
<Popover <Popover
content="Content sample" content="Content sample"
title="Popover title" title="Popover title"
onVisibleChange={onVisibleChange} onOpenChange={onOpenChange}
> >
<Button>Hover me</Button> <Button>Hover me</Button>
</Popover>, </Popover>,
); );
userEvent.hover(screen.getByRole('button')); userEvent.hover(screen.getByRole('button'));
await waitFor(() => expect(onVisibleChange).toHaveBeenCalledTimes(1)); await waitFor(() => expect(onOpenChange).toHaveBeenCalledTimes(1));
}); });
test('renders with theme', () => { test('renders with theme', () => {
render(<Popover content="Content sample" title="Popover title" visible />); render(<Popover content="Content sample" title="Popover title" open />);
const title = screen.getByText('Popover title'); const title = screen.getByText('Popover title');
expect(title).toHaveStyle({ expect(title).toHaveStyle({
fontSize: supersetTheme.gridUnit * 3.5, fontSize: supersetTheme.gridUnit * 3.5,

View File

@ -1,27 +0,0 @@
/**
* 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 { Popover as AntdPopover } from 'antd';
import type { PopoverProps as AntdPopoverProps } from 'antd/lib/popover';
export interface PopoverProps extends AntdPopoverProps {
forceRender?: boolean;
}
export const Popover = (props: PopoverProps) => <AntdPopover {...props} />;

View File

@ -16,9 +16,13 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
export type { PopoverProps } from 'antd/lib/popover'; import { Popover as AntdPopover } from 'antd-v5';
export type { TooltipPlacement } from 'antd/lib/tooltip'; import { PopoverProps as AntdPopoverProps } from 'antd-v5/lib/popover';
// Eventually Popover can be wrapped and customized in this file export interface PopoverProps extends AntdPopoverProps {
// for now we're just redirecting forceRender?: boolean;
export { Popover as default } from './Popover'; }
const Popover = (props: PopoverProps) => <AntdPopover {...props} />;
export default Popover;

View File

@ -44,7 +44,7 @@ function HeaderWithRadioGroup(props: HeaderWithRadioGroupProps) {
> >
<Popover <Popover
trigger="click" trigger="click"
visible={popoverVisible} open={popoverVisible}
content={ content={
<div> <div>
<div <div
@ -72,7 +72,7 @@ function HeaderWithRadioGroup(props: HeaderWithRadioGroupProps) {
</div> </div>
} }
placement="bottomLeft" placement="bottomLeft"
arrowPointAtCenter arrow={{ pointAtCenter: true }}
> >
<Icons.SettingOutlined <Icons.SettingOutlined
iconSize="m" iconSize="m"

View File

@ -30,7 +30,6 @@ import {
useTheme, useTheme,
useElementOnScreen, useElementOnScreen,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { Global } from '@emotion/react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import ErrorBoundary from 'src/components/ErrorBoundary'; import ErrorBoundary from 'src/components/ErrorBoundary';
import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane'; import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane';
@ -653,13 +652,6 @@ const DashboardBuilder = () => {
</Droppable> </Droppable>
</StyledHeader> </StyledHeader>
<StyledContent fullSizeChartId={fullSizeChartId}> <StyledContent fullSizeChartId={fullSizeChartId}>
<Global
styles={css`
// @z-index-above-dashboard-header (100) + 1 = 101
${fullSizeChartId &&
`div > .filterStatusPopover.ant-popover{z-index: 101}`}
`}
/>
{!editMode && {!editMode &&
!topLevelTabs && !topLevelTabs &&
dashboardLayout[DASHBOARD_GRID_ID]?.children?.length === 0 && ( dashboardLayout[DASHBOARD_GRID_ID]?.children?.length === 0 && (

View File

@ -19,8 +19,7 @@
import { RefObject, useEffect, useRef, KeyboardEvent } from 'react'; import { RefObject, useEffect, useRef, KeyboardEvent } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Global, css } from '@emotion/react'; import { t, useTheme } from '@superset-ui/core';
import { t } from '@superset-ui/core';
import Popover from 'src/components/Popover'; import Popover from 'src/components/Popover';
import { import {
FiltersContainer, FiltersContainer,
@ -120,7 +119,7 @@ const DetailsPanelPopover = ({
const indicatorKey = (indicator: Indicator): string => const indicatorKey = (indicator: Indicator): string =>
`${indicator.column} - ${indicator.name}`; `${indicator.column} - ${indicator.name}`;
const theme = useTheme();
const content = ( const content = (
<FiltersDetailsContainer <FiltersDetailsContainer
ref={popoverContentRef} ref={popoverContentRef}
@ -129,54 +128,6 @@ const DetailsPanelPopover = ({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
role="menu" role="menu"
> >
<Global
styles={theme => css`
.filterStatusPopover {
.ant-popover-inner {
background-color: ${theme.colors.grayscale.dark2}cc;
.ant-popover-inner-content {
padding: ${theme.gridUnit * 2}px;
}
}
&.ant-popover-placement-bottom,
&.ant-popover-placement-bottomLeft,
&.ant-popover-placement-bottomRight {
& > .ant-popover-content > .ant-popover-arrow {
border-top-color: ${theme.colors.grayscale.dark2}cc;
border-left-color: ${theme.colors.grayscale.dark2}cc;
}
}
&.ant-popover-placement-top,
&.ant-popover-placement-topLeft,
&.ant-popover-placement-topRight {
& > .ant-popover-content > .ant-popover-arrow {
border-bottom-color: ${theme.colors.grayscale.dark2}cc;
border-right-color: ${theme.colors.grayscale.dark2}cc;
}
}
&.ant-popover-placement-left,
&.ant-popover-placement-leftTop,
&.ant-popover-placement-leftBottom {
& > .ant-popover-content > .ant-popover-arrow {
border-top-color: ${theme.colors.grayscale.dark2}cc;
border-right-color: ${theme.colors.grayscale.dark2}cc;
}
}
&.ant-popover-placement-right,
&.ant-popover-placement-rightTop,
&.ant-popover-placement-rightBottom {
& > .ant-popover-content > .ant-popover-arrow {
border-bottom-color: ${theme.colors.grayscale.dark2}cc;
border-left-color: ${theme.colors.grayscale.dark2}cc;
}
}
&.ant-popover {
color: ${theme.colors.grayscale.light4};
z-index: 99;
}
}
`}
/>
<div> <div>
{appliedCrossFilterIndicators.length ? ( {appliedCrossFilterIndicators.length ? (
<div> <div>
@ -224,12 +175,13 @@ const DetailsPanelPopover = ({
return ( return (
<Popover <Popover
overlayClassName="filterStatusPopover" color={`${theme.colors.grayscale.dark2}cc`}
content={content} content={content}
visible={popoverVisible} open={popoverVisible}
onVisibleChange={handleVisibility} onOpenChange={handleVisibility}
placement="bottomRight" placement="bottomRight"
trigger={['hover']} trigger={['hover']}
data-test="filter-status-popover"
> >
{children} {children}
</Popover> </Popover>

View File

@ -30,7 +30,6 @@ export const FilterCard = ({
placement, placement,
}: FilterCardProps) => { }: FilterCardProps) => {
const [internalIsVisible, setInternalIsVisible] = useState(false); const [internalIsVisible, setInternalIsVisible] = useState(false);
const hidePopover = () => { const hidePopover = () => {
setInternalIsVisible(false); setInternalIsVisible(false);
}; };
@ -43,15 +42,18 @@ export const FilterCard = ({
return ( return (
<Popover <Popover
placement={placement} placement={placement}
overlayClassName="filter-card-popover" overlayStyle={{
width: '240px',
}}
mouseEnterDelay={0.2} mouseEnterDelay={0.2}
mouseLeaveDelay={0.2} mouseLeaveDelay={0.2}
onVisibleChange={visible => { onOpenChange={visible => {
setInternalIsVisible(externalIsVisible && visible); setInternalIsVisible(externalIsVisible && visible);
}} }}
visible={externalIsVisible && internalIsVisible} open={externalIsVisible && internalIsVisible}
content={<FilterCardContent filter={filter} hidePopover={hidePopover} />} content={<FilterCardContent filter={filter} hidePopover={hidePopover} />}
getPopupContainer={getPopupContainer ?? (() => document.body)} getPopupContainer={getPopupContainer ?? (() => document.body)}
arrow={false}
> >
{children} {children}
</Popover> </Popover>

View File

@ -66,32 +66,6 @@ export const chartHeaderStyles = (theme: SupersetTheme) => css`
`; `;
export const filterCardPopoverStyle = (theme: SupersetTheme) => css` export const filterCardPopoverStyle = (theme: SupersetTheme) => css`
.filter-card-popover {
width: 240px;
padding: 0;
border-radius: 4px;
&.ant-popover-placement-bottom {
padding-top: ${theme.gridUnit}px;
}
&.ant-popover-placement-left {
padding-right: ${theme.gridUnit * 3}px;
}
.ant-popover-inner {
box-shadow: 0 0 8px rgb(0 0 0 / 10%);
}
.ant-popover-inner-content {
padding: ${theme.gridUnit * 4}px;
}
.ant-popover-arrow {
display: none;
}
}
.filter-card-tooltip { .filter-card-tooltip {
&.antd5-tooltip-placement-bottom { &.antd5-tooltip-placement-bottom {
padding-top: 0; padding-top: 0;

View File

@ -27,7 +27,6 @@ import {
TimeFormats, TimeFormats,
useTheme, useTheme,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { Global } from '@emotion/react';
import { Column } from 'react-table'; import { Column } from 'react-table';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { Input } from 'src/components/Input'; import { Input } from 'src/components/Input';
@ -195,13 +194,6 @@ const DataTableTemporalHeaderCell = ({
datasourceId ? ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions datasourceId ? ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
<FormatPickerContainer onClick={e => e.stopPropagation()}> <FormatPickerContainer onClick={e => e.stopPropagation()}>
{/* hack to disable click propagation from popover content to table header, which triggers sorting column */} {/* hack to disable click propagation from popover content to table header, which triggers sorting column */}
<Global
styles={css`
.column-formatting-popover .ant-popover-inner-content {
padding: 0;
}
`}
/>
<FormatPickerLabel>{t('Column Formatting')}</FormatPickerLabel> <FormatPickerLabel>{t('Column Formatting')}</FormatPickerLabel>
<FormatPicker <FormatPicker
onChange={onChange} onChange={onChange}
@ -219,11 +211,10 @@ const DataTableTemporalHeaderCell = ({
return datasourceId ? ( return datasourceId ? (
<span> <span>
<Popover <Popover
overlayClassName="column-formatting-popover"
trigger="click" trigger="click"
content={overlayContent} content={overlayContent}
placement="bottomLeft" placement="bottomLeft"
arrowPointAtCenter arrow={{ pointAtCenter: true }}
> >
<Icons.SettingOutlined <Icons.SettingOutlined
iconSize="m" iconSize="m"

View File

@ -231,8 +231,8 @@ class AnnotationLayerControl extends PureComponent<Props, PopoverState> {
anno, anno,
this.props.annotationError[anno.name], this.props.annotationError[anno.name],
)} )}
visible={this.state.popoverVisible[i]} open={this.state.popoverVisible[i]}
onVisibleChange={visible => this.handleVisibleChange(visible, i)} onOpenChange={visible => this.handleVisibleChange(visible, i)}
> >
<CustomListItem selectable> <CustomListItem selectable>
<span>{anno.name}</span> <span>{anno.name}</span>
@ -254,9 +254,9 @@ class AnnotationLayerControl extends PureComponent<Props, PopoverState> {
'', '',
)} )}
title={t('Add annotation layer')} title={t('Add annotation layer')}
visible={this.state.popoverVisible[addLayerPopoverKey]} open={this.state.popoverVisible[addLayerPopoverKey]}
destroyTooltipOnHide destroyTooltipOnHide
onVisibleChange={visible => onOpenChange={visible =>
this.handleVisibleChange(visible, addLayerPopoverKey) this.handleVisibleChange(visible, addLayerPopoverKey)
} }
> >

View File

@ -51,8 +51,8 @@ export const FormattingPopover = ({
extraColorChoices={extraColorChoices} extraColorChoices={extraColorChoices}
/> />
} }
visible={visible} open={visible}
onVisibleChange={setVisible} onOpenChange={setVisible}
trigger={['click']} trigger={['click']}
overlayStyle={{ width: '450px' }} overlayStyle={{ width: '450px' }}
{...props} {...props}

View File

@ -18,7 +18,7 @@
*/ */
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { PopoverProps } from 'antd/lib/popover'; import { PopoverProps } from 'src/components/Popover';
import { Comparator, ControlComponentProps } from '@superset-ui/chart-controls'; import { Comparator, ControlComponentProps } from '@superset-ui/chart-controls';
export type ConditionalFormattingConfig = { export type ConditionalFormattingConfig = {

View File

@ -47,9 +47,9 @@ const ContourPopoverTrigger = ({
<ControlPopover <ControlPopover
trigger="click" trigger="click"
content={popoverContent} content={popoverContent}
defaultVisible={visible} defaultOpen={visible}
visible={visible} open={visible}
onVisibleChange={setVisibility} onOpenChange={setVisibility}
destroyTooltipOnHide destroyTooltipOnHide
> >
{props.children} {props.children}

View File

@ -88,7 +88,7 @@ test('Should lock the vertical scroll when the popover is visible', () => {
test('Should place popover at the top', async () => { test('Should place popover at the top', async () => {
const { setStateMock } = setupTest({ const { setStateMock } = setupTest({
...createProps(), ...createProps(),
getVisibilityRatio: () => 0.2, getVisibilityRatio: () => ({ yRatio: 0.2, xRatio: 0.3 }),
}); });
expect(screen.getByTestId('control-popover')).toBeInTheDocument(); expect(screen.getByTestId('control-popover')).toBeInTheDocument();
@ -102,21 +102,21 @@ test('Should place popover at the top', async () => {
test('Should place popover at the center', async () => { test('Should place popover at the center', async () => {
const { setStateMock } = setupTest({ const { setStateMock } = setupTest({
...createProps(), ...createProps(),
getVisibilityRatio: () => 0.5, getVisibilityRatio: () => ({ yRatio: 0.5, xRatio: 0.7 }),
}); });
expect(screen.getByTestId('control-popover')).toBeInTheDocument(); expect(screen.getByTestId('control-popover')).toBeInTheDocument();
userEvent.click(screen.getByTestId('control-popover')); userEvent.click(screen.getByTestId('control-popover'));
await waitFor(() => { await waitFor(() => {
expect(setStateMock).toHaveBeenCalledWith('right'); expect(setStateMock).toHaveBeenCalledWith('left');
}); });
}); });
test('Should place popover at the bottom', async () => { test('Should place popover at the bottom', async () => {
const { setStateMock } = setupTest({ const { setStateMock } = setupTest({
...createProps(), ...createProps(),
getVisibilityRatio: () => 0.7, getVisibilityRatio: () => ({ yRatio: 0.9, xRatio: 0.2 }),
}); });
expect(screen.getByTestId('control-popover')).toBeInTheDocument(); expect(screen.getByTestId('control-popover')).toBeInTheDocument();
@ -162,7 +162,7 @@ test('Controlled mode', async () => {
const baseProps = { const baseProps = {
...createProps(), ...createProps(),
destroyTooltipOnHide: true, destroyTooltipOnHide: true,
visible: false, open: false,
}; };
const { rerender } = setupTest(baseProps); const { rerender } = setupTest(baseProps);
@ -170,7 +170,7 @@ test('Controlled mode', async () => {
expect(screen.getByTestId('control-popover')).toBeInTheDocument(); expect(screen.getByTestId('control-popover')).toBeInTheDocument();
expect(screen.queryByText('Control Popover Test')).not.toBeInTheDocument(); expect(screen.queryByText('Control Popover Test')).not.toBeInTheDocument();
rerender(<TestComponent {...baseProps} visible />); rerender(<TestComponent {...baseProps} open />);
expect(await screen.findByText('Control Popover Test')).toBeInTheDocument(); expect(await screen.findByText('Control Popover Test')).toBeInTheDocument();
rerender(<TestComponent {...baseProps} />); rerender(<TestComponent {...baseProps} />);

View File

@ -21,49 +21,70 @@ import React, { FC, useCallback, useRef, useEffect, useState } from 'react';
import Popover, { import Popover, {
PopoverProps as BasePopoverProps, PopoverProps as BasePopoverProps,
TooltipPlacement,
} from 'src/components/Popover'; } from 'src/components/Popover';
import { TooltipPlacement } from 'src/components/Tooltip';
const sectionContainerId = 'controlSections'; const sectionContainerId = 'controlSections';
export const getSectionContainerElement = () => export const getSectionContainerElement = () =>
document.getElementById(sectionContainerId)?.lastElementChild as HTMLElement; document.getElementById(sectionContainerId)?.lastElementChild as HTMLElement;
const getElementYVisibilityRatioOnContainer = (node: HTMLElement) => { const getElementVisibilityRatio = (node?: HTMLElement) => {
const containerHeight = window?.innerHeight; const containerHeight = window?.innerHeight;
const nodePositionInViewport = node?.getBoundingClientRect()?.top; const containerWidth = window?.innerWidth;
if (!containerHeight || !nodePositionInViewport) {
return 0; const rect = node?.getBoundingClientRect();
if (!containerHeight || !containerWidth || !rect?.top) {
return { yRatio: 0, xRatio: 0 };
} }
return nodePositionInViewport / containerHeight; const yRatio = rect.top / containerHeight;
const xRatio = rect.left / containerWidth;
return { yRatio, xRatio };
}; };
export type PopoverProps = BasePopoverProps & { export type PopoverProps = BasePopoverProps & {
getVisibilityRatio?: typeof getElementYVisibilityRatioOnContainer; getVisibilityRatio?: typeof getElementVisibilityRatio;
}; };
const ControlPopover: FC<PopoverProps> = ({ const ControlPopover: FC<PopoverProps> = ({
getPopupContainer, getPopupContainer,
getVisibilityRatio = getElementYVisibilityRatioOnContainer, getVisibilityRatio = getElementVisibilityRatio,
visible: visibleProp, open: visibleProp,
destroyTooltipOnHide = false, destroyTooltipOnHide = false,
placement: initialPlacement = 'right',
...props ...props
}) => { }) => {
const triggerElementRef = useRef<HTMLElement>(); const triggerElementRef = useRef<HTMLElement>();
const [visible, setVisible] = useState( const [visible, setVisible] = useState(
visibleProp === undefined ? props.defaultVisible : visibleProp, visibleProp === undefined ? props.defaultOpen : visibleProp,
); );
const [placement, setPlacement] = React.useState<TooltipPlacement>('right'); const [placement, setPlacement] =
React.useState<TooltipPlacement>(initialPlacement);
const calculatePlacement = useCallback(() => { const calculatePlacement = useCallback(() => {
const visibilityRatio = getVisibilityRatio(triggerElementRef.current!); if (!triggerElementRef.current) return;
if (visibilityRatio < 0.35 && placement !== 'rightTop') {
setPlacement('rightTop'); const { yRatio, xRatio } = getVisibilityRatio(triggerElementRef.current);
} else if (visibilityRatio > 0.65 && placement !== 'rightBottom') {
setPlacement('rightBottom'); const horizontalPlacement =
} else { xRatio < 0.35 ? 'right' : xRatio > 0.65 ? 'left' : '';
setPlacement('right');
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]); }, [getVisibilityRatio]);
@ -97,7 +118,7 @@ const ControlPopover: FC<PopoverProps> = ({
} }
setVisible(!!visible); setVisible(!!visible);
props.onVisibleChange?.(!!visible); props.onOpenChange?.(!!visible);
}, },
[props, changeContainerScrollStatus], [props, changeContainerScrollStatus],
); );
@ -106,7 +127,7 @@ const ControlPopover: FC<PopoverProps> = ({
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
setVisible(false); setVisible(false);
props.onVisibleChange?.(false); props.onOpenChange?.(false);
} }
}, },
[props], [props],
@ -143,10 +164,10 @@ const ControlPopover: FC<PopoverProps> = ({
return ( return (
<Popover <Popover
{...props} {...props}
visible={visible} open={visible}
arrowPointAtCenter arrow={{ pointAtCenter: true }}
placement={placement} placement={placement}
onVisibleChange={handleOnVisibleChange} onOpenChange={handleOnVisibleChange}
getPopupContainer={handleGetPopupContainer} getPopupContainer={handleGetPopupContainer}
destroyTooltipOnHide={destroyTooltipOnHide} destroyTooltipOnHide={destroyTooltipOnHide}
/> />

View File

@ -355,23 +355,24 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
<span className="text">{t('Edit time range')}</span> <span className="text">{t('Edit time range')}</span>
</IconWrapper> </IconWrapper>
); );
const popoverContent = ( const popoverContent = (
<ControlPopover <ControlPopover
placement="right" autoAdjustOverflow={false}
trigger="click" trigger="click"
placement="right"
content={overlayContent} content={overlayContent}
title={title} title={title}
defaultVisible={show} defaultOpen={show}
visible={show} open={show}
onVisibleChange={toggleOverlay} onOpenChange={toggleOverlay}
overlayStyle={{ width: '600px' }} overlayStyle={{ width: '600px' }}
getPopupContainer={triggerNode => destroyTooltipOnHide
getPopupContainer={nodeTrigger =>
isOverflowingFilterBar isOverflowingFilterBar
? (triggerNode.parentNode as HTMLElement) ? (nodeTrigger.parentNode as HTMLElement)
: document.body : document.body
} }
destroyTooltipOnHide overlayClassName="time-range-popover"
> >
<Tooltip placement="top" title={tooltipTitle}> <Tooltip placement="top" title={tooltipTitle}>
<DateLabel <DateLabel

View File

@ -165,9 +165,9 @@ const ColumnSelectPopoverTrigger = ({
<ControlPopover <ControlPopover
trigger="click" trigger="click"
content={overlayContent} content={overlayContent}
defaultVisible={visible} defaultOpen={visible}
visible={visible} open={visible}
onVisibleChange={handleTogglePopover} onOpenChange={handleTogglePopover}
title={popoverTitle} title={popoverTitle}
destroyTooltipOnHide destroyTooltipOnHide
> >

View File

@ -106,9 +106,9 @@ class AdhocFilterPopoverTrigger extends PureComponent<
<ControlPopover <ControlPopover
trigger="click" trigger="click"
content={overlayContent} content={overlayContent}
defaultVisible={visible} defaultOpen={visible}
visible={visible} open={visible}
onVisibleChange={togglePopover} onOpenChange={togglePopover}
destroyTooltipOnHide destroyTooltipOnHide
> >
{this.props.children} {this.props.children}

View File

@ -18,7 +18,7 @@
*/ */
import { ControlHeader } from '@superset-ui/chart-controls'; import { ControlHeader } from '@superset-ui/chart-controls';
import { css, styled, t } from '@superset-ui/core'; import { css, styled, t } from '@superset-ui/core';
import { Popover } from 'antd'; import Popover from 'src/components/Popover';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { EditItem, LayerConf, LayerConfigsControlProps } from './types'; import { EditItem, LayerConf, LayerConfigsControlProps } from './types';
import LayerConfigsPopoverContent from './LayerConfigsPopoverContent'; import LayerConfigsPopoverContent from './LayerConfigsPopoverContent';
@ -160,7 +160,7 @@ export const LayerConfigsControl: FC<LayerConfigsControlProps> = ({
<div> <div>
<ControlHeader {...controlHeaderProps} /> <ControlHeader {...controlHeaderProps} />
<Popover <Popover
visible={popoverVisible} open={popoverVisible}
trigger="click" trigger="click"
title={popoverTitle} title={popoverTitle}
placement="right" placement="right"

View File

@ -18,7 +18,8 @@
*/ */
import { ControlHeader } from '@superset-ui/chart-controls'; import { ControlHeader } from '@superset-ui/chart-controls';
import { css, styled, t } from '@superset-ui/core'; import { css, styled, t } from '@superset-ui/core';
import { Button, Popover } from 'antd'; import Button from 'src/components/Button';
import Popover from 'src/components/Popover';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { mix } from 'polished'; import { mix } from 'polished';
import { MapViewConfigs, MapViewConfigsControlProps } from './types'; import { MapViewConfigs, MapViewConfigsControlProps } from './types';
@ -159,7 +160,7 @@ export const MapViewControl: FC<MapViewConfigsControlProps> = ({
{isCustomMode() && value && ( {isCustomMode() && value && (
<Popover <Popover
visible={popoverVisible} open={popoverVisible}
trigger="click" trigger="click"
title={popoverTitle} title={popoverTitle}
placement="right" placement="right"

View File

@ -17,7 +17,8 @@
* under the License. * under the License.
*/ */
import { css, styled, t } from '@superset-ui/core'; import { css, styled, t } from '@superset-ui/core';
import { Button, Form } from 'antd'; import { Form } from 'src/components/Form';
import Button from 'src/components/Button';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { mix } from 'polished'; import { mix } from 'polished';
import { MapViewConfigs, MapViewPopoverContentProps } from './types'; import { MapViewConfigs, MapViewPopoverContentProps } from './types';

View File

@ -259,9 +259,9 @@ class AdhocMetricPopoverTrigger extends PureComponent<
placement="right" placement="right"
trigger="click" trigger="click"
content={overlayContent} content={overlayContent}
defaultVisible={visible} defaultOpen={visible}
visible={visible} open={visible}
onVisibleChange={togglePopover} onOpenChange={togglePopover}
title={popoverTitle} title={popoverTitle}
destroyTooltipOnHide destroyTooltipOnHide
> >

View File

@ -361,8 +361,8 @@ export default class TimeSeriesColumnControl extends Component {
trigger="click" trigger="click"
content={this.renderPopover()} content={this.renderPopover()}
title={t('Column Configuration')} title={t('Column Configuration')}
visible={this.state.popoverVisible} open={this.state.popoverVisible}
onVisibleChange={this.onPopoverVisibleChange} onOpenChange={this.onPopoverVisibleChange}
> >
<InfoTooltipWithTrigger <InfoTooltipWithTrigger
icon="edit" icon="edit"