Compare commits

...
Author SHA1 Message Date
Enzo MartellucciandClaude Opus 5 2f71175a17 fix(filters): guard the remeasure clamp before the wrapper is measured
Keep the consumer's max-width while useResizeDetector still reports 0, and
make the clamp test measure the row against the clamped edge instead of only
asserting the inline style is present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 14:24:39 +02:00
Enzo Martellucci bed117fadc Merge branch 'master' into enxdev/feat/filter-bar-overflow-edge-fix 2026-09-08 14:18:53 +02:00
Enzo Martellucci 191c89cd98 fix(filters): clamp remeasurement to wrapper width 2026-09-07 23:48:13 +02:00
Enzo Martellucci fc069b66f2 Merge branch 'master' into enxdev/feat/filter-bar-overflow-edge-fix 2026-09-07 23:33:18 +02:00
Enzo MartellucciandClaude Sonnet 5 b43f611342 refactor(filters): tighten comments in DropdownContainer
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:58:27 +02:00
Enzo MartellucciandClaude Sonnet 5 9ef93cea76 fix(filters): clamp to the row's own width, not the outer container's
Two issues flagged by automated review on PR #43966:

- The clamp used the outer container's width (items row + trigger
  button combined), so the row could still grow to fill that entire
  width and squeeze out the button - the exact failure it was meant to
  prevent. Clamp to the row's own last measured width instead.
- The clamp could be silently defeated by a consumer-supplied `style`
  prop, since inline styles win over a plain class. Add `!important` so
  the defensive clamp holds regardless.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:41:03 +02:00
Enzo MartellucciandClaude Sonnet 5 c3fadb824b fix(filters): clamp dropdown row width while remeasuring after items change
Adding a cross-filter chip while enough native filters already trigger
the "More filters" overflow briefly remounts all items to measure
their widths. In some browsers (Microsoft Edge) the intermediate frame
can paint before the recalculation collapses back down, letting the
row grow past its allotted space and push native filters and the
"More filters" trigger out of view.

Clamp the row to its last known width while remeasuring so that
transient frame, if painted, stays visually bounded instead of
overflowing the bar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 15:55:18 +02:00
2 changed files with 109 additions and 1 deletions
@@ -16,7 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import type { CSSProperties } from 'react';
import { screen, render } from '@superset-ui/core/spec';
import * as resizeDetector from 'react-resize-detector';
import { Button, DropdownContainer, Icons } from '..';
const generateItems = (n: number) =>
@@ -178,3 +180,98 @@ test('component renders and functions without throwing errors', () => {
// Basic functionality test
expect(screen.getByText('Element 1')).toBeInTheDocument();
});
const WRAPPER_WIDTH = 300;
const ITEM_WIDTH = 100;
/* Width the flex layout leaves the row once the trigger button is laid out. */
const ROW_WIDTH = 250;
/**
* Lays items out at ITEM_WIDTH each and reports the item row as bounded only
* by its own content, which is the frame Edge can paint before the flex layout
* bounds the row. An inline `max-width` in pixels is the only bound left, so
* the mock honors it and `onRowMeasure` receives the row, and the right edge
* the overflow calculation sees, while the row holds every item.
*/
const mockBoundingRects = (
onRowMeasure: (row: HTMLElement, right: number) => void,
) => {
const getBoundingClientRect: (this: HTMLElement) => DOMRect = function () {
let right: number;
if (this.dataset.test === 'container') {
const clamp = /^(\d+(?:\.\d+)?)px$/.exec(this.style.maxWidth);
right = Math.min(
this.children.length * ITEM_WIDTH,
clamp ? Number(clamp[1]) : ROW_WIDTH,
);
if (this.children.length === 4) {
onRowMeasure(this, right);
}
} else {
const itemNumber = Number(this.textContent?.match(/Element (\d+)/)?.[1]);
right = itemNumber ? itemNumber * ITEM_WIDTH : ROW_WIDTH;
}
return {
bottom: 0,
height: 0,
left: right - ITEM_WIDTH,
right,
top: 0,
width: ITEM_WIDTH,
x: right - ITEM_WIDTH,
y: 0,
toJSON: () => ({}),
};
};
jest
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
.mockImplementation(getBoundingClientRect);
};
const mockWrapperWidth = (width: number) =>
jest.spyOn(resizeDetector, 'useResizeDetector').mockReturnValue({
ref: { current: null as HTMLDivElement | null },
width,
});
/* Grows an overflowing row by one item, which mounts every item for a frame
* while the new overflow index is calculated. */
const remeasureWithExtraItem = (style?: CSSProperties) => {
const { rerender } = render(<DropdownContainer items={generateItems(3)} />);
rerender(<DropdownContainer items={generateItems(3)} />);
expect(screen.getByTestId('dropdown-container-btn')).toBeInTheDocument();
rerender(<DropdownContainer items={generateItems(4)} style={style} />);
};
test('clamps the item row to the wrapper width while remeasuring', () => {
mockWrapperWidth(WRAPPER_WIDTH);
let clampedRowRight: number | undefined;
mockBoundingRects((row, right) => {
expect(row).toHaveStyle({
maxWidth: `${WRAPPER_WIDTH}px`,
overflow: 'hidden',
});
clampedRowRight = right;
});
remeasureWithExtraItem({ maxWidth: 'none', overflow: 'visible' });
/* The all-items row holds 400px of items, so the overflow index is computed
* against the 300px clamp and not against the row's own content edge. The
* clamp holds even though the consumer asked for `max-width: none`. */
expect(clampedRowRight).toBe(WRAPPER_WIDTH);
});
test('does not clamp the item row before the wrapper is measured', () => {
mockWrapperWidth(0);
let clampedMaxWidth: string | undefined;
mockBoundingRects(row => {
clampedMaxWidth = row.style.maxWidth;
});
remeasureWithExtraItem({ maxWidth: 'none' });
/* A zero width means the resize callback hasn't fired yet. Clamping to it
* would hide every item instead of only the overflowing ones. */
expect(clampedMaxWidth).toBe('none');
});
@@ -330,7 +330,18 @@ export const DropdownContainer = forwardRef(
min-width: 0px;
`}
data-test="container"
style={style}
style={
recalculating
? {
...style,
/* Clamp the transient all-items row to the wrapper width.
* `width` is 0 until the first resize callback, so fall back
* to the consumer's value rather than clamping to nothing. */
maxWidth: width || style?.maxWidth,
overflow: 'hidden',
}
: style
}
>
{notOverflowedItems.map(item => item.element)}
</div>