Compare commits

...

4 Commits

Author SHA1 Message Date
Evan Rusackas
bf6e779200 address review: use Button tooltip prop so hint shows on disabled trigger
AntD disabled buttons swallow hover/focus, so the previous
<Tooltip><Button disabled /></Tooltip> wrapper left the
"No applied filters" hint unreachable in the exact state this PR
introduces (button visible, no popover content). The superset-ui Button
component's built-in `tooltip` prop already wraps disabled buttons in a
<span> so the tooltip fires — switch to that.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 16:01:03 -07:00
Evan Rusackas
ce6501c36c address review: guard empty popover and disable trigger when no content 2026-04-22 15:58:53 -07:00
Evan Rusackas
6ea6b01f4f address review: timeless comment wording 2026-04-22 15:58:24 -07:00
Evan Rusackas
c1376c05a9 fix(FilterBar): always show 'More filters' button when items exist
This fixes issue #28060 where the "More filters" button would disappear
when filter values were set to their defaults and nothing was overflowing.

The issue was caused by the button only rendering when `popoverContent`
was truthy, which required either `dropdownContent` to be defined OR
`overflowingCount > 0`. When neither condition was met (common when all
filters fit in the container), the button would vanish, causing:
- Inconsistent UI behavior
- Layout shifts as the button appears/disappears
- Poor user experience when resizing the browser window

The fix introduces a `shouldShowButton` flag that ensures the button is
always visible when items exist, regardless of overflow state. The badge
correctly shows 0 when nothing is overflowing, providing clear feedback
to users.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-22 15:58:24 -07:00
2 changed files with 74 additions and 54 deletions

View File

@@ -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();
}); });

View File

@@ -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;
@@ -234,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 ? (
@@ -293,72 +297,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>
{popoverContent && (
<>
<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}
onOpenChange={visible => 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;
@@ -388,8 +333,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>