diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d896728b3..80703be436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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` @@ -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. diff --git a/components/dash-core-components/src/components/RangeSlider.tsx b/components/dash-core-components/src/components/RangeSlider.tsx index 0fb241c004..ba6baa24ee 100644 --- a/components/dash-core-components/src/components/RangeSlider.tsx +++ b/components/dash-core-components/src/components/RangeSlider.tsx @@ -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`: @@ -38,6 +39,7 @@ export default function RangeSlider({ updatemode={updatemode} verticalHeight={verticalHeight} step={step} + allow_direct_input={allow_direct_input} {...props} /> diff --git a/components/dash-core-components/src/components/Slider.tsx b/components/dash-core-components/src/components/Slider.tsx index a1b4dfd368..5080d5d2fe 100644 --- a/components/dash-core-components/src/components/Slider.tsx +++ b/components/dash-core-components/src/components/Slider.tsx @@ -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, @@ -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} diff --git a/components/dash-core-components/src/components/css/calendar.css b/components/dash-core-components/src/components/css/calendar.css index 7b6fe9e287..814307123d 100644 --- a/components/dash-core-components/src/components/css/calendar.css +++ b/components/dash-core-components/src/components/css/calendar.css @@ -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); } diff --git a/components/dash-core-components/src/components/css/datepickers.css b/components/dash-core-components/src/components/css/datepickers.css index 9878735c60..45f89ecc01 100644 --- a/components/dash-core-components/src/components/css/datepickers.css +++ b/components/dash-core-components/src/components/css/datepickers.css @@ -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 { @@ -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; } diff --git a/components/dash-core-components/src/fragments/DatePickerRange.tsx b/components/dash-core-components/src/fragments/DatePickerRange.tsx index 6f97440338..ccac8012fd 100644 --- a/components/dash-core-components/src/fragments/DatePickerRange.tsx +++ b/components/dash-core-components/src/fragments/DatePickerRange.tsx @@ -19,6 +19,7 @@ import { isSameDay, strAsDate, } from '../utils/calendar/helpers'; +import {captureCSSForPortal} from '../utils/calendar/cssVariables'; import '../components/css/datepickers.css'; const DatePickerRange = ({ @@ -106,6 +107,11 @@ const DatePickerRange = ({ const calendarRef = useRef(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]); @@ -382,7 +388,9 @@ const DatePickerRange = ({ - + (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]); @@ -201,7 +207,9 @@ const DatePickerSingle = ({ - + { * 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 { @@ -604,6 +610,12 @@ export interface RangeSliderProps extends BaseDccProps { * 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; diff --git a/components/dash-core-components/src/utils/calendar/cssVariables.ts b/components/dash-core-components/src/utils/calendar/cssVariables.ts new file mode 100644 index 0000000000..84eab5a226 --- /dev/null +++ b/components/dash-core-components/src/utils/calendar/cssVariables.ts @@ -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, + prefix = '--Dash-' +): Record { + if (typeof window === 'undefined') { + return {}; + } + + const element = containerRef.current || document.documentElement; + const computedStyle = window.getComputedStyle(element); + const styles: Record = {}; + + // 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; +} diff --git a/components/dash-core-components/tests/integration/calendar/test_portal.py b/components/dash-core-components/tests/integration/calendar/test_portal.py index 69ebf4b984..fb629550ae 100644 --- a/components/dash-core-components/tests/integration/calendar/test_portal.py +++ b/components/dash-core-components/tests/integration/calendar/test_portal.py @@ -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. @@ -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): @@ -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. diff --git a/components/dash-core-components/tests/integration/sliders/test_sliders.py b/components/dash-core-components/tests/integration/sliders/test_sliders.py index 65f56a8b03..fed33c30d8 100644 --- a/components/dash-core-components/tests/integration/sliders/test_sliders.py +++ b/components/dash-core-components/tests/integration/sliders/test_sliders.py @@ -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() == []