Skip to content

Commit 64861d9

Browse files
nehaaprasadsangeethababu9223heloiselui
authored
fix(popover): keep popover open when selecting a date from datepicker (#21705)
* fix(popover): keep popover open when selecting a date from DatePicker * fix(popover): keep popover open when selecting a date from DatePicker * chore(popover): replace _flatpickr cast with safe narrowing * fix(popover): remove unnecessary optional chaining and add TODO comment --------- Co-authored-by: Sangeetha Babu <sangeetha9223@gmail.com> Co-authored-by: “heloiselui” <helolui27@gmail.com>
1 parent 34a9b3e commit 64861d9

2 files changed

Lines changed: 138 additions & 1 deletion

File tree

packages/react/src/components/Popover/__tests__/Popover-test.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,108 @@ describe('Popover', () => {
459459
await userEvent.click(screen.getByTestId('tabtip-checkbox'));
460460
expect(onRequestClose).not.toHaveBeenCalled();
461461
});
462+
463+
it('should NOT call onRequestClose when clicking DatePicker calendar rendered in body', async () => {
464+
const onRequestClose = jest.fn();
465+
render(
466+
<Popover open autoAlign onRequestClose={onRequestClose}>
467+
<button type="button">Open</button>
468+
<PopoverContent>
469+
<input id="date-picker-input" aria-label="Date input" readOnly />
470+
</PopoverContent>
471+
</Popover>
472+
);
473+
474+
const input = screen.getByLabelText('Date input');
475+
const calendar = document.createElement('div');
476+
calendar.className = 'flatpickr-calendar open';
477+
calendar.innerHTML = '<span class="flatpickr-day">26</span>';
478+
document.body.appendChild(calendar);
479+
480+
try {
481+
Object.defineProperty(input, '_flatpickr', {
482+
value: { calendarContainer: calendar },
483+
configurable: true,
484+
});
485+
486+
await userEvent.click(calendar.querySelector('.flatpickr-day'));
487+
488+
expect(onRequestClose).not.toHaveBeenCalled();
489+
} finally {
490+
document.body.removeChild(calendar);
491+
}
492+
});
493+
494+
it('should NOT call onRequestClose when focus moves to DatePicker calendar', async () => {
495+
const onRequestClose = jest.fn();
496+
const { container } = render(
497+
<Popover open autoAlign onRequestClose={onRequestClose}>
498+
<button type="button">Open</button>
499+
<PopoverContent>
500+
<input id="date-picker-input" aria-label="Date input" readOnly />
501+
</PopoverContent>
502+
</Popover>
503+
);
504+
505+
const input = screen.getByLabelText('Date input');
506+
const calendar = document.createElement('div');
507+
calendar.className = 'flatpickr-calendar open';
508+
calendar.innerHTML = '<span class="flatpickr-day" tabindex="0">26</span>';
509+
document.body.appendChild(calendar);
510+
511+
try {
512+
Object.defineProperty(input, '_flatpickr', {
513+
value: { calendarContainer: calendar },
514+
configurable: true,
515+
});
516+
517+
input.focus();
518+
const popoverEl = container.firstChild;
519+
const focusoutEvent = new FocusEvent('focusout', {
520+
bubbles: true,
521+
relatedTarget: calendar.querySelector('.flatpickr-day'),
522+
});
523+
popoverEl.dispatchEvent(focusoutEvent);
524+
525+
expect(onRequestClose).not.toHaveBeenCalled();
526+
} finally {
527+
document.body.removeChild(calendar);
528+
}
529+
});
530+
531+
it('should call onRequestClose when clicking DatePicker calendar outside popover', async () => {
532+
const onRequestClose = jest.fn();
533+
render(
534+
<Popover open autoAlign onRequestClose={onRequestClose}>
535+
<button type="button">Open</button>
536+
<PopoverContent>
537+
<input id="popover-input" aria-label="Popover input" readOnly />
538+
</PopoverContent>
539+
</Popover>
540+
);
541+
542+
const outsideInput = document.createElement('input');
543+
outsideInput.setAttribute('aria-label', 'Outside input');
544+
outsideInput.readOnly = true;
545+
document.body.appendChild(outsideInput);
546+
547+
const calendar = document.createElement('div');
548+
calendar.className = 'flatpickr-calendar open';
549+
calendar.innerHTML = '<span class="flatpickr-day">26</span>';
550+
document.body.appendChild(calendar);
551+
552+
try {
553+
Object.defineProperty(outsideInput, '_flatpickr', {
554+
value: { calendarContainer: calendar },
555+
configurable: true,
556+
});
557+
558+
await userEvent.click(calendar.querySelector('.flatpickr-day'));
559+
560+
expect(onRequestClose).toHaveBeenCalled();
561+
} finally {
562+
document.body.removeChild(calendar);
563+
document.body.removeChild(outsideInput);
564+
}
565+
});
462566
});

packages/react/src/components/Popover/index.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,30 @@ export const Popover: PopoverComponent & {
201201
useFeatureFlag('enable-v12-dynamic-floating-styles') || autoAlign;
202202
const lastClickWasInsidePopoverContent = useRef(false);
203203

204+
const isTargetInDatePickerInsidePopover = (target: Node) => {
205+
if (!popover.current) return false;
206+
// TODO: Revisit this DatePicker/Popover integration to avoid relying on
207+
// Flatpickr internals (`_flatpickr`) and DOM traversal (`closest`) if we
208+
// can introduce a more stable shared marker or ref between the components.
209+
const calendar =
210+
target instanceof Element && target.closest('.flatpickr-calendar');
211+
if (!calendar) return false;
212+
const inputs = popover.current.querySelectorAll('input');
213+
for (const input of inputs) {
214+
if (!('_flatpickr' in input)) continue;
215+
const fp = input._flatpickr;
216+
if (
217+
fp &&
218+
typeof fp === 'object' &&
219+
'calendarContainer' in fp &&
220+
fp.calendarContainer === calendar
221+
) {
222+
return true;
223+
}
224+
}
225+
return false;
226+
};
227+
204228
let align = mapPopoverAlign(initialAlign);
205229

206230
// Tracks clicks inside PopoverContent to prevent it from closing when clicked, this handles an edge
@@ -231,6 +255,10 @@ export const Popover: PopoverComponent & {
231255

232256
onRequestClose?.();
233257
} else if (relatedTarget && !popover.current?.contains(relatedTarget)) {
258+
if (isTargetInDatePickerInsidePopover(relatedTarget)) {
259+
return;
260+
}
261+
234262
const isOutsideFloating =
235263
enableFloatingStyles && refs.floating.current
236264
? !refs.floating.current.contains(relatedTarget)
@@ -249,7 +277,12 @@ export const Popover: PopoverComponent & {
249277
});
250278

251279
useWindowEvent('click', ({ target }) => {
252-
if (open && target instanceof Node && !popover.current?.contains(target)) {
280+
if (
281+
open &&
282+
target instanceof Node &&
283+
!popover.current?.contains(target) &&
284+
!isTargetInDatePickerInsidePopover(target)
285+
) {
253286
onRequestClose?.();
254287
}
255288
});

0 commit comments

Comments
 (0)