Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/
import { render, screen, userEvent } from 'spec/helpers/testing-library';
import { FilterBarOrientation } from 'src/dashboard/types';
import ActionButtons from './index';

const createProps = () => ({
Expand Down Expand Up @@ -75,3 +76,30 @@ test('should apply', () => {
userEvent.click(applyBtn);
expect(mockedProps.onApply).toHaveBeenCalled();
});

// Use toHaveStyleRule (from @emotion/jest, extended in spec/helpers/setup.ts) so we
// read emotion's CSS rules directly rather than jsdom's computed styles, which do not
// reflect styled-component / emotion class-based CSS.
test('vertical container uses flex layout (no position:fixed)', () => {
const mockedProps = createProps();
render(<ActionButtons {...mockedProps} />, { useRedux: true });
const container = screen.getByTestId('filterbar-action-buttons');
// flex layout is now responsible for keeping the button visible at any zoom level
expect(container).toHaveStyleRule('display', 'flex');
expect(container).toHaveStyleRule('flex-direction', 'column');
expect(container).not.toHaveStyleRule('position', 'fixed');
});

test('horizontal container uses auto left-margin layout', () => {
const mockedProps = createProps();
render(
<ActionButtons
{...mockedProps}
filterBarOrientation={FilterBarOrientation.Horizontal}
/>,
{ useRedux: true },
);
const container = screen.getByTestId('filterbar-action-buttons');
expect(container).toHaveStyleRule('margin-left', 'auto');
expect(container).not.toHaveStyleRule('position', 'fixed');
});
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ const ButtonsContainer = styled.div<{ isVertical: boolean }>`
? css`
flex-direction: column;
align-items: center;
position: sticky;
z-index: 100;
bottom: 0;
padding: ${theme.sizeUnit * 4}px;
padding-top: ${theme.sizeUnit * 6}px;
background: linear-gradient(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { Icons } from '@superset-ui/core/components/Icons';
import { EmptyState, Loading } from '@superset-ui/core/components';
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
import { FILTER_BAR_HEADER_HEIGHT } from 'src/dashboard/constants';
import { getFilterBarTestId, useChartsVerboseMaps } from './utils';
import { VerticalBarProps } from './types';
import Header from './Header';
Expand All @@ -63,8 +64,8 @@ const BarWrapper = styled.div<{ width: number }>`
}
`;

const Bar = styled.div<{ width: number }>`
${({ theme, width }) => `
const Bar = styled.div<{ width: number; height: string | number }>`
${({ theme, width, height }) => `
& .ant-typography-edit-content {
left: 0;
margin-top: 0;
Expand All @@ -76,10 +77,10 @@ const Bar = styled.div<{ width: number }>`
flex-direction: column;
flex-grow: 1;
width: ${width}px;
height: ${typeof height === 'number' ? `${height}px` : height};
background: ${theme.colorBgContainer};
border-right: 1px solid ${theme.colorSplit};
border-bottom: 1px solid ${theme.colorSplit};
min-height: 100%;
display: none;
&.open {
display: flex;
Expand Down Expand Up @@ -120,8 +121,6 @@ const FilterControlsWrapper = styled.div`
gap: ${theme.sizeUnit * 2}px;
padding: ${theme.sizeUnit * 4}px;
padding-top: 0; /* Works with other changes in PR https://github.com/apache/superset/pull/38646 to reduces space between filter header and 1st filter */
// 108px padding to make room for buttons with position: absolute
padding-bottom: ${theme.sizeUnit * 27}px;
`}
`;

Expand Down Expand Up @@ -171,11 +170,34 @@ const VerticalFilterBar: FC<VerticalBarProps> = ({
};
}, [onScroll]);

const tabPaneStyle = useMemo(
() => ({ overflow: 'auto', height, overscrollBehavior: 'contain' }),
// The `height` prop is computed in DashboardBuilder as:
// calc(100vh - FILTER_BAR_HEADER_HEIGHT - MAIN_HEADER_HEIGHT)
// It therefore represents the height of the scrollable content area only
// (everything below the filter bar header). Adding FILTER_BAR_HEADER_HEIGHT
// back gives the total panel height, equivalent to (100vh - MAIN_HEADER_HEIGHT).
// This lets Bar use an explicit height so the action buttons can be a natural
// flex footer instead of relying on position:fixed (which breaks at high
// display scale factors such as 150% on a 1920×1080 screen).
const barHeight = useMemo(
() =>
typeof height === 'number'
? height + FILTER_BAR_HEADER_HEIGHT
: `calc(${height} + ${FILTER_BAR_HEADER_HEIGHT}px)`,
[height],
);

// Empty deps are intentional: the style object no longer depends on `height`
// because the scrollable area grows via flex:1 rather than a fixed pixel value.
const tabPaneStyle = useMemo(
() => ({
flex: 1,
minHeight: 0,
overflow: 'auto',
overscrollBehavior: 'contain',
}),
[],
);

const dataMask = useSelector<RootState, DataMaskStateWithId>(
state => state.dataMask,
);
Expand Down Expand Up @@ -285,12 +307,17 @@ const VerticalFilterBar: FC<VerticalBarProps> = ({
iconSize="l"
/>
</CollapsedBar>
<Bar className={cx({ open: filtersOpen })} width={width}>
<Bar
className={cx({ open: filtersOpen })}
width={width}
height={barHeight}
>
<Header toggleFiltersBar={toggleFiltersBar} />
{!isInitialized ? (
<div
css={{
height,
flex: 1,
minHeight: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ const StyledRangeType = styled(Select)`

const ContentStyleWrapper = styled.div`
${({ theme }) => css`
display: flex;
flex-direction: column;
max-height: 70vh;

.ant-row {
margin-top: 8px;
}
Expand Down Expand Up @@ -99,7 +103,14 @@ const ContentStyleWrapper = styled.div`
width: 217px;
}

.content-body {
flex: 1;
min-height: 0;
overflow-y: auto;
}

.footer {
flex-shrink: 0;
text-align: right;
}
`}
Expand Down Expand Up @@ -273,51 +284,55 @@ export default function DateFilterLabel(props: DateFilterControlProps) {

const overlayContent = (
<ContentStyleWrapper>
<div className="control-label">{t('Range type')}</div>
<StyledRangeType
ariaLabel={t('Range type')}
options={FRAME_OPTIONS}
value={frame}
onChange={onChangeFrame}
/>
{frame !== 'No filter' && <Divider />}
{frame === 'Common' && (
<CommonFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'Calendar' && (
<CalendarFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'Current' && (
<CurrentCalendarFrame
value={timeRangeValue}
onChange={setTimeRangeValue}
/>
)}
{frame === 'Advanced' && (
<AdvancedFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'Custom' && (
<CustomFrame
value={timeRangeValue}
onChange={setTimeRangeValue}
isOverflowingFilterBar={isOverflowingFilterBar}
<div className="content-body">
<div className="control-label">{t('Range type')}</div>
<StyledRangeType
ariaLabel={t('Range type')}
options={FRAME_OPTIONS}
value={frame}
onChange={onChangeFrame}
/>
)}
{frame === 'No filter' && <div data-test={DateFilterTestKey.NoFilter} />}
<Divider />
<div>
<div className="section-title">{t('Actual time range')}</div>
{validTimeRange && (
<div>
{evalResponse === 'No filter' ? t('No filter') : evalResponse}
</div>
{frame !== 'No filter' && <Divider />}
{frame === 'Common' && (
<CommonFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'Calendar' && (
<CalendarFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'Current' && (
<CurrentCalendarFrame
value={timeRangeValue}
onChange={setTimeRangeValue}
/>
)}
{!validTimeRange && (
<IconWrapper className="warning">
<Icons.ExclamationCircleOutlined iconColor={theme.colorError} />
<span className="text error">{evalResponse}</span>
</IconWrapper>
{frame === 'Advanced' && (
<AdvancedFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'Custom' && (
<CustomFrame
value={timeRangeValue}
onChange={setTimeRangeValue}
isOverflowingFilterBar={isOverflowingFilterBar}
/>
)}
{frame === 'No filter' && (
<div data-test={DateFilterTestKey.NoFilter} />
)}
<Divider />
<div>
<div className="section-title">{t('Actual time range')}</div>
{validTimeRange && (
<div>
{evalResponse === 'No filter' ? t('No filter') : evalResponse}
</div>
)}
{!validTimeRange && (
<IconWrapper className="warning">
<Icons.ExclamationCircleOutlined iconColor={theme.colorError} />
<span className="text error">{evalResponse}</span>
</IconWrapper>
)}
</div>
</div>
<Divider />
<div className="footer">
Expand Down Expand Up @@ -346,7 +361,7 @@ export default function DateFilterLabel(props: DateFilterControlProps) {

const popoverContent = (
<ControlPopover
autoAdjustOverflow={false}
autoAdjustOverflow
trigger="click"
placement="right"
content={overlayContent}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@ test('DateFilter popover should attach to parent node when overflowing in filter
expect(popover?.parentElement).toBe(trigger.parentElement);
});

test('popover content uses scrollable body with pinned footer for Apply/Cancel', () => {
render(setup());
userEvent.click(screen.getByText(NO_TIME_RANGE));

const applyBtn = screen.getByTestId(DateFilterTestKey.ApplyButton);
const cancelBtn = screen.getByTestId(DateFilterTestKey.CancelButton);
const footer = applyBtn.closest('.footer') as HTMLElement;
const wrapper = footer.parentElement as HTMLElement;
const contentBody = wrapper.querySelector('.content-body') as HTMLElement;

expect(contentBody).toBeInTheDocument();
expect(footer).toBeInTheDocument();
// Footer is a sibling of content-body, not nested inside it,
// so it stays visible when the body scrolls.
expect(footer.parentElement).toBe(contentBody.parentElement);
expect(contentBody.contains(cancelBtn)).toBe(false);
expect(contentBody.contains(applyBtn)).toBe(false);
});

test('DateFilter should properly handle isOverflowingFilterBar prop changes', () => {
const { rerender } = render(
setup({ ...defaultProps, isOverflowingFilterBar: false }),
Expand Down
Loading