mirror of
https://github.com/apache/superset.git
synced 2026-06-10 10:09:14 +00:00
Compare commits
5 Commits
enxdev/cha
...
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');
|
||||
});
|
||||
|
||||
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)} />);
|
||||
// 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();
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import { t } from '@apache-superset/core/translation';
|
||||
import { usePrevious } from '@superset-ui/core';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
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';
|
||||
|
||||
const MAX_HEIGHT = 500;
|
||||
@@ -72,15 +72,6 @@ export const DropdownContainer = forwardRef(
|
||||
|
||||
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
|
||||
// width of any of the child changes
|
||||
const recalculateItemWidths = useCallback(() => {
|
||||
@@ -180,7 +171,6 @@ export const DropdownContainer = forwardRef(
|
||||
);
|
||||
} else {
|
||||
setOverflowingIndex(-1);
|
||||
setRecalculating(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +211,6 @@ export const DropdownContainer = forwardRef(
|
||||
}
|
||||
|
||||
setOverflowingIndex(newOverflowingIndex);
|
||||
setRecalculating(false);
|
||||
}
|
||||
}, [
|
||||
current,
|
||||
@@ -245,6 +234,10 @@ export const DropdownContainer = forwardRef(
|
||||
const overflowingCount =
|
||||
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(
|
||||
() =>
|
||||
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(() => {
|
||||
if (popoverVisible) {
|
||||
// Measures scroll height after rendering the elements
|
||||
@@ -313,6 +297,44 @@ export const DropdownContainer = forwardRef(
|
||||
};
|
||||
}, [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 (
|
||||
<div
|
||||
ref={ref}
|
||||
@@ -334,7 +356,7 @@ export const DropdownContainer = forwardRef(
|
||||
>
|
||||
{notOverflowedItems.map(item => item.element)}
|
||||
</div>
|
||||
{showDropdownButton && (
|
||||
{shouldShowButton && (
|
||||
<>
|
||||
<Global
|
||||
styles={css`
|
||||
@@ -359,62 +381,27 @@ export const DropdownContainer = forwardRef(
|
||||
`}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: `${MAX_HEIGHT}px`,
|
||||
overflow: showOverflow ? 'auto' : 'visible',
|
||||
},
|
||||
}}
|
||||
content={popoverContent}
|
||||
trigger="click"
|
||||
open={popoverVisible && !!popoverContent}
|
||||
onOpenChange={visible => {
|
||||
// While a recalculation keeps the trigger mounted but there is
|
||||
// no content yet, ignore open attempts so it stays visible
|
||||
// without opening an empty popover.
|
||||
if (popoverContent) setPopoverVisible(visible);
|
||||
}}
|
||||
placement="bottom"
|
||||
forceRender={forceRender}
|
||||
fresh // This prop prevents caching and stale data for filter scoping.
|
||||
>
|
||||
<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>
|
||||
{popoverContent ? (
|
||||
<Popover
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: `${MAX_HEIGHT}px`,
|
||||
overflow: showOverflow ? 'auto' : 'visible',
|
||||
},
|
||||
}}
|
||||
content={popoverContent}
|
||||
trigger="click"
|
||||
open={popoverVisible}
|
||||
onOpenChange={visible => setPopoverVisible(visible)}
|
||||
placement="bottom"
|
||||
forceRender={forceRender}
|
||||
fresh // This prop prevents caching and stale data for filter scoping.
|
||||
>
|
||||
{triggerButton}
|
||||
</Popover>
|
||||
) : (
|
||||
triggerButton
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user