mirror of
https://github.com/apache/superset.git
synced 2026-06-11 18:49:15 +00:00
Compare commits
5 Commits
fix/chart-
...
fix/dropdo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cef74b422 | ||
|
|
55d9e13725 | ||
|
|
ec13a2bb7b | ||
|
|
e274b45bc6 | ||
|
|
bd1420cfd8 |
@@ -51,8 +51,16 @@ test('renders children with custom horizontal spacing', () => {
|
|||||||
expect(screen.getByTestId('container')).toHaveStyle('gap: 20px');
|
expect(screen.getByTestId('container')).toHaveStyle('gap: 20px');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not render a dropdown button when not overflowing', () => {
|
test('renders dropdown button when items exist even when not overflowing', () => {
|
||||||
render(<DropdownContainer items={generateItems(3)} />);
|
render(<DropdownContainer items={generateItems(3)} />);
|
||||||
|
// Button should always be visible when items exist to prevent layout shifts
|
||||||
|
expect(screen.getByText('More')).toBeInTheDocument();
|
||||||
|
// Badge should show 0 when nothing is overflowing
|
||||||
|
expect(screen.getByText('0')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not render a dropdown button when no items', () => {
|
||||||
|
render(<DropdownContainer items={[]} />);
|
||||||
expect(screen.queryByText('More')).not.toBeInTheDocument();
|
expect(screen.queryByText('More')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { t } from '@apache-superset/core/translation';
|
|||||||
import { usePrevious } from '@superset-ui/core';
|
import { usePrevious } from '@superset-ui/core';
|
||||||
import { css, useTheme } from '@apache-superset/core/theme';
|
import { css, useTheme } from '@apache-superset/core/theme';
|
||||||
import { useResizeDetector } from 'react-resize-detector';
|
import { useResizeDetector } from 'react-resize-detector';
|
||||||
import { Badge, Icons, Button, Tooltip, Popover } from '..';
|
import { Badge, Icons, Button, Popover } from '..';
|
||||||
import { DropdownContainerProps, DropdownItem, DropdownRef } from './types';
|
import { DropdownContainerProps, DropdownItem, DropdownRef } from './types';
|
||||||
|
|
||||||
const MAX_HEIGHT = 500;
|
const MAX_HEIGHT = 500;
|
||||||
@@ -72,15 +72,6 @@ export const DropdownContainer = forwardRef(
|
|||||||
|
|
||||||
const [showOverflow, setShowOverflow] = useState(false);
|
const [showOverflow, setShowOverflow] = useState(false);
|
||||||
|
|
||||||
// When the item set changes, the overflow index is briefly reset while the
|
|
||||||
// new widths are measured (see the layout effect below). During that window
|
|
||||||
// the dropdown content momentarily becomes empty, which would hide and then
|
|
||||||
// re-show the trigger, causing a flicker. We track whether a recalculation
|
|
||||||
// is pending so the trigger can stay mounted across the transient (when it
|
|
||||||
// was showing content just before) without lingering in the steady state
|
|
||||||
// when nothing actually overflows.
|
|
||||||
const [recalculating, setRecalculating] = useState(false);
|
|
||||||
|
|
||||||
// callback to update item widths so that the useLayoutEffect runs whenever
|
// callback to update item widths so that the useLayoutEffect runs whenever
|
||||||
// width of any of the child changes
|
// width of any of the child changes
|
||||||
const recalculateItemWidths = useCallback(() => {
|
const recalculateItemWidths = useCallback(() => {
|
||||||
@@ -180,7 +171,6 @@ export const DropdownContainer = forwardRef(
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setOverflowingIndex(-1);
|
setOverflowingIndex(-1);
|
||||||
setRecalculating(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +211,6 @@ export const DropdownContainer = forwardRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
setOverflowingIndex(newOverflowingIndex);
|
setOverflowingIndex(newOverflowingIndex);
|
||||||
setRecalculating(false);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
current,
|
current,
|
||||||
@@ -245,6 +234,10 @@ export const DropdownContainer = forwardRef(
|
|||||||
const overflowingCount =
|
const overflowingCount =
|
||||||
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
|
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
|
||||||
|
|
||||||
|
// Always show button when items exist to prevent layout shifts
|
||||||
|
// and ensure consistent UI even when no items are overflowing
|
||||||
|
const shouldShowButton = items.length > 0 || !!dropdownContent;
|
||||||
|
|
||||||
const popoverContent = useMemo(
|
const popoverContent = useMemo(
|
||||||
() =>
|
() =>
|
||||||
dropdownContent || overflowingCount ? (
|
dropdownContent || overflowingCount ? (
|
||||||
@@ -272,15 +265,6 @@ export const DropdownContainer = forwardRef(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// The trigger had content in the previous render if popoverContent was
|
|
||||||
// truthy then. During the brief mid-recalculation render where
|
|
||||||
// popoverContent flips to null, this still reflects the prior (non-empty)
|
|
||||||
// value, letting us keep the trigger mounted across the transient.
|
|
||||||
const hadPopoverContent = usePrevious(!!popoverContent, false);
|
|
||||||
|
|
||||||
const showDropdownButton =
|
|
||||||
!!popoverContent || (recalculating && hadPopoverContent);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (popoverVisible) {
|
if (popoverVisible) {
|
||||||
// Measures scroll height after rendering the elements
|
// Measures scroll height after rendering the elements
|
||||||
@@ -313,6 +297,44 @@ export const DropdownContainer = forwardRef(
|
|||||||
};
|
};
|
||||||
}, [popoverVisible]);
|
}, [popoverVisible]);
|
||||||
|
|
||||||
|
const triggerButton = (
|
||||||
|
<Button
|
||||||
|
buttonStyle="secondary"
|
||||||
|
data-test="dropdown-container-btn"
|
||||||
|
icon={dropdownTriggerIcon}
|
||||||
|
disabled={!popoverContent}
|
||||||
|
tooltip={dropdownTriggerTooltip}
|
||||||
|
css={css`
|
||||||
|
padding-left: ${theme.paddingXS}px;
|
||||||
|
padding-right: ${theme.paddingXXS}px;
|
||||||
|
gap: ${theme.sizeXXS}px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{dropdownTriggerText}
|
||||||
|
<Badge
|
||||||
|
count={dropdownTriggerCount ?? overflowingCount}
|
||||||
|
color={
|
||||||
|
(dropdownTriggerCount ?? overflowingCount) > 0
|
||||||
|
? theme.colorPrimary
|
||||||
|
: theme.colorTextSecondary
|
||||||
|
}
|
||||||
|
showZero
|
||||||
|
css={css`
|
||||||
|
margin-left: ${theme.sizeUnit * 2}px;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
<Icons.DownOutlined
|
||||||
|
iconSize="m"
|
||||||
|
iconColor={theme.colorIcon}
|
||||||
|
css={css`
|
||||||
|
.anticon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -334,7 +356,7 @@ export const DropdownContainer = forwardRef(
|
|||||||
>
|
>
|
||||||
{notOverflowedItems.map(item => item.element)}
|
{notOverflowedItems.map(item => item.element)}
|
||||||
</div>
|
</div>
|
||||||
{showDropdownButton && (
|
{shouldShowButton && (
|
||||||
<>
|
<>
|
||||||
<Global
|
<Global
|
||||||
styles={css`
|
styles={css`
|
||||||
@@ -359,62 +381,27 @@ export const DropdownContainer = forwardRef(
|
|||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Popover
|
{popoverContent ? (
|
||||||
styles={{
|
<Popover
|
||||||
body: {
|
styles={{
|
||||||
maxHeight: `${MAX_HEIGHT}px`,
|
body: {
|
||||||
overflow: showOverflow ? 'auto' : 'visible',
|
maxHeight: `${MAX_HEIGHT}px`,
|
||||||
},
|
overflow: showOverflow ? 'auto' : 'visible',
|
||||||
}}
|
},
|
||||||
content={popoverContent}
|
}}
|
||||||
trigger="click"
|
content={popoverContent}
|
||||||
open={popoverVisible && !!popoverContent}
|
trigger="click"
|
||||||
onOpenChange={visible => {
|
open={popoverVisible}
|
||||||
// While a recalculation keeps the trigger mounted but there is
|
onOpenChange={visible => setPopoverVisible(visible)}
|
||||||
// no content yet, ignore open attempts so it stays visible
|
placement="bottom"
|
||||||
// without opening an empty popover.
|
forceRender={forceRender}
|
||||||
if (popoverContent) setPopoverVisible(visible);
|
fresh // This prop prevents caching and stale data for filter scoping.
|
||||||
}}
|
>
|
||||||
placement="bottom"
|
{triggerButton}
|
||||||
forceRender={forceRender}
|
</Popover>
|
||||||
fresh // This prop prevents caching and stale data for filter scoping.
|
) : (
|
||||||
>
|
triggerButton
|
||||||
<Tooltip title={dropdownTriggerTooltip}>
|
)}
|
||||||
<Button
|
|
||||||
buttonStyle="secondary"
|
|
||||||
data-test="dropdown-container-btn"
|
|
||||||
icon={dropdownTriggerIcon}
|
|
||||||
css={css`
|
|
||||||
padding-left: ${theme.paddingXS}px;
|
|
||||||
padding-right: ${theme.paddingXXS}px;
|
|
||||||
gap: ${theme.sizeXXS}px;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{dropdownTriggerText}
|
|
||||||
<Badge
|
|
||||||
count={dropdownTriggerCount ?? overflowingCount}
|
|
||||||
color={
|
|
||||||
(dropdownTriggerCount ?? overflowingCount) > 0
|
|
||||||
? theme.colorPrimary
|
|
||||||
: theme.colorTextSecondary
|
|
||||||
}
|
|
||||||
showZero
|
|
||||||
css={css`
|
|
||||||
margin-left: ${theme.sizeUnit * 2}px;
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
<Icons.DownOutlined
|
|
||||||
iconSize="m"
|
|
||||||
iconColor={theme.colorIcon}
|
|
||||||
css={css`
|
|
||||||
.anticon {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Popover>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user