From 92e24fa9e60a00007422eb72a814a354c20a7fe8 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Mon, 22 Dec 2025 16:08:01 -0700 Subject: [PATCH 1/7] Restore radix default fixed position in popovers --- .../src/components/css/datepickers.css | 5 - .../src/components/css/dropdown.css | 5 - .../src/fragments/DatePickerRange.tsx | 3 - .../src/fragments/DatePickerSingle.tsx | 3 - .../src/fragments/Dropdown.tsx | 3 - .../misc/test_popover_visibility.py | 197 ++++++++++++++++++ 6 files changed, 197 insertions(+), 19 deletions(-) create mode 100644 components/dash-core-components/tests/integration/misc/test_popover_visibility.py diff --git a/components/dash-core-components/src/components/css/datepickers.css b/components/dash-core-components/src/components/css/datepickers.css index 103fb51f5e..fa103b62de 100644 --- a/components/dash-core-components/src/components/css/datepickers.css +++ b/components/dash-core-components/src/components/css/datepickers.css @@ -226,8 +226,3 @@ width: 20px; height: 20px; } - -/* Override Radix's position: fixed to use position: absolute when using custom container */ -div[data-radix-popper-content-wrapper]:has(.dash-datepicker-content) { - position: absolute !important; -} diff --git a/components/dash-core-components/src/components/css/dropdown.css b/components/dash-core-components/src/components/css/dropdown.css index 4bd605a969..8333f32506 100644 --- a/components/dash-core-components/src/components/css/dropdown.css +++ b/components/dash-core-components/src/components/css/dropdown.css @@ -235,8 +235,3 @@ .dash-dropdown-wrapper { position: relative; } - -/* Override Radix's position: fixed to use position: absolute when using custom container */ -div[data-radix-popper-content-wrapper]:has(.dash-dropdown-content) { - position: absolute !important; -} diff --git a/components/dash-core-components/src/fragments/DatePickerRange.tsx b/components/dash-core-components/src/fragments/DatePickerRange.tsx index f7bcc603d9..bccb719cc7 100644 --- a/components/dash-core-components/src/fragments/DatePickerRange.tsx +++ b/components/dash-core-components/src/fragments/DatePickerRange.tsx @@ -384,9 +384,6 @@ const DatePickerRange = ({ className="dash-datepicker-content" align="start" sideOffset={5} - collisionBoundary={containerRef.current?.closest( - '#_dash-app-content' - )} onOpenAutoFocus={e => e.preventDefault()} onCloseAutoFocus={e => { e.preventDefault(); diff --git a/components/dash-core-components/src/fragments/DatePickerSingle.tsx b/components/dash-core-components/src/fragments/DatePickerSingle.tsx index da71ca5cb8..3abf7f034f 100644 --- a/components/dash-core-components/src/fragments/DatePickerSingle.tsx +++ b/components/dash-core-components/src/fragments/DatePickerSingle.tsx @@ -203,9 +203,6 @@ const DatePickerSingle = ({ className="dash-datepicker-content" align="start" sideOffset={5} - collisionBoundary={containerRef.current?.closest( - '#_dash-app-content' - )} onOpenAutoFocus={e => e.preventDefault()} onCloseAutoFocus={e => { e.preventDefault(); diff --git a/components/dash-core-components/src/fragments/Dropdown.tsx b/components/dash-core-components/src/fragments/Dropdown.tsx index 646e31b746..fcf3b41ef4 100644 --- a/components/dash-core-components/src/fragments/Dropdown.tsx +++ b/components/dash-core-components/src/fragments/Dropdown.tsx @@ -458,9 +458,6 @@ const Dropdown = (props: DropdownProps) => { className="dash-dropdown-content" align="start" sideOffset={5} - collisionBoundary={positioningContainerRef.current?.closest( - '#_dash-app-content' - )} onOpenAutoFocus={e => e.preventDefault()} onKeyDown={handleKeyDown} style={{ diff --git a/components/dash-core-components/tests/integration/misc/test_popover_visibility.py b/components/dash-core-components/tests/integration/misc/test_popover_visibility.py new file mode 100644 index 0000000000..23a859db83 --- /dev/null +++ b/components/dash-core-components/tests/integration/misc/test_popover_visibility.py @@ -0,0 +1,197 @@ +from datetime import date +from dash import Dash, html, dcc +from selenium.webdriver.common.by import By +from time import sleep + + +def click_everything_in_datepicker(datepicker_id, dash_dcc): + """Click on every clickable element in a datepicker calendar. + + Args: + datepicker_id: CSS selector for the datepicker element (e.g., "#dpr") + dash_dcc: The dash_dcc fixture + """ + # Click on the datepicker to open calendar + datepicker = dash_dcc.find_element(datepicker_id) + datepicker.click() + + # Wait for calendar to open + popover = dash_dcc.find_element(".dash-datepicker-content") + + interactive_elements = [] + interactive_elements.extend(popover.find_elements(By.CSS_SELECTOR, "td")) + interactive_elements.extend(popover.find_elements(By.CSS_SELECTOR, "button")) + interactive_elements.extend(popover.find_elements(By.CSS_SELECTOR, "input")) + for el in interactive_elements: + try: + el.click() + sleep(0.05) + except Exception as e: + print(e) + assert not e, f"Unable to click on {el.tag_name})" + + +def test_mspv001_popover_visibility_when_app_is_smaller_than_popup(dash_dcc): + """ + This test clicks on each datepicker and verifies all calendar elements are clickable. + It verifies that the calendar popover is properly positioned and not clipped. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("Popover Visibility when app is only a few pixels tall"), + dcc.DatePickerSingle( + id="dps", + date=date(2024, 1, 15), + stay_open_on_select=True, + ), + dcc.DatePickerRange( + id="dpr", + start_date=date(2024, 1, 1), + end_date=date(2024, 1, 15), + stay_open_on_select=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False) + + # Wait for the page to load + dash_dcc.wait_for_element("#dps") + dash_dcc.wait_for_element("#dpr") + + # Test DatePickerSingle - click everything to verify all elements are accessible + click_everything_in_datepicker("#dps", dash_dcc) + + # Close the calendar by pressing escape + from selenium.webdriver.common.keys import Keys + + dps_input = dash_dcc.find_element("#dps") + dps_input.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2) + + # Test DatePickerRange - click everything to verify all elements are accessible + click_everything_in_datepicker("#dpr", dash_dcc) + + assert dash_dcc.get_logs() == [] + + +def test_mspv002_popover_visibility_when_app_is_scrolled_down(dash_dcc): + """ + This test clicks on each datepicker scrolled far down the page and verifies + that the popover contents are still visible + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("Popover Visibility when app is only a few pixels tall"), + html.P("", style={"height": "2000px"}), + dcc.DatePickerSingle( + id="dps", + date=date(2024, 1, 1), + stay_open_on_select=True, + ), + html.P("", style={"height": "2000px"}), + ], + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False) + + # Wait for the page to load + dash_dcc.wait_for_element("#dps") + + click_everything_in_datepicker("#dps", dash_dcc) + assert dash_dcc.get_logs() == [] + + +def test_mspv003_popover_contained_within_dash_app(dash_dcc): + """Test that datepicker popovers are visible and clickable when multiple pickers are present. + + This test clicks on each datepicker and selects the first day of the month that appears. + It verifies that the calendar popover is properly positioned and not clipped. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H1( + "Test popover is visible inside an embedded app", + style={"width": "200px"}, + ), + html.Div( + [ + html.H3("DatePicker Popover Visibility Test"), + dcc.DatePickerSingle(id="dps", date=date(2024, 1, 15)), + dcc.DatePickerRange( + id="dpr", + start_date=date(2024, 1, 1), + end_date=date(2024, 1, 15), + stay_open_on_select=True, + ), + ], + id="react-entry-point", + style={"overflow": "hidden", "display": "inline-flex"}, + ), + html.Div("This column is outside of embedded app"), + ], + style={ + "display": "inline-flex", + "minHeight": "600px", + }, + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + + # Wait for the page to load + dash_dcc.wait_for_element("#dpr") + + # Click everything in the datepicker to verify all elements are accessible + click_everything_in_datepicker("#dpr", dash_dcc) + + assert dash_dcc.get_logs() == [] + + +def test_mspv004_popover_inherits_container_styles(dash_dcc): + """Test that calendar days inherit font color and size from container. + + This test verifies that when a datepicker is placed inside a container with + specific font styles (color and size), the calendar days inherit those styles. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("DatePicker Style Inheritance Test"), + html.Div( + [ + dcc.DatePickerSingle(id="dps", date=date(2024, 1, 15)), + ], + style={"color": "limegreen", "fontSize": "24px"}, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + + # Wait for the page to load + dash_dcc.wait_for_element("#dps") + + # Click to open the calendar + dps_input = dash_dcc.find_element("#dps") + dps_input.click() + + # Wait for calendar to open + dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + + # Find a calendar day element (inside date, not outside days) + calendar_day = dash_dcc.find_element(".dash-datepicker-calendar-date-inside") + + # Get computed styles + font_size = calendar_day.value_of_css_property("font-size") + + # Font size should be 24px + assert font_size == "24px", "Expected calendar day to inherit its font size" + + assert dash_dcc.get_logs() == [] From 681a3791ddb3c688ec6a532b21e95837d37bdd2a Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Mon, 22 Dec 2025 16:08:47 -0700 Subject: [PATCH 2/7] Contain scrolling within dropdown popover --- .../dash-core-components/src/components/css/datepickers.css | 1 + components/dash-core-components/src/components/css/dropdown.css | 1 + 2 files changed, 2 insertions(+) diff --git a/components/dash-core-components/src/components/css/datepickers.css b/components/dash-core-components/src/components/css/datepickers.css index fa103b62de..375786ca8d 100644 --- a/components/dash-core-components/src/components/css/datepickers.css +++ b/components/dash-core-components/src/components/css/datepickers.css @@ -143,6 +143,7 @@ z-index: 500; box-shadow: 0px 10px 38px -10px var(--Dash-Shading-Strong), 0px 10px 20px -15px var(--Dash-Shading-Weak); + overscroll-behavior: contain; } .dash-datepicker-calendar-wrapper { diff --git a/components/dash-core-components/src/components/css/dropdown.css b/components/dash-core-components/src/components/css/dropdown.css index 8333f32506..f095f13421 100644 --- a/components/dash-core-components/src/components/css/dropdown.css +++ b/components/dash-core-components/src/components/css/dropdown.css @@ -84,6 +84,7 @@ z-index: 500; box-shadow: 0px 10px 38px -10px var(--Dash-Shading-Strong), 0px 10px 20px -15px var(--Dash-Shading-Weak); + overscroll-behavior: contain; } .dash-dropdown-value-count, From 95f098f9b00883852baeaafe30aeaa9df3770c71 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Mon, 22 Dec 2025 16:39:11 -0700 Subject: [PATCH 3/7] Fix floating point precision bug in input steppers --- .../src/components/Input.tsx | 14 ++- .../integration/input/test_number_input.py | 88 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/components/dash-core-components/src/components/Input.tsx b/components/dash-core-components/src/components/Input.tsx index 465a019b86..e099b2ee36 100644 --- a/components/dash-core-components/src/components/Input.tsx +++ b/components/dash-core-components/src/components/Input.tsx @@ -176,6 +176,11 @@ function Input({ (direction: 'increment' | 'decrement') => { const currentValue = parseFloat(input.current.value) || 0; const stepAsNum = parseFloat(step as string) || 1; + + // Count decimal places to avoid floating point precision issues + const decimalPlaces = (stepAsNum.toString().split('.')[1] || '') + .length; + const newValue = direction === 'increment' ? currentValue + stepAsNum @@ -196,8 +201,13 @@ function Input({ ); } - input.current.value = constrainedValue.toString(); - setValue(constrainedValue.toString()); + // Round to the step's decimal precision + const roundedValue = parseFloat( + constrainedValue.toFixed(decimalPlaces) + ); + + input.current.value = roundedValue.toString(); + setValue(roundedValue.toString()); onEvent(); }, [step, props.min, props.max, onEvent] diff --git a/components/dash-core-components/tests/integration/input/test_number_input.py b/components/dash-core-components/tests/integration/input/test_number_input.py index 846c25dfd9..01deb79287 100644 --- a/components/dash-core-components/tests/integration/input/test_number_input.py +++ b/components/dash-core-components/tests/integration/input/test_number_input.py @@ -1,5 +1,6 @@ import time import sys +import pytest from dash import Dash, Input, Output, html, dcc from selenium.webdriver.common.keys import Keys @@ -148,6 +149,93 @@ def update_output(val): assert dash_dcc.get_logs() == [] +@pytest.mark.parametrize("step", [0.1, 0.01, 0.001, 0.0001]) +def test_inni006_stepper_floating_point_precision(dash_dcc, step): + """Test that stepper increments/decrements with decimal steps don't accumulate floating point errors.""" + + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Input(id="decimal-input", value=0, type="number", step=step), + html.Div(id="output"), + ] + ) + + @app.callback(Output("output", "children"), [Input("decimal-input", "value")]) + def update_output(val): + return val + + dash_dcc.start_server(app) + increment_btn = dash_dcc.find_element(".dash-stepper-increment") + decrement_btn = dash_dcc.find_element(".dash-stepper-decrement") + + # Determine decimal places for formatting + decimal_places = len(str(step).split(".")[1]) if "." in str(step) else 0 + num_clicks = 9 + + # Test increment: without precision fix, accumulates floating point errors (e.g., 0.30000000000000004) + for i in range(1, num_clicks + 1): + increment_btn.click() + expected = format(step * i, f".{decimal_places}f") + dash_dcc.wait_for_text_to_equal("#output", expected) + + # Test decrement: should go back down through the same values + for i in range(num_clicks - 1, 0, -1): + decrement_btn.click() + expected = format(step * i, f".{decimal_places}f") + dash_dcc.wait_for_text_to_equal("#output", expected) + + # One more decrement to get back to 0 + decrement_btn.click() + dash_dcc.wait_for_text_to_equal("#output", "0") + + assert dash_dcc.get_logs() == [] + + +@pytest.mark.parametrize("step", [0.00001, 0.000001]) +def test_inni007_stepper_very_small_steps(dash_dcc, step): + """Test that stepper works correctly with very small decimal steps.""" + + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Input(id="decimal-input", value=0, type="number", step=step), + html.Div(id="output"), + ] + ) + + @app.callback(Output("output", "children"), [Input("decimal-input", "value")]) + def update_output(val): + return val + + dash_dcc.start_server(app) + increment_btn = dash_dcc.find_element(".dash-stepper-increment") + decrement_btn = dash_dcc.find_element(".dash-stepper-decrement") + + # For very small steps, format with enough precision then strip trailing zeros + step_str = f"{step:.10f}".rstrip("0").rstrip(".") + decimal_places = len(step_str.split(".")[1]) if "." in step_str else 0 + num_clicks = 5 + + # Test increment + for i in range(1, num_clicks + 1): + increment_btn.click() + expected = f"{step * i:.{decimal_places}f}".rstrip("0").rstrip(".") + dash_dcc.wait_for_text_to_equal("#output", expected) + + # Test decrement + for i in range(num_clicks - 1, 0, -1): + decrement_btn.click() + expected = f"{step * i:.{decimal_places}f}".rstrip("0").rstrip(".") + dash_dcc.wait_for_text_to_equal("#output", expected) + + # One more decrement to get back to 0 + decrement_btn.click() + dash_dcc.wait_for_text_to_equal("#output", "0") + + assert dash_dcc.get_logs() == [] + + def test_inni010_valid_numbers(dash_dcc, ninput_app): dash_dcc.start_server(ninput_app) for num, op in ( From c9dbad0dbbde13d6f116fa55d3e715188f5ef03f Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Tue, 30 Dec 2025 12:22:27 -0700 Subject: [PATCH 4/7] Add missing implementation for `with_portal` and `with_full_screen_portal` on datepickers --- .../src/components/css/datepickers.css | 69 +++ .../src/fragments/DatePickerRange.tsx | 30 +- .../src/fragments/DatePickerSingle.tsx | 30 +- .../src/fragments/Dropdown.tsx | 7 +- .../tests/integration/calendar/test_portal.py | 426 ++++++++++++++++++ 5 files changed, 554 insertions(+), 8 deletions(-) create mode 100644 components/dash-core-components/tests/integration/calendar/test_portal.py diff --git a/components/dash-core-components/src/components/css/datepickers.css b/components/dash-core-components/src/components/css/datepickers.css index 375786ca8d..6f0043cf74 100644 --- a/components/dash-core-components/src/components/css/datepickers.css +++ b/components/dash-core-components/src/components/css/datepickers.css @@ -146,6 +146,74 @@ overscroll-behavior: contain; } +.dash-datepicker + [data-radix-popper-content-wrapper]:has(.dash-datepicker-portal) { + transform: none !important; +} + +.dash-datepicker-portal { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + max-width: 100vw; + display: flex; + align-items: center; + justify-content: center; + background: var(--Dash-Shading-Strong); + border: none; + box-shadow: none; + overflow: visible; + padding: 0; + pointer-events: none; +} + +.dash-datepicker-portal .dash-datepicker-calendar-wrapper { + background: var(--Dash-Fill-Inverse-Strong); + border-radius: var(--Dash-Spacing); + border: 1px solid var(--Dash-Stroke-Strong); + padding: 16px; + box-shadow: 0px 10px 38px -10px var(--Dash-Shading-Strong), + 0px 10px 20px -15px var(--Dash-Shading-Weak); + z-index: 1; + pointer-events: auto; + width: fit-content; + max-width: 95vw; +} + +.dash-datepicker-fullscreen { + pointer-events: auto; + background: var(--Dash-Fill-Inverse-Strong); +} + +.dash-datepicker-close-button { + position: fixed; + top: calc(var(--Dash-Spacing) * 2); + right: calc(var(--Dash-Spacing) * 2); + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + background: var(--Dash-Fill-Inverse-Strong); + border: none; + border-radius: var(--Dash-Spacing); + color: var(--Dash-Text-Strong); + cursor: pointer; + z-index: 501; + pointer-events: auto; +} + +.dash-datepicker-close-button:hover { + background: var(--Dash-Fill-Weak); + color: var(--Dash-Fill-Interactive-Strong); +} + +.dash-datepicker-close-button:focus { + outline: 2px solid var(--Dash-Fill-Interactive-Strong); + outline-offset: 2px; +} + .dash-datepicker-calendar-wrapper { display: flex; flex-direction: column; @@ -156,6 +224,7 @@ display: flex; align-items: flex-start; gap: calc(var(--Dash-Spacing) * 4); + flex-wrap: wrap; } .dash-datepicker-controls { diff --git a/components/dash-core-components/src/fragments/DatePickerRange.tsx b/components/dash-core-components/src/fragments/DatePickerRange.tsx index bccb719cc7..12c6670608 100644 --- a/components/dash-core-components/src/fragments/DatePickerRange.tsx +++ b/components/dash-core-components/src/fragments/DatePickerRange.tsx @@ -51,6 +51,8 @@ const DatePickerRange = ({ end_date_id, start_date_placeholder_text = 'Start Date', end_date_placeholder_text = 'End Date', + with_portal = false, + with_full_screen_portal = false, }: DatePickerRangeProps) => { const [internalStartDate, setInternalStartDate] = useState( strAsDate(start_date) @@ -102,6 +104,7 @@ const DatePickerRange = ({ const startInputRef = useRef(null); const endInputRef = useRef(null); const calendarRef = useRef(null); + const hasPortal = with_portal || with_full_screen_portal; useEffect(() => { setInternalStartDate(strAsDate(start_date)); @@ -381,9 +384,21 @@ const DatePickerRange = ({ e.preventDefault() + : undefined + } onOpenAutoFocus={e => e.preventDefault()} onCloseAutoFocus={e => { e.preventDefault(); @@ -404,6 +419,15 @@ const DatePickerRange = ({ } }} > + {with_full_screen_portal && ( + + )} { const [internalDate, setInternalDate] = useState(strAsDate(date)); const direction = is_RTL @@ -61,6 +63,7 @@ const DatePickerSingle = ({ const containerRef = useRef(null); const inputRef = useRef(null); const calendarRef = useRef(null); + const hasPortal = with_portal || with_full_screen_portal; useEffect(() => { setInternalDate(strAsDate(date)); @@ -200,9 +203,21 @@ const DatePickerSingle = ({ e.preventDefault() + : undefined + } onOpenAutoFocus={e => e.preventDefault()} onCloseAutoFocus={e => { e.preventDefault(); @@ -212,6 +227,15 @@ const DatePickerSingle = ({ } }} > + {with_full_screen_portal && ( + + )} { } }} className={`dash-dropdown ${className ?? ''}`} - style={style} aria-labelledby={`${accessibleId}-value-count ${accessibleId}-value`} aria-haspopup="listbox" aria-expanded={isOpen} @@ -540,7 +539,11 @@ const Dropdown = (props: DropdownProps) => { ); return ( -
+
{popover}
); diff --git a/components/dash-core-components/tests/integration/calendar/test_portal.py b/components/dash-core-components/tests/integration/calendar/test_portal.py new file mode 100644 index 0000000000..69ebf4b984 --- /dev/null +++ b/components/dash-core-components/tests/integration/calendar/test_portal.py @@ -0,0 +1,426 @@ +from datetime import date +from dash import Dash, html, dcc +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from time import sleep +import pytest + + +def click_everything_in_datepicker(datepicker_id, dash_dcc): + """Click on every clickable element in a datepicker calendar. + + Args: + datepicker_id: CSS selector for the datepicker element (e.g., "#dpr") + dash_dcc: The dash_dcc fixture + """ + # Click on the datepicker to open calendar + datepicker = dash_dcc.find_element(datepicker_id) + datepicker.click() + + # Wait for calendar to open + popover = dash_dcc.find_element(".dash-datepicker-content") + + interactive_elements = [] + interactive_elements.extend(popover.find_elements(By.CSS_SELECTOR, "td")) + interactive_elements.extend(popover.find_elements(By.CSS_SELECTOR, "input")) + + buttons = reversed( + popover.find_elements(By.CSS_SELECTOR, "button") + ) # reversed so that "close" button will be clicked after all other buttons + interactive_elements.extend(buttons) # Add close buttons last + + for el in interactive_elements: + try: + el.click() + sleep(0.05) + except Exception as e: + print(e) + assert not e, f"Unable to click on {el.tag_name})" + + +def test_dppt000_datepicker_single_default(dash_dcc): + """Test DatePickerSingle with default (no portal) configuration. + + Verifies that the calendar opens without portal and all elements are clickable. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("DatePickerSingle Default"), + dcc.DatePickerSingle( + id="dps-default", + date=date(2024, 1, 15), + stay_open_on_select=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + + dash_dcc.wait_for_element("#dps-default") + + click_everything_in_datepicker("#dps-default", dash_dcc) + + dps_input = dash_dcc.find_element("#dps-default") + dps_input.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2) + + assert dash_dcc.get_logs() == [] + + +def test_dppt001_datepicker_single_with_portal(dash_dcc): + """Test DatePickerSingle with with_portal=True. + + Verifies that the calendar opens in a portal (document.body) and all + elements are clickable. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("DatePickerSingle with Portal"), + dcc.DatePickerSingle( + id="dps-portal", + date=date(2024, 1, 15), + stay_open_on_select=True, + with_portal=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + + # Wait for the page to load + dash_dcc.wait_for_element("#dps-portal") + + # Test DatePickerSingle with portal - click everything to verify all elements are accessible + click_everything_in_datepicker("#dps-portal", dash_dcc) + + # Close the calendar by pressing escape + dps_input = dash_dcc.find_element("#dps-portal") + dps_input.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2) + + assert dash_dcc.get_logs() == [] + + +def test_dppt006_fullscreen_portal_close_button_keyboard(dash_dcc): + """Test fullscreen portal dismiss behavior and keyboard accessibility. + + Verifies clicking background doesn't close the portal and close button + is keyboard-accessible. + """ + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerSingle( + id="dps-fullscreen", + date=date(2024, 1, 15), + with_full_screen_portal=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + dash_dcc.wait_for_element("#dps-fullscreen") + + dps = dash_dcc.find_element("#dps-fullscreen") + dps.click() + + popover = dash_dcc.find_element(".dash-datepicker-content") + assert popover.is_displayed() + + action = ActionChains(dash_dcc.driver) + action.move_to_element_with_offset(popover, 10, 10).click().perform() + sleep(0.2) + + popover = dash_dcc.find_element(".dash-datepicker-content") + assert ( + popover.is_displayed() + ), "Fullscreen portal should not close when clicking background" + + dash_dcc.find_element(".dash-datepicker-close-button") + + action.send_keys(Keys.TAB).perform() + sleep(0.1) + action.send_keys(Keys.ENTER).perform() + sleep(0.2) + + dash_dcc.wait_for_no_elements(".dash-datepicker-content", timeout=2) + assert dash_dcc.get_logs() == [] + + +def test_dppt007_portal_close_by_clicking_outside(dash_dcc): + """Test regular portal closes when clicking outside the calendar.""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerSingle( + id="dps-portal", + date=date(2024, 1, 15), + with_portal=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + dash_dcc.wait_for_element("#dps-portal") + + dps = dash_dcc.find_element("#dps-portal") + dps.click() + + popover = dash_dcc.find_element(".dash-datepicker-content") + assert popover.is_displayed() + + popover.click() + sleep(0.2) + + dash_dcc.wait_for_no_elements(".dash-datepicker-content", timeout=2) + assert dash_dcc.get_logs() == [] + + +def test_dppt001a_datepicker_range_default(dash_dcc): + """Test DatePickerRange with default (no portal) configuration. + + Verifies that the calendar opens without portal and all elements are clickable. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("DatePickerRange Default"), + dcc.DatePickerRange( + id="dpr-default", + start_date=date(2024, 1, 1), + end_date=date(2024, 1, 15), + stay_open_on_select=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + + dash_dcc.wait_for_element("#dpr-default") + + click_everything_in_datepicker("#dpr-default", dash_dcc) + + dpr_input = dash_dcc.find_element("#dpr-default") + 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_dppt002_datepicker_range_with_portal(dash_dcc): + """Test DatePickerRange with with_portal=True. + + Verifies that the calendar opens in a portal (document.body) and all + elements are clickable. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("DatePickerRange with Portal"), + dcc.DatePickerRange( + id="dpr-portal", + start_date=date(2024, 1, 1), + end_date=date(2024, 1, 15), + stay_open_on_select=True, + with_portal=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + + # Wait for the page to load + dash_dcc.wait_for_element("#dpr-portal") + + # Test DatePickerRange with portal - click everything to verify all elements are accessible + click_everything_in_datepicker("#dpr-portal", dash_dcc) + + # Close the calendar by pressing escape + dpr_input = dash_dcc.find_element("#dpr-portal") + 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. + + Verifies that the calendar opens in a full-screen portal overlay and all + elements are clickable. Also verifies that the fullscreen CSS class is applied. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("DatePickerSingle with Full Screen Portal"), + dcc.DatePickerSingle( + id="dps-fullscreen", + date=date(2024, 1, 15), + stay_open_on_select=True, + with_full_screen_portal=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + + # Wait for the page to load + dash_dcc.wait_for_element("#dps-fullscreen") + + # Click to open the calendar + dps = dash_dcc.find_element("#dps-fullscreen") + dps.click() + + # Wait for calendar to open + popover = dash_dcc.find_element(".dash-datepicker-content") + + # Verify fullscreen class is applied + assert "dash-datepicker-fullscreen" in popover.get_attribute( + "class" + ), "Full screen portal should have dash-datepicker-fullscreen class" + + # Verify the popover has fixed positioning (full screen overlay) + position = popover.value_of_css_property("position") + assert position == "fixed", "Full screen portal should use fixed positioning" + + # Close to prepare for click everything test + dps.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2) + + # 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): + """Test DatePickerRange with with_full_screen_portal=True. + + Verifies that the calendar opens in a full-screen portal overlay and all + elements are clickable. Also verifies that the fullscreen CSS class is applied. + + Note: Marked as flaky due to headless Chrome layout issues with wide calendars + (2 months shown by default in DatePickerRange). Test passes consistently in + non-headless mode. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("DatePickerRange with Full Screen Portal"), + dcc.DatePickerRange( + id="dpr-fullscreen", + start_date=date(2024, 1, 1), + end_date=date(2024, 1, 15), + stay_open_on_select=True, + with_full_screen_portal=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + + # Wait for the page to load + dash_dcc.wait_for_element("#dpr-fullscreen") + + # Click to open the calendar + dpr = dash_dcc.find_element("#dpr-fullscreen") + dpr.click() + + # Wait for calendar to open + popover = dash_dcc.find_element(".dash-datepicker-content") + + # Verify fullscreen class is applied + assert "dash-datepicker-fullscreen" in popover.get_attribute( + "class" + ), "Full screen portal should have dash-datepicker-fullscreen class" + + # Verify the popover has fixed positioning (full screen overlay) + position = popover.value_of_css_property("position") + assert position == "fixed", "Full screen portal should use fixed positioning" + + # Close to prepare for click everything test + dpr.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2) + + # 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. + + Verifies that default datepickers don't have portal classes, while + with_portal=True datepickers have the portal class but not fullscreen class. + """ + app = Dash(__name__) + + app.layout = html.Div( + [ + html.H3("Default (no portal)"), + dcc.DatePickerSingle( + id="dps-default", + date=date(2024, 1, 15), + ), + html.H3("With portal", style={"marginTop": "50px"}), + dcc.DatePickerSingle( + id="dps-with-portal", + date=date(2024, 1, 15), + with_portal=True, + ), + ] + ) + + dash_dcc.start_server(app, debug=True, use_reloader=False, dev_tools_ui=False) + + # Wait for the page to load + dash_dcc.wait_for_element("#dps-default") + dash_dcc.wait_for_element("#dps-with-portal") + + # Open default datepicker + dps_default = dash_dcc.find_element("#dps-default") + dps_default.click() + + # Wait for calendar to open + popover_default = dash_dcc.find_element(".dash-datepicker-content") + + # Verify it doesn't have fullscreen class + assert "dash-datepicker-fullscreen" not in popover_default.get_attribute( + "class" + ), "Default datepicker should not have fullscreen class" + + # Close default + dps_default.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-content", timeout=2) + + # Open portal datepicker + dps_portal = dash_dcc.find_element("#dps-with-portal") + dps_portal.click() + + # Wait for calendar to open + popover_portal = dash_dcc.find_element(".dash-datepicker-content") + + # Verify it has portal class but not fullscreen class + assert "dash-datepicker-portal" in popover_portal.get_attribute( + "class" + ), "Portal should have dash-datepicker-portal class" + assert "dash-datepicker-fullscreen" not in popover_portal.get_attribute( + "class" + ), "Portal (non-fullscreen) should not have fullscreen class" + + # Verify it uses fixed positioning (both portal types use fixed positioning) + position = popover_portal.value_of_css_property("position") + assert position == "fixed", "Portal should use fixed positioning" + + assert dash_dcc.get_logs() == [] From eca3197dc2c325bdfa14b66b0366afe3d66574b7 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Tue, 30 Dec 2025 12:33:33 -0700 Subject: [PATCH 5/7] Fix test --- .../tests/integration/misc/test_popover_visibility.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/components/dash-core-components/tests/integration/misc/test_popover_visibility.py b/components/dash-core-components/tests/integration/misc/test_popover_visibility.py index 23a859db83..aaf0d1b20e 100644 --- a/components/dash-core-components/tests/integration/misc/test_popover_visibility.py +++ b/components/dash-core-components/tests/integration/misc/test_popover_visibility.py @@ -74,8 +74,6 @@ def test_mspv001_popover_visibility_when_app_is_smaller_than_popup(dash_dcc): # Test DatePickerRange - click everything to verify all elements are accessible click_everything_in_datepicker("#dpr", dash_dcc) - assert dash_dcc.get_logs() == [] - def test_mspv002_popover_visibility_when_app_is_scrolled_down(dash_dcc): """ @@ -103,7 +101,6 @@ def test_mspv002_popover_visibility_when_app_is_scrolled_down(dash_dcc): dash_dcc.wait_for_element("#dps") click_everything_in_datepicker("#dps", dash_dcc) - assert dash_dcc.get_logs() == [] def test_mspv003_popover_contained_within_dash_app(dash_dcc): @@ -150,8 +147,6 @@ def test_mspv003_popover_contained_within_dash_app(dash_dcc): # Click everything in the datepicker to verify all elements are accessible click_everything_in_datepicker("#dpr", dash_dcc) - assert dash_dcc.get_logs() == [] - def test_mspv004_popover_inherits_container_styles(dash_dcc): """Test that calendar days inherit font color and size from container. @@ -193,5 +188,3 @@ def test_mspv004_popover_inherits_container_styles(dash_dcc): # Font size should be 24px assert font_size == "24px", "Expected calendar day to inherit its font size" - - assert dash_dcc.get_logs() == [] From b56ea7293b9c0d5b4668a7346e6fd222812745b1 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Tue, 30 Dec 2025 12:33:33 -0700 Subject: [PATCH 6/7] Fix test --- .../tests/integration/misc/test_popover_visibility.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/components/dash-core-components/tests/integration/misc/test_popover_visibility.py b/components/dash-core-components/tests/integration/misc/test_popover_visibility.py index 23a859db83..34b310f197 100644 --- a/components/dash-core-components/tests/integration/misc/test_popover_visibility.py +++ b/components/dash-core-components/tests/integration/misc/test_popover_visibility.py @@ -56,6 +56,7 @@ def test_mspv001_popover_visibility_when_app_is_smaller_than_popup(dash_dcc): ) dash_dcc.start_server(app, debug=True, use_reloader=False) + dash_dcc.driver.set_window_size(1280, 1024) # Wait for the page to load dash_dcc.wait_for_element("#dps") @@ -74,8 +75,6 @@ def test_mspv001_popover_visibility_when_app_is_smaller_than_popup(dash_dcc): # Test DatePickerRange - click everything to verify all elements are accessible click_everything_in_datepicker("#dpr", dash_dcc) - assert dash_dcc.get_logs() == [] - def test_mspv002_popover_visibility_when_app_is_scrolled_down(dash_dcc): """ @@ -103,7 +102,6 @@ def test_mspv002_popover_visibility_when_app_is_scrolled_down(dash_dcc): dash_dcc.wait_for_element("#dps") click_everything_in_datepicker("#dps", dash_dcc) - assert dash_dcc.get_logs() == [] def test_mspv003_popover_contained_within_dash_app(dash_dcc): @@ -150,8 +148,6 @@ def test_mspv003_popover_contained_within_dash_app(dash_dcc): # Click everything in the datepicker to verify all elements are accessible click_everything_in_datepicker("#dpr", dash_dcc) - assert dash_dcc.get_logs() == [] - def test_mspv004_popover_inherits_container_styles(dash_dcc): """Test that calendar days inherit font color and size from container. @@ -193,5 +189,3 @@ def test_mspv004_popover_inherits_container_styles(dash_dcc): # Font size should be 24px assert font_size == "24px", "Expected calendar day to inherit its font size" - - assert dash_dcc.get_logs() == [] From 5f76332ab1a43be0171e2f052a08f9a596c57066 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Tue, 30 Dec 2025 12:33:33 -0700 Subject: [PATCH 7/7] Fix test --- .../src/fragments/DatePickerRange.tsx | 4 ++-- .../src/fragments/DatePickerSingle.tsx | 2 +- .../tests/integration/misc/test_popover_visibility.py | 8 +------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/components/dash-core-components/src/fragments/DatePickerRange.tsx b/components/dash-core-components/src/fragments/DatePickerRange.tsx index bccb719cc7..c869a40bbc 100644 --- a/components/dash-core-components/src/fragments/DatePickerRange.tsx +++ b/components/dash-core-components/src/fragments/DatePickerRange.tsx @@ -333,7 +333,7 @@ const DatePickerRange = ({ id={start_date_id || accessibleId} inputClassName="dash-datepicker-input dash-datepicker-start-date" value={startInputValue} - onChange={e => setStartInputValue(e.target.value)} + onChange={e => setStartInputValue(e.target?.value)} onKeyDown={handleStartInputKeyDown} onFocus={() => { if (isCalendarOpen) { @@ -354,7 +354,7 @@ const DatePickerRange = ({ id={end_date_id || accessibleId + '-end-date'} inputClassName="dash-datepicker-input dash-datepicker-end-date" value={endInputValue} - onChange={e => setEndInputValue(e.target.value)} + onChange={e => setEndInputValue(e.target?.value)} onKeyDown={handleEndInputKeyDown} onFocus={() => { if (isCalendarOpen) { diff --git a/components/dash-core-components/src/fragments/DatePickerSingle.tsx b/components/dash-core-components/src/fragments/DatePickerSingle.tsx index 3abf7f034f..4c3a4fde6b 100644 --- a/components/dash-core-components/src/fragments/DatePickerSingle.tsx +++ b/components/dash-core-components/src/fragments/DatePickerSingle.tsx @@ -177,7 +177,7 @@ const DatePickerSingle = ({ id={accessibleId} inputClassName="dash-datepicker-input dash-datepicker-end-date" value={inputValue} - onChange={e => setInputValue(e.target.value)} + onChange={e => setInputValue(e.target?.value)} onKeyDown={handleInputKeyDown} placeholder={placeholder} disabled={disabled} diff --git a/components/dash-core-components/tests/integration/misc/test_popover_visibility.py b/components/dash-core-components/tests/integration/misc/test_popover_visibility.py index 23a859db83..34b310f197 100644 --- a/components/dash-core-components/tests/integration/misc/test_popover_visibility.py +++ b/components/dash-core-components/tests/integration/misc/test_popover_visibility.py @@ -56,6 +56,7 @@ def test_mspv001_popover_visibility_when_app_is_smaller_than_popup(dash_dcc): ) dash_dcc.start_server(app, debug=True, use_reloader=False) + dash_dcc.driver.set_window_size(1280, 1024) # Wait for the page to load dash_dcc.wait_for_element("#dps") @@ -74,8 +75,6 @@ def test_mspv001_popover_visibility_when_app_is_smaller_than_popup(dash_dcc): # Test DatePickerRange - click everything to verify all elements are accessible click_everything_in_datepicker("#dpr", dash_dcc) - assert dash_dcc.get_logs() == [] - def test_mspv002_popover_visibility_when_app_is_scrolled_down(dash_dcc): """ @@ -103,7 +102,6 @@ def test_mspv002_popover_visibility_when_app_is_scrolled_down(dash_dcc): dash_dcc.wait_for_element("#dps") click_everything_in_datepicker("#dps", dash_dcc) - assert dash_dcc.get_logs() == [] def test_mspv003_popover_contained_within_dash_app(dash_dcc): @@ -150,8 +148,6 @@ def test_mspv003_popover_contained_within_dash_app(dash_dcc): # Click everything in the datepicker to verify all elements are accessible click_everything_in_datepicker("#dpr", dash_dcc) - assert dash_dcc.get_logs() == [] - def test_mspv004_popover_inherits_container_styles(dash_dcc): """Test that calendar days inherit font color and size from container. @@ -193,5 +189,3 @@ def test_mspv004_popover_inherits_container_styles(dash_dcc): # Font size should be 24px assert font_size == "24px", "Expected calendar day to inherit its font size" - - assert dash_dcc.get_logs() == []