Skip to content

Commit f6dd876

Browse files
committed
Merge branch 'dev' into bugfix/3572
2 parents 4ddfe81 + 8366cfc commit f6dd876

File tree

10 files changed

+278
-22
lines changed

10 files changed

+278
-22
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# These owners will be the default owners for everything in
22
# the repo. Unless a later match takes precedence
3-
* @T4rk1n @ndrezn @emilykl @camdecoster
3+
* @T4rk1n @ndrezn @camdecoster

.github/dependabot.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
version: 2
2+
updates:
3+
# Root package dependencies
4+
- package-ecosystem: "npm"
5+
directory: "/"
6+
schedule:
7+
interval: "weekly"
8+
day: "monday"
9+
groups:
10+
npm-dependencies:
11+
patterns:
12+
- "*"
13+
ignore:
14+
# Ignore @plotly packages
15+
- dependency-name: "@plotly/*"
16+
17+
# Dash renderer
18+
- package-ecosystem: "npm"
19+
directory: "/dash/dash-renderer"
20+
schedule:
21+
interval: "weekly"
22+
day: "monday"
23+
groups:
24+
npm-dependencies:
25+
patterns:
26+
- "*"
27+
28+
# Components - dash-core-components
29+
- package-ecosystem: "npm"
30+
directory: "/components/dash-core-components"
31+
schedule:
32+
interval: "weekly"
33+
day: "monday"
34+
groups:
35+
npm-dependencies:
36+
patterns:
37+
- "*"
38+
39+
# Components - dash-html-components
40+
- package-ecosystem: "npm"
41+
directory: "/components/dash-html-components"
42+
schedule:
43+
interval: "weekly"
44+
day: "monday"
45+
groups:
46+
npm-dependencies:
47+
patterns:
48+
- "*"
49+
50+
# Components - dash-table
51+
- package-ecosystem: "npm"
52+
directory: "/components/dash-table"
53+
schedule:
54+
interval: "weekly"
55+
day: "monday"
56+
groups:
57+
npm-dependencies:
58+
patterns:
59+
- "*"
60+
61+
# Python dependencies
62+
- package-ecosystem: "pip"
63+
directory: "/"
64+
schedule:
65+
interval: "weekly"
66+
day: "monday"
67+
groups:
68+
pip-dependencies:
69+
patterns:
70+
- "*"

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [UNRELEASED]
6+
7+
## Added
8+
- [#3637](https://github.com/plotly/dash/pull/3637) Added `debounce` prop to `Dropdown`.
9+
10+
## Fixed
11+
- [#3629](https://github.com/plotly/dash/pull/3629) Fix date pickers not showing date when initially rendered in a hidden container.
12+
13+
14+
515
## [4.0.0] - 2026-02-03
616

717
## Added

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
strAsDate,
2121
} from '../utils/calendar/helpers';
2222
import {captureCSSForPortal} from '../utils/calendar/cssVariables';
23+
import ResizeDetector from '../utils/ResizeDetector';
2324
import '../components/css/datepickers.css';
2425

2526
const DatePickerRange = ({
@@ -104,6 +105,8 @@ const DatePickerRange = ({
104105
const containerRef = useRef<HTMLDivElement>(null);
105106
const startInputRef = useRef<HTMLInputElement | null>(null);
106107
const endInputRef = useRef<HTMLInputElement | null>(null);
108+
const startAutosizeRef = useRef<any>(null);
109+
const endAutosizeRef = useRef<any>(null);
107110
const calendarRef = useRef<CalendarHandle>(null);
108111
const hasPortal = with_portal || with_full_screen_portal;
109112

@@ -128,6 +131,19 @@ const DatePickerRange = ({
128131
setEndInputValue(formatDate(internalEndDate, display_format));
129132
}, [internalEndDate, display_format]);
130133

134+
const handleResize = useCallback(() => {
135+
startAutosizeRef.current?.updateInputWidth?.();
136+
endAutosizeRef.current?.updateInputWidth?.();
137+
}, []);
138+
139+
useEffect(() => {
140+
startAutosizeRef.current?.updateInputWidth?.();
141+
}, [startInputValue]);
142+
143+
useEffect(() => {
144+
endAutosizeRef.current?.updateInputWidth?.();
145+
}, [endInputValue]);
146+
131147
useEffect(() => {
132148
// Controls when setProps is called. Basically, whenever internal state
133149
// diverges from props (i.e., user interaction)
@@ -313,6 +329,10 @@ const DatePickerRange = ({
313329
);
314330

315331
return (
332+
<ResizeDetector
333+
onResize={handleResize}
334+
targets={[containerRef]}
335+
>
316336
<div className="dash-datepicker" ref={containerRef}>
317337
<Popover.Root
318338
open={!disabled && isCalendarOpen}
@@ -335,6 +355,7 @@ const DatePickerRange = ({
335355
}}
336356
>
337357
<AutosizeInput
358+
ref={startAutosizeRef}
338359
inputRef={node => {
339360
startInputRef.current = node;
340361
}}
@@ -356,6 +377,7 @@ const DatePickerRange = ({
356377
/>
357378
<ArrowIcon className="dash-datepicker-range-arrow" />
358379
<AutosizeInput
380+
ref={endAutosizeRef}
359381
inputRef={node => {
360382
endInputRef.current = node;
361383
}}
@@ -458,6 +480,7 @@ const DatePickerRange = ({
458480
</Popover.Portal>
459481
</Popover.Root>
460482
</div>
483+
</ResizeDetector>
461484
);
462485
};
463486

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
strAsDate,
1515
} from '../utils/calendar/helpers';
1616
import {captureCSSForPortal} from '../utils/calendar/cssVariables';
17+
import ResizeDetector from '../utils/ResizeDetector';
1718
import '../components/css/datepickers.css';
1819

1920
const DatePickerSingle = ({
@@ -63,6 +64,7 @@ const DatePickerSingle = ({
6364

6465
const containerRef = useRef<HTMLDivElement>(null);
6566
const inputRef = useRef<HTMLInputElement | null>(null);
67+
const autosizeRef = useRef<any>(null);
6668
const calendarRef = useRef<CalendarHandle>(null);
6769
const hasPortal = with_portal || with_full_screen_portal;
6870

@@ -87,6 +89,15 @@ const DatePickerSingle = ({
8789
}
8890
}, [internalDate]);
8991

92+
const handleResize = useCallback(() => {
93+
autosizeRef.current?.updateInputWidth?.();
94+
}, []);
95+
96+
97+
useEffect(() => {
98+
autosizeRef.current?.updateInputWidth?.();
99+
}, [inputValue]);
100+
90101
const parseUserInput = useCallback(
91102
(focusCalendar = false) => {
92103
if (inputValue === '') {
@@ -157,6 +168,7 @@ const DatePickerSingle = ({
157168
}
158169

159170
return (
171+
<ResizeDetector onResize={handleResize} targets={[containerRef]}>
160172
<div className="dash-datepicker" ref={containerRef}>
161173
<Popover.Root
162174
open={!disabled && isCalendarOpen}
@@ -179,6 +191,7 @@ const DatePickerSingle = ({
179191
}}
180192
>
181193
<AutosizeInput
194+
ref={autosizeRef}
182195
inputRef={node => {
183196
inputRef.current = node;
184197
}}
@@ -274,6 +287,7 @@ const DatePickerSingle = ({
274287
</Popover.Portal>
275288
</Popover.Root>
276289
</div>
290+
</ResizeDetector>
277291
);
278292
};
279293

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

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const Dropdown = (props: DropdownProps) => {
2727
className,
2828
closeOnSelect,
2929
clearable,
30+
debounce,
3031
disabled,
3132
labels,
3233
maxHeight,
@@ -42,6 +43,7 @@ const Dropdown = (props: DropdownProps) => {
4243
const [optionsCheck, setOptionsCheck] = useState<DetailedOption[]>();
4344
const [isOpen, setIsOpen] = useState(false);
4445
const [displayOptions, setDisplayOptions] = useState<DetailedOption[]>([]);
46+
const [val, setVal] = useState<DropdownProps['value']>(value);
4547
const persistentOptions = useRef<DropdownProps['options']>([]);
4648
const dropdownContainerRef = useRef<HTMLButtonElement>(null);
4749
const dropdownContentRef = useRef<HTMLDivElement>(
@@ -53,6 +55,13 @@ const Dropdown = (props: DropdownProps) => {
5355
const ctx = window.dash_component_api.useDashContext();
5456
const loading = ctx.useLoading();
5557

58+
// Sync val when external value prop changes
59+
useEffect(() => {
60+
if (!isEqual(value, val)) {
61+
setVal(value);
62+
}
63+
}, [value]);
64+
5665
if (!persistentOptions || !isEqual(options, persistentOptions.current)) {
5766
persistentOptions.current = options;
5867
}
@@ -68,14 +77,27 @@ const Dropdown = (props: DropdownProps) => {
6877
);
6978

7079
const sanitizedValues: OptionValue[] = useMemo(() => {
71-
if (value instanceof Array) {
72-
return value;
80+
if (val instanceof Array) {
81+
return val;
7382
}
74-
if (isNil(value)) {
83+
if (isNil(val)) {
7584
return [];
7685
}
77-
return [value];
78-
}, [value]);
86+
return [val];
87+
}, [val]);
88+
89+
const handleSetProps = useCallback(
90+
(newValue: DropdownProps['value']) => {
91+
if (debounce && isOpen) {
92+
// local only
93+
setVal(newValue);
94+
} else {
95+
setVal(newValue);
96+
setProps({value: newValue});
97+
}
98+
},
99+
[debounce, isOpen, setProps]
100+
);
79101

80102
const updateSelection = useCallback(
81103
(selection: OptionValue[]) => {
@@ -90,30 +112,28 @@ const Dropdown = (props: DropdownProps) => {
90112
if (selection.length === 0) {
91113
// Empty selection: only allow if clearable is true
92114
if (clearable) {
93-
setProps({value: []});
115+
handleSetProps([]);
94116
}
95117
// If clearable is false and trying to set empty, do nothing
96118
// return;
97119
} else {
98-
// Non-empty selection: always allowed in multi-select
99-
setProps({value: selection});
120+
handleSetProps(selection);
100121
}
101122
} else {
102123
// For single-select, take the first value or null
103124
if (selection.length === 0) {
104125
// Empty selection: only allow if clearable is true
105126
if (clearable) {
106-
setProps({value: null});
127+
handleSetProps(null);
107128
}
108129
// If clearable is false and trying to set empty, do nothing
109130
// return;
110131
} else {
111-
// Take the first value for single-select
112-
setProps({value: selection[selection.length - 1]});
132+
handleSetProps(selection[selection.length - 1]);
113133
}
114134
}
115135
},
116-
[multi, clearable, closeOnSelect]
136+
[multi, clearable, closeOnSelect, handleSetProps]
117137
);
118138

119139
const onInputChange = useCallback(
@@ -182,8 +202,8 @@ const Dropdown = (props: DropdownProps) => {
182202

183203
const handleClear = useCallback(() => {
184204
const finalValue: DropdownProps['value'] = multi ? [] : null;
185-
setProps({value: finalValue});
186-
}, [multi]);
205+
handleSetProps(finalValue);
206+
}, [multi, handleSetProps]);
187207

188208
const handleSelectAll = useCallback(() => {
189209
if (multi) {
@@ -192,12 +212,12 @@ const Dropdown = (props: DropdownProps) => {
192212
.filter(option => !sanitizedValues.includes(option.value))
193213
.map(option => option.value)
194214
);
195-
setProps({value: allValues});
215+
handleSetProps(allValues);
196216
}
197217
if (closeOnSelect) {
198218
setIsOpen(false);
199219
}
200-
}, [multi, displayOptions, sanitizedValues, closeOnSelect]);
220+
}, [multi, displayOptions, sanitizedValues, closeOnSelect, handleSetProps]);
201221

202222
const handleDeselectAll = useCallback(() => {
203223
if (multi) {
@@ -206,12 +226,12 @@ const Dropdown = (props: DropdownProps) => {
206226
displayOption => displayOption.value === option
207227
);
208228
});
209-
setProps({value: withDeselected});
229+
handleSetProps(withDeselected);
210230
}
211231
if (closeOnSelect) {
212232
setIsOpen(false);
213233
}
214-
}, [multi, displayOptions, sanitizedValues, closeOnSelect]);
234+
}, [multi, displayOptions, sanitizedValues, closeOnSelect, handleSetProps]);
215235

216236
// Sort options when popover opens - selected options first
217237
// Update display options when filtered options or selection changes
@@ -370,11 +390,24 @@ const Dropdown = (props: DropdownProps) => {
370390
setIsOpen(open);
371391

372392
if (!open) {
373-
setProps({search_value: undefined});
374393
pendingSearchRef.current = '';
394+
const updates: Partial<DropdownProps> = {};
395+
396+
if (!isNil(search_value)) {
397+
updates.search_value = undefined;
398+
}
399+
400+
// Commit debounced value on close only
401+
if (debounce && !isEqual(value, val)) {
402+
updates.value = val;
403+
}
404+
405+
if (Object.keys(updates).length > 0) {
406+
setProps(updates);
407+
}
375408
}
376409
},
377-
[filteredOptions, sanitizedValues]
410+
[debounce, value, val, search_value, setProps]
378411
);
379412

380413
const accessibleId = id ?? uuid();

components/dash-core-components/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,11 @@ export interface DropdownProps extends BaseDccProps<DropdownProps> {
741741
clear_selection?: string;
742742
no_options_found?: string;
743743
};
744+
/**
745+
* If True, changes to input values will be sent back to the Dash server only when dropdown menu closes.
746+
* Use with `closeOnSelect=False`
747+
*/
748+
debounce?: boolean;
744749
}
745750

746751
export interface ChecklistProps extends BaseDccProps<ChecklistProps> {

0 commit comments

Comments
 (0)