mirror of
https://github.com/apache/superset.git
synced 2026-06-30 20:05:36 +00:00
Compare commits
6 Commits
chore/ci/s
...
fix/dropdo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed14e8b9d7 | ||
|
|
4cef74b422 | ||
|
|
55d9e13725 | ||
|
|
ec13a2bb7b | ||
|
|
e274b45bc6 | ||
|
|
bd1420cfd8 |
@@ -51,8 +51,18 @@ 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();
|
||||||
|
// Button is disabled when there is nothing to open, so it can't reveal an empty popover
|
||||||
|
expect(screen.getByTestId('dropdown-container-btn')).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
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,13 @@ 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.
|
||||||
|
// When items exist but nothing overflows, the button is rendered
|
||||||
|
// disabled (not hidden) with a 0 badge so the container width stays
|
||||||
|
// constant across overflow state changes.
|
||||||
|
const shouldShowButton = items.length > 0 || !!dropdownContent;
|
||||||
|
|
||||||
const popoverContent = useMemo(
|
const popoverContent = useMemo(
|
||||||
() =>
|
() =>
|
||||||
dropdownContent || overflowingCount ? (
|
dropdownContent || overflowingCount ? (
|
||||||
@@ -272,15 +268,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,77 +300,13 @@ export const DropdownContainer = forwardRef(
|
|||||||
};
|
};
|
||||||
}, [popoverVisible]);
|
}, [popoverVisible]);
|
||||||
|
|
||||||
return (
|
const triggerButton = (
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
css={css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
css={css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: ${theme.sizeUnit * 4}px;
|
|
||||||
margin-right: ${theme.sizeUnit * 4}px;
|
|
||||||
min-width: 0px;
|
|
||||||
`}
|
|
||||||
data-test="container"
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
{notOverflowedItems.map(item => item.element)}
|
|
||||||
</div>
|
|
||||||
{showDropdownButton && (
|
|
||||||
<>
|
|
||||||
<Global
|
|
||||||
styles={css`
|
|
||||||
.ant-popover-inner {
|
|
||||||
// Some OS versions only show the scroll when hovering.
|
|
||||||
// These settings will make the scroll always visible.
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 14px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 9px;
|
|
||||||
background-color: ${theme.colorFillSecondary};
|
|
||||||
border: 3px solid transparent;
|
|
||||||
background-clip: content-box;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background-color: ${theme.colorFillQuaternary};
|
|
||||||
border-left: 1px solid ${theme.colorFillTertiary};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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
|
<Button
|
||||||
buttonStyle="secondary"
|
buttonStyle="secondary"
|
||||||
data-test="dropdown-container-btn"
|
data-test="dropdown-container-btn"
|
||||||
icon={dropdownTriggerIcon}
|
icon={dropdownTriggerIcon}
|
||||||
|
disabled={!popoverContent}
|
||||||
|
tooltip={dropdownTriggerTooltip}
|
||||||
css={css`
|
css={css`
|
||||||
padding-left: ${theme.paddingXS}px;
|
padding-left: ${theme.paddingXS}px;
|
||||||
padding-right: ${theme.paddingXXS}px;
|
padding-right: ${theme.paddingXXS}px;
|
||||||
@@ -413,8 +336,75 @@ export const DropdownContainer = forwardRef(
|
|||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
css={css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${theme.sizeUnit * 4}px;
|
||||||
|
margin-right: ${theme.sizeUnit * 4}px;
|
||||||
|
min-width: 0px;
|
||||||
|
`}
|
||||||
|
data-test="container"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{notOverflowedItems.map(item => item.element)}
|
||||||
|
</div>
|
||||||
|
{shouldShowButton && (
|
||||||
|
<>
|
||||||
|
<Global
|
||||||
|
styles={css`
|
||||||
|
.ant-popover-inner {
|
||||||
|
// Some OS versions only show the scroll when hovering.
|
||||||
|
// These settings will make the scroll always visible.
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 9px;
|
||||||
|
background-color: ${theme.colorFillSecondary};
|
||||||
|
border: 3px solid transparent;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: ${theme.colorFillQuaternary};
|
||||||
|
border-left: 1px solid ${theme.colorFillTertiary};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{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>
|
</Popover>
|
||||||
|
) : (
|
||||||
|
triggerButton
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user