fix(client): keep a flipped date-picker popup anchored to its trigger

CustomDatePicker flipped above a trigger by subtracting a fixed worst-case
height (360) from its top, regardless of the popup's real rendered height.
When that guess overshot — a short calendar view, or a form with little
room, like the settle-up modal — the gap left the popup floating away from
its trigger and over unrelated fields instead of sitting right above it
(#2334, found while testing the settle-up date field).

Anchor the flipped case from the trigger's own top edge via `bottom`
instead, the same technique useAnchoredPosition already uses for other
popovers, so the popup is always flush against its trigger no matter its
actual height.
This commit is contained in:
RemEx
2026-09-11 19:40:52 +00:00
committed by Maurice
parent 3a8ae6334c
commit bef11c5222
2 changed files with 43 additions and 7 deletions
@@ -577,9 +577,12 @@ describe('CustomDatePicker branches', () => {
render(<CustomDatePicker value="2026-03-15" onChange={onChange} />);
openCalendar();
const dialog = screen.getByRole('dialog');
// vw < 360 centres the 268px popup; the popup flips above the trigger
// vw < 360 centres the 268px popup; the popup flips above the trigger,
// anchored from the trigger's own top edge via `bottom` (not a `top`
// computed by subtracting a guessed popup height — see FE-W5DP-024).
expect(dialog.style.left).toBe('26px');
expect(dialog.style.top).toBe('336px');
expect(dialog.style.top).toBe('');
expect(dialog.style.bottom).toBe('72px');
} finally {
Element.prototype.getBoundingClientRect = originalRect;
Object.defineProperty(window, 'innerWidth', { value: originalWidth, configurable: true });
@@ -605,6 +608,31 @@ describe('CustomDatePicker branches', () => {
}
});
it('FE-W5DP-024: a flipped popup stays flush against its trigger instead of a fixed guessed offset (#2334)', () => {
// A settle-up-style form: the trigger sits low enough that the popup can't
// fit below it, but there is plenty of room above — a shorter gap than the
// worst-case guess would previously leave the popup floating over unrelated
// fields higher up the form instead of right above its own trigger.
const originalRect = Element.prototype.getBoundingClientRect;
Element.prototype.getBoundingClientRect = function () {
return { left: 440, top: 529, right: 803, bottom: 566, width: 363, height: 37, x: 440, y: 529, toJSON: () => ({}) } as DOMRect;
};
try {
render(<CustomDatePicker value="2026-03-15" onChange={onChange} />);
openCalendar();
const dialog = screen.getByRole('dialog');
// jsdom's default innerHeight (768) leaves 768-566=202px below the
// trigger — short of the flip threshold — so it flips, anchored to the
// trigger's own top edge (768-529+4=243), not a `top` derived from
// subtracting a guessed popup height.
expect(dialog.style.top).toBe('');
expect(dialog.style.bottom).toBe('243px');
} finally {
Element.prototype.getBoundingClientRect = originalRect;
}
});
it('FE-W5DP-018: the header arrows and the label highlight on hover', () => {
render(<CustomDatePicker value="2026-03-15" onChange={onChange} />);
openCalendar();
@@ -383,22 +383,30 @@ export function CustomDatePicker({
position: 'fixed',
// reanchor ticks on scroll / resize / keyboard so this recomputes
// against a fresh rect instead of the one measured on open (#1999).
...((): { top: number; left: number } => {
...((): { top?: number; bottom?: number; left: number } => {
void reanchor;
const r = ref.current?.getBoundingClientRect();
if (!r) return { top: 0, left: 0 };
const w = 268,
pad = 8,
// Only used to decide *whether* the popup fits below the trigger —
// its real height varies with the month's row count and the view
// (days/months/years), so it can't be known before it renders.
h = 360;
const vw = window.innerWidth;
const vh = window.visualViewport?.height ?? window.innerHeight;
let left = r.left;
let top = r.bottom + 4;
if (left + w > vw - pad) left = Math.max(pad, vw - w - pad);
if (top + h > vh - pad) top = r.top - h - 4;
top = Math.max(pad, Math.min(top, vh - h - pad));
if (vw < 360) left = Math.max(pad, (vw - w) / 2);
return { top, left };
if (r.bottom + 4 + h <= vh - pad) return { top: r.bottom + 4, left };
// Flipped: anchor from the trigger's own top edge via `bottom`
// instead of subtracting a guessed height from `top`. A guessed
// `top` leaves a gap sized by however wrong the guess was — in a
// short modal (e.g. the settle-up form) that gap was big enough to
// land the popup over unrelated fields instead of the trigger it
// belongs to. Anchoring the opposite edge means the popup always
// sits flush against the trigger no matter its real height.
return { bottom: Math.max(pad, vh - r.top + 4), left };
})(),
zIndex: 99999,
background: 'var(--bg-card)',