Skip to content

Commit 0a15809

Browse files
committed
Fix Radix popovers not positioned correctly in certain DDK or Embedded scenarios
1 parent 07c4b12 commit 0a15809

File tree

4 files changed

+50
-14
lines changed

4 files changed

+50
-14
lines changed

components/dash-core-components/src/components/css/datepickers.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
outline: none;
99
width: 100%;
1010
font-size: inherit;
11-
overflow: hidden;
11+
position: relative;
1212
accent-color: var(--Dash-Fill-Interactive-Strong);
1313
outline-color: var(--Dash-Fill-Interactive-Strong);
1414
}
@@ -231,3 +231,8 @@
231231
width: 20px;
232232
height: 20px;
233233
}
234+
235+
/* Override Radix's position: fixed to use position: absolute when using custom container */
236+
div[data-radix-popper-content-wrapper]:has(.dash-datepicker-content) {
237+
position: absolute !important;
238+
}

components/dash-core-components/src/components/css/dropdown.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,13 @@
212212
padding: calc(var(--Dash-Spacing) * 2) calc(var(--Dash-Spacing) * 3);
213213
box-shadow: 0 -1px 0 0 var(--Dash-Fill-Disabled) inset;
214214
}
215+
216+
/* Positioning container for the dropdown */
217+
.dash-dropdown-wrapper {
218+
position: relative;
219+
}
220+
221+
/* Override Radix's position: fixed to use position: absolute when using custom container */
222+
div[data-radix-popper-content-wrapper]:has(.dash-dropdown-content) {
223+
position: absolute !important;
224+
}

components/dash-core-components/src/fragments/Dropdown.tsx

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const Dropdown = (props: DropdownProps) => {
4747
const dropdownContentRef = useRef<HTMLDivElement>(
4848
document.createElement('div')
4949
);
50+
const searchInputRef = useRef<HTMLInputElement>(null);
5051

5152
const ctx = window.dash_component_api.useDashContext();
5253
const loading = ctx.useLoading();
@@ -234,22 +235,33 @@ const Dropdown = (props: DropdownProps) => {
234235
}
235236
}, [filteredOptions, isOpen]);
236237

237-
// Focus (and scroll) the first selected item when dropdown opens
238+
// Focus first selected item or search input when dropdown opens
238239
useEffect(() => {
239-
if (!isOpen || multi || search_value) {
240+
if (!isOpen || search_value) {
240241
return;
241242
}
242243

243244
// waiting for the DOM to be ready after the dropdown renders
244245
requestAnimationFrame(() => {
245-
const selectedValue = sanitizedValues[0];
246-
247-
const selectedElement = dropdownContentRef.current.querySelector(
248-
`.dash-options-list-option-checkbox[value="${selectedValue}"]`
249-
);
246+
// Try to focus the first selected item (for single-select)
247+
if (!multi) {
248+
const selectedValue = sanitizedValues[0];
249+
if (selectedValue) {
250+
const selectedElement =
251+
dropdownContentRef.current.querySelector(
252+
`.dash-options-list-option-checkbox[value="${selectedValue}"]`
253+
);
254+
255+
if (selectedElement instanceof HTMLElement) {
256+
selectedElement.focus();
257+
return;
258+
}
259+
}
260+
}
250261

251-
if (selectedElement instanceof HTMLElement) {
252-
selectedElement?.focus();
262+
// Fallback: focus search input if available and no selected item was focused
263+
if (searchable && searchInputRef.current) {
264+
searchInputRef.current.focus();
253265
}
254266
});
255267
}, [isOpen, multi, displayOptions, sanitizedValues]);
@@ -335,7 +347,7 @@ const Dropdown = (props: DropdownProps) => {
335347
} else {
336348
focusableElements[nextIndex].scrollIntoView({
337349
behavior: 'auto',
338-
block: 'center',
350+
block: 'nearest',
339351
});
340352
}
341353
}
@@ -354,8 +366,9 @@ const Dropdown = (props: DropdownProps) => {
354366
);
355367

356368
const accessibleId = id ?? uuid();
369+
const positioningContainerRef = useRef<HTMLDivElement>(null);
357370

358-
return (
371+
const popover = (
359372
<Popover.Root open={isOpen} onOpenChange={handleOpenChange}>
360373
<Popover.Trigger asChild>
361374
<button
@@ -365,6 +378,7 @@ const Dropdown = (props: DropdownProps) => {
365378
type="button"
366379
onKeyDown={e => {
367380
if (e.key === 'ArrowDown') {
381+
e.preventDefault();
368382
setIsOpen(true);
369383
}
370384
}}
@@ -426,7 +440,7 @@ const Dropdown = (props: DropdownProps) => {
426440
// container is required otherwise popover will be rendered
427441
// at document root, which may be outside of the Dash app (i.e.
428442
// an embedded app)
429-
container={dropdownContainerRef.current?.parentElement}
443+
container={positioningContainerRef.current}
430444
>
431445
<Popover.Content
432446
ref={dropdownContentRef}
@@ -451,7 +465,7 @@ const Dropdown = (props: DropdownProps) => {
451465
value={search_value || ''}
452466
autoComplete="off"
453467
onChange={e => onInputChange(e.target.value)}
454-
autoFocus
468+
ref={searchInputRef}
455469
/>
456470
{search_value && (
457471
<button
@@ -512,6 +526,12 @@ const Dropdown = (props: DropdownProps) => {
512526
</Popover.Portal>
513527
</Popover.Root>
514528
);
529+
530+
return (
531+
<div ref={positioningContainerRef} className="dash-dropdown-wrapper">
532+
{popover}
533+
</div>
534+
);
515535
};
516536

517537
export default Dropdown;

components/dash-core-components/tests/integration/calendar/test_calendar_props.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def test_cdpr001_date_clearable_true_works(dash_dcc):
3737
assert selected, "single date should get a value"
3838
close_btn = dash_dcc.wait_for_element("#dps-wrapper .dash-datepicker-clear")
3939
close_btn.click()
40+
sleep(0.25)
4041
(single_date,) = dash_dcc.get_date_range("dps")
4142
assert not single_date, "date should be cleared"
4243

0 commit comments

Comments
 (0)