Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## UNRELEASED

## Added
- Add a prop to sliders, `allow_direct_input`, that can be used to disable the inputs rendered with sliders.
- Improve CSS styles in calendar when looking at selected dates outside the current calendar month (`show_outside_days=True`)

## [4.0.0rc6] - 2026-01-07

## Added
Expand All @@ -13,7 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## [4.0.0rc5] - 2025-12-16

## Added
- New prop in `dcc.Upload` allows users to recursively upload entire folders at once
- [#3464](https://github.com/plotly/dash/issues/3464) Add folder upload functionality to `dcc.Upload` component. When `multiple=True`, users can now select and upload entire folders in addition to individual files. The folder hierarchy is preserved in filenames (e.g., `folder/subfolder/file.txt`). Files within folders are filtered according to the `accept` prop. Folder support is available in Chrome, Edge, and Opera; other browsers gracefully fall back to file-only mode. The uploaded files use the same output API as multiple file uploads.

## Changed
- Bugfixes for feedback received in `rc4`
Expand Down Expand Up @@ -63,7 +69,6 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## [3.3.0] - 2025-11-12

## Added
- [#3464](https://github.com/plotly/dash/issues/3464) Add folder upload functionality to `dcc.Upload` component. When `multiple=True`, users can now select and upload entire folders in addition to individual files. The folder hierarchy is preserved in filenames (e.g., `folder/subfolder/file.txt`). Files within folders are filtered according to the `accept` prop. Folder support is available in Chrome, Edge, and Opera; other browsers gracefully fall back to file-only mode. The uploaded files use the same output API as multiple file uploads.
- [#3395](https://github.com/plotly/dash/pull/3396) Add position argument to hooks.devtool
- [#3403](https://github.com/plotly/dash/pull/3403) Add app_context to get_app, allowing to get the current app in routes.
- [#3407](https://github.com/plotly/dash/pull/3407) Add `hidden` to callback arguments, hiding the callback from appearing in the devtool callback graph.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function RangeSlider({
// eslint-disable-next-line no-magic-numbers
verticalHeight = 400,
step = undefined,
allow_direct_input = true,
...props
}: RangeSliderProps) {
// Some considerations for the default value of `step`:
Expand All @@ -38,6 +39,7 @@ export default function RangeSlider({
updatemode={updatemode}
verticalHeight={verticalHeight}
step={step}
allow_direct_input={allow_direct_input}
{...props}
/>
</Suspense>
Expand Down
2 changes: 2 additions & 0 deletions components/dash-core-components/src/components/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function Slider({
// eslint-disable-next-line no-magic-numbers
verticalHeight = 400,
step = undefined,
allow_direct_input = true,
setProps,
value,
drag_value,
Expand Down Expand Up @@ -77,6 +78,7 @@ export default function Slider({
updatemode={updatemode}
verticalHeight={verticalHeight}
step={step}
allow_direct_input={allow_direct_input}
value={mappedValue}
drag_value={mappedDragValue}
setProps={mappedSetProps}
Expand Down
23 changes: 20 additions & 3 deletions components/dash-core-components/src/components/css/calendar.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,34 @@
position: relative;
}

.dash-datepicker-calendar td.dash-datepicker-calendar-date-highlighted {
/* Highlighted dates (i.e. dates within a selected range) get highlight colours */
.dash-datepicker-calendar
td.dash-datepicker-calendar-date-highlighted:not(
.dash-datepicker-calendar-date-outside
) {
background-color: var(--Dash-Fill-Interactive-Weak);
color: var(--Dash-Fill-Interactive-Strong);
}

.dash-datepicker-calendar td.dash-datepicker-calendar-date-selected {
/* Outside days get highlighted colours only on hover */
.dash-datepicker-calendar
td.dash-datepicker-calendar-date-highlighted.dash-datepicker-calendar-date-outside:hover {
background-color: var(--Dash-Fill-Interactive-Weak);
color: var(--Dash-Fill-Interactive-Strong);
}

/* Selected dates (start & end) get accented colours */
.dash-datepicker-calendar
td.dash-datepicker-calendar-date-selected:not(
.dash-datepicker-calendar-date-outside
) {
background-color: var(--Dash-Fill-Interactive-Strong);
color: var(--Dash-Fill-Inverse-Strong);
}

.dash-datepicker-calendar td.dash-datepicker-calendar-date-selected {
/* Outside days, when selected, get accented colours only when active (being clicked) */
.dash-datepicker-calendar
td.dash-datepicker-calendar-date-outside.dash-datepicker-calendar-date-selected:active {
background-color: var(--Dash-Fill-Interactive-Strong);
color: var(--Dash-Fill-Inverse-Strong);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
position: relative;
accent-color: var(--Dash-Fill-Interactive-Strong);
outline-color: var(--Dash-Fill-Interactive-Strong);
font-family: inherit;
font-size: inherit;
color: inherit;
}

.dash-datepicker-input-wrapper {
Expand Down Expand Up @@ -146,8 +149,7 @@
overscroll-behavior: contain;
}

.dash-datepicker
[data-radix-popper-content-wrapper]:has(.dash-datepicker-portal) {
[data-radix-popper-content-wrapper]:has(.dash-datepicker-portal) {
transform: none !important;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
isSameDay,
strAsDate,
} from '../utils/calendar/helpers';
import {captureCSSForPortal} from '../utils/calendar/cssVariables';
import '../components/css/datepickers.css';

const DatePickerRange = ({
Expand Down Expand Up @@ -106,6 +107,11 @@ const DatePickerRange = ({
const calendarRef = useRef<CalendarHandle>(null);
const hasPortal = with_portal || with_full_screen_portal;

// Capture CSS variables for portal mode
const portalStyle = useMemo(() => {
return hasPortal ? captureCSSForPortal(containerRef) : undefined;
}, [hasPortal]);

useEffect(() => {
setInternalStartDate(strAsDate(start_date));
}, [start_date]);
Expand Down Expand Up @@ -382,7 +388,9 @@ const DatePickerRange = ({
</div>
</Popover.Trigger>

<Popover.Portal container={containerRef.current}>
<Popover.Portal
container={hasPortal ? undefined : containerRef.current}
>
<Popover.Content
className={`dash-datepicker-content${
hasPortal ? ' dash-datepicker-portal' : ''
Expand All @@ -391,6 +399,7 @@ const DatePickerRange = ({
? ' dash-datepicker-fullscreen'
: ''
}`}
style={portalStyle}
align={hasPortal ? 'center' : 'start'}
sideOffset={hasPortal ? 0 : 5}
avoidCollisions={!hasPortal}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isSameDay,
strAsDate,
} from '../utils/calendar/helpers';
import {captureCSSForPortal} from '../utils/calendar/cssVariables';
import '../components/css/datepickers.css';

const DatePickerSingle = ({
Expand Down Expand Up @@ -65,6 +66,11 @@ const DatePickerSingle = ({
const calendarRef = useRef<CalendarHandle>(null);
const hasPortal = with_portal || with_full_screen_portal;

// Capture CSS variables for portal mode
const portalStyle = useMemo(() => {
return hasPortal ? captureCSSForPortal(containerRef) : undefined;
}, [hasPortal, isCalendarOpen]);

useEffect(() => {
setInternalDate(strAsDate(date));
}, [date]);
Expand Down Expand Up @@ -201,7 +207,9 @@ const DatePickerSingle = ({
</div>
</Popover.Trigger>

<Popover.Portal container={containerRef.current}>
<Popover.Portal
container={hasPortal ? undefined : containerRef.current}
>
<Popover.Content
className={`dash-datepicker-content${
hasPortal ? ' dash-datepicker-portal' : ''
Expand All @@ -210,6 +218,7 @@ const DatePickerSingle = ({
? ' dash-datepicker-fullscreen'
: ''
}`}
style={portalStyle}
align={hasPortal ? 'center' : 'start'}
sideOffset={hasPortal ? 0 : 5}
avoidCollisions={!hasPortal}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default function RangeSlider(props: RangeSliderProps) {
pushable,
count,
reverse,
allow_direct_input = true,
} = props;

// For range slider, we expect an array of values
Expand Down Expand Up @@ -263,6 +264,7 @@ export default function RangeSlider(props: RangeSliderProps) {

// Determine if inputs should be rendered at all (CSS will handle responsive visibility)
const shouldShowInputs =
allow_direct_input !== false && // Not disabled by allow_direct_input
step !== null && // Not disabled by step=None
value.length <= 2 && // Only for single or range sliders
!vertical; // Only for horizontal sliders
Expand Down
12 changes: 12 additions & 0 deletions components/dash-core-components/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,12 @@ export interface SliderProps extends BaseDccProps<SliderProps> {
* The height, in px, of the slider if it is vertical.
*/
verticalHeight?: number;

/**
* If false, the input elements for directly entering values will be hidden.
* Only the slider will be visible and it will occupy 100% width of the container.
*/
allow_direct_input?: boolean;
}

export interface RangeSliderProps extends BaseDccProps<RangeSliderProps> {
Expand Down Expand Up @@ -604,6 +610,12 @@ export interface RangeSliderProps extends BaseDccProps<RangeSliderProps> {
* The height, in px, of the slider if it is vertical.
*/
verticalHeight?: number;

/**
* If false, the input elements for directly entering values will be hidden.
* Only the slider will be visible and it will occupy 100% width of the container.
*/
allow_direct_input?: boolean;
}

export type OptionValue = string | number | boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Captures CSS variables and key inherited properties from a container element for use in portaled content.
* When content is portaled outside its normal DOM hierarchy (e.g., to document.body),
* it loses access to CSS variables defined on parent elements and inherited properties.
* This function extracts those so they can be applied as inline styles.
*/
export function captureCSSForPortal(
containerRef: React.RefObject<HTMLElement>,
prefix = '--Dash-'
): Record<string, string> {
if (typeof window === 'undefined') {
return {};
}

const element = containerRef.current || document.documentElement;
const computedStyle = window.getComputedStyle(element);
const styles: Record<string, string> = {};

// Capture CSS variables (custom properties starting with prefix)
for (let i = 0; i < computedStyle.length; i++) {
const prop = computedStyle[i];
if (prop.startsWith(prefix)) {
styles[prop] = computedStyle.getPropertyValue(prop);
}
}

// Capture key inherited properties
const inheritedProps = ['fontFamily', 'fontSize', 'color'];
inheritedProps.forEach(prop => {
const value = computedStyle.getPropertyValue(prop);
if (value) {
styles[prop] = value;
}
});

return styles;
}
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,6 @@ def test_dppt002_datepicker_range_with_portal(dash_dcc):
dpr_input.send_keys(Keys.ESCAPE)
dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2)

assert dash_dcc.get_logs() == []


def test_dppt003_datepicker_single_with_fullscreen_portal(dash_dcc):
"""Test DatePickerSingle with with_full_screen_portal=True.
Expand Down Expand Up @@ -298,8 +296,6 @@ def test_dppt003_datepicker_single_with_fullscreen_portal(dash_dcc):
# Test clicking everything to verify all elements are accessible
click_everything_in_datepicker("#dps-fullscreen", dash_dcc)

assert dash_dcc.get_logs() == []


@pytest.mark.flaky(max_runs=3)
def test_dppt004_datepicker_range_with_fullscreen_portal(dash_dcc):
Expand Down Expand Up @@ -355,8 +351,6 @@ def test_dppt004_datepicker_range_with_fullscreen_portal(dash_dcc):
# Test clicking everything to verify all elements are accessible
click_everything_in_datepicker("#dpr-fullscreen", dash_dcc)

assert dash_dcc.get_logs() == []


def test_dppt005_portal_has_correct_classes(dash_dcc):
"""Test that portal datepickers have the correct CSS classes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,3 +654,83 @@ def update_output(value):
assert len(logs) > 0
warning_found = any("Too many marks" in log["message"] for log in logs)
assert warning_found, "Expected warning about too many marks not found in logs"


def test_slsl019_allow_direct_input_false(dash_dcc):
"""Test that allow_direct_input=False hides input elements for both Slider and RangeSlider"""
app = Dash(__name__)
app.layout = html.Div(
[
html.Div(
[
html.Label("Slider with allow_direct_input=False"),
dcc.Slider(
id="slider-no-input",
min=0,
max=100,
step=5,
value=50,
allow_direct_input=False,
),
html.Div(id="slider-output"),
]
),
html.Div(
[
html.Label("RangeSlider with allow_direct_input=False"),
dcc.RangeSlider(
id="rangeslider-no-input",
min=0,
max=100,
step=5,
value=[25, 75],
allow_direct_input=False,
),
html.Div(id="rangeslider-output"),
]
),
]
)

@app.callback(
Output("slider-output", "children"), [Input("slider-no-input", "value")]
)
def update_slider(value):
return f"Slider: {value}"

@app.callback(
Output("rangeslider-output", "children"),
[Input("rangeslider-no-input", "value")],
)
def update_rangeslider(value):
return f"RangeSlider: {value[0]}-{value[1]}"

dash_dcc.start_server(app)
dash_dcc.wait_for_text_to_equal("#slider-output", "Slider: 50")
dash_dcc.wait_for_text_to_equal("#rangeslider-output", "RangeSlider: 25-75")

# Verify no input elements exist for slider with allow_direct_input=False
slider_inputs = dash_dcc.find_elements("#slider-no-input .dash-range-slider-input")
assert (
len(slider_inputs) == 0
), "Expected 0 inputs for slider with allow_direct_input=False"

# Verify no input elements exist for rangeslider with allow_direct_input=False
rangeslider_inputs = dash_dcc.find_elements(
"#rangeslider-no-input .dash-range-slider-input"
)
assert (
len(rangeslider_inputs) == 0
), "Expected 0 inputs for rangeslider with allow_direct_input=False"

# Verify sliders are still functional by clicking them
slider = dash_dcc.find_element("#slider-no-input")
dash_dcc.click_at_coord_fractions(slider, 0.75, 0.5)
dash_dcc.wait_for_text_to_equal("#slider-output", "Slider: 75")

rangeslider = dash_dcc.find_element("#rangeslider-no-input")
# Click closer to the left to move the lower handle
dash_dcc.click_at_coord_fractions(rangeslider, 0.1, 0.5)
dash_dcc.wait_for_text_to_equal("#rangeslider-output", "RangeSlider: 10-75")

assert dash_dcc.get_logs() == []