diff --git a/components/dash-core-components/src/components/Input.tsx b/components/dash-core-components/src/components/Input.tsx index 86641c7e57..465a019b86 100644 --- a/components/dash-core-components/src/components/Input.tsx +++ b/components/dash-core-components/src/components/Input.tsx @@ -253,7 +253,7 @@ function Input({ }) as Pick, HTMLInputProps>; const isNumberInput = type === HTMLInputTypes.number; - const currentNumericValue = convert(input.current.value || '0'); + const currentNumericValue = parseFloat(String(value ?? 0)) || 0; const minValue = convert(props.min); const maxValue = convert(props.max); const isDecrementDisabled = diff --git a/components/dash-core-components/src/components/css/button.css b/components/dash-core-components/src/components/css/button.css index 92594df6e3..2422c6e9df 100644 --- a/components/dash-core-components/src/components/css/button.css +++ b/components/dash-core-components/src/components/css/button.css @@ -11,6 +11,8 @@ border: 1px solid var(--Dash-Fill-Interactive-Strong); box-sizing: border-box; vertical-align: middle; + font-family: inherit; + font-size: inherit; } /* Hover state - stronger background */ @@ -28,15 +30,12 @@ /* Keyboard focus - inverted colors */ .dash-button:focus-visible { - outline: none; - background: var(--Dash-Fill-Interactive-Strong); - color: var(--Dash-Fill-Inverse-Strong); + outline: 1px solid var(--Dash-Fill-Interactive-Strong); } /* Hover after keyboard focus - keep inverted but acknowledge hover */ .dash-button:focus-visible:hover { - background: var(--Dash-Fill-Interactive-Strong); - color: var(--Dash-Fill-Inverse-Strong); + outline: 1px solid var(--Dash-Fill-Interactive-Strong); } /* Active state after keyboard focus - inverted colors */ diff --git a/components/dash-core-components/src/components/css/datepickers.css b/components/dash-core-components/src/components/css/datepickers.css index 57e4007897..1d153a3c09 100644 --- a/components/dash-core-components/src/components/css/datepickers.css +++ b/components/dash-core-components/src/components/css/datepickers.css @@ -56,6 +56,8 @@ padding: 0; background-color: inherit; color: inherit; + font-family: inherit; + font-size: inherit; } .dash-datepicker-input::selection, @@ -68,6 +70,10 @@ border: 1px solid var(--Dash-Fill-Interactive-Strong); } +.dash-datepicker-input-wrapper:has(:focus-visible) { + outline: 1px solid var(--Dash-Fill-Interactive-Strong); +} + .dash-datepicker-input-wrapper-disabled { opacity: 0.6; cursor: not-allowed; @@ -85,33 +91,6 @@ color: var(--Dash-Text-Disabled); } -.dash-datepicker-trigger-button { - padding: var(--Dash-Spacing); - background: transparent; - border: none; - border-radius: 4px; - color: var(--Dash-Text-Strong); - cursor: pointer; -} - -.dash-datepicker-trigger-button:hover { - background: var(--Dash-Fill-Weak); -} - -.dash-datepicker-trigger-button:focus-visible { - outline: 2px solid var(--Dash-Fill-Interactive-Strong); - outline-offset: 2px; -} - -.dash-datepicker-trigger-button[aria-expanded='true'] { - background: var(--Dash-Fill-Weak); -} - -.dash-datepicker-trigger-icon { - width: 16px; - height: 16px; -} - .dash-datepicker-caret-icon { color: var(--Dash-Text-Strong); fill: var(--Dash-Text-Strong); @@ -176,9 +155,9 @@ display: flex; align-items: center; justify-content: center; - gap: var(--Dash-Spacing); + gap: calc(var(--Dash-Spacing) * 2); margin-bottom: calc(var(--Dash-Spacing) * 2); - font-size: 14px; + font-size: inherit; } .dash-datepicker-controls .dash-dropdown { @@ -220,7 +199,6 @@ .dash-datepicker-month-nav:focus-visible { outline: 2px solid var(--Dash-Fill-Interactive-Strong); - outline-offset: 2px; } .dash-datepicker-month-nav:disabled { diff --git a/components/dash-core-components/src/components/css/dcc.css b/components/dash-core-components/src/components/css/dcc.css index 9eb28ed724..9e75aafd69 100644 --- a/components/dash-core-components/src/components/css/dcc.css +++ b/components/dash-core-components/src/components/css/dcc.css @@ -8,7 +8,7 @@ --Dash-Text-Primary: rgba(0, 18, 77, 0.87); --Dash-Text-Strong: rgba(0, 9, 38, 0.9); --Dash-Text-Weak: rgba(0, 12, 51, 0.65); - --Dash-Text-Disabled: rgba(0, 21, 89, 0.3); + --Dash-Text-Disabled: rgba(0, 21, 89, 0.6); --Dash-Fill-Primary-Hover: rgba(0, 18, 77, 0.04); --Dash-Fill-Primary-Active: rgba(0, 18, 77, 0.08); --Dash-Fill-Disabled: rgba(0, 24, 102, 0.1); diff --git a/components/dash-core-components/src/components/css/dropdown.css b/components/dash-core-components/src/components/css/dropdown.css index f36b364471..4bd605a969 100644 --- a/components/dash-core-components/src/components/css/dropdown.css +++ b/components/dash-core-components/src/components/css/dropdown.css @@ -8,6 +8,7 @@ outline: none; width: 100%; cursor: pointer; + font-family: inherit; font-size: inherit; overflow: hidden; accent-color: var(--Dash-Fill-Interactive-Strong); @@ -46,6 +47,7 @@ .dash-dropdown:focus { border: 1px solid var(--Dash-Fill-Interactive-Strong); + outline: 1px solid var(--Dash-Fill-Interactive-Strong); } .dash-dropdown:disabled { @@ -102,8 +104,11 @@ .dash-dropdown-value-count { line-height: 18px; - padding: 0 2px; + padding: 4px; + border-radius: 4px; + color: var(--Dash-Text-Weak); background: var(--Dash-Fill-Weak); + font-size: 0.875em; } .dash-dropdown-search-container { @@ -118,12 +123,13 @@ .dash-dropdown-search-container:focus-within { border-color: var(--Dash-Fill-Interactive-Strong); + outline: 1px solid var(--Dash-Fill-Interactive-Strong); } .dash-dropdown-search-icon, .dash-dropdown-clear { - width: calc(var(--Dash-Spacing) * 3); - height: calc(var(--Dash-Spacing) * 3); + width: 1em; + height: 1em; } .dash-dropdown-search-container:focus-within .dash-dropdown-search-icon { @@ -160,6 +166,8 @@ color: var(--Dash-Text-Strong); outline: none; padding: 0; + font-family: inherit; + font-size: inherit; /* Hide the "x" clear button in search inputs */ &::-webkit-search-cancel-button { @@ -186,11 +194,12 @@ background: none; border: none; cursor: pointer; - font-size: 14px; + font-family: inherit; + font-size: 0.875em; font-weight: 600; padding: 0; text-decoration: none; - color: var(--Dash-Text-Disabled); + color: var(--Dash-Text-Weak); white-space: nowrap; accent-color: var(--Dash-Fill-Interactive-Strong); outline-color: var(--Dash-Fill-Interactive-Strong); @@ -213,6 +222,15 @@ box-shadow: 0 -1px 0 0 var(--Dash-Fill-Disabled) inset; } +.dash-dropdown-option + .dash-options-list-option-wrapper:has(input[type='radio']) { + /* radio buttons are used in single-select dropdowns to aid keyboard + * selection and screen readers, but visually, they are hidden + */ + width: 0; + overflow: hidden; +} + /* Positioning container for the dropdown */ .dash-dropdown-wrapper { position: relative; diff --git a/components/dash-core-components/src/components/css/input.css b/components/dash-core-components/src/components/css/input.css index 0f5ba23a9e..1adcdb8e81 100644 --- a/components/dash-core-components/src/components/css/input.css +++ b/components/dash-core-components/src/components/css/input.css @@ -19,6 +19,10 @@ border: 1px solid var(--Dash-Fill-Interactive-Strong); } +.dash-input-container:has(:focus-visible) { + outline: 1px solid var(--Dash-Fill-Interactive-Strong); +} + .dash-input-container:has(.dash-input-element:disabled) { opacity: 0.6; cursor: not-allowed; @@ -46,6 +50,8 @@ z-index: 1; order: 2; accent-color: var(--Dash-Fill-Interactive-Strong); + font-family: inherit; + font-size: inherit; } .dash-input-element::selection, @@ -139,5 +145,5 @@ } input.dash-input-element:invalid { - outline: solid red; + color: red; } diff --git a/components/dash-core-components/src/components/css/optionslist.css b/components/dash-core-components/src/components/css/optionslist.css index ba285604fa..c1b5227c15 100644 --- a/components/dash-core-components/src/components/css/optionslist.css +++ b/components/dash-core-components/src/components/css/optionslist.css @@ -4,7 +4,7 @@ color: var(--Dash-Text-Strong); cursor: pointer; display: flex; - align-items: center; + align-items: baseline; user-select: none; } @@ -19,6 +19,7 @@ cursor: not-allowed; } +.dash-options-list-option-wrapper, .dash-options-list-option-text { display: flex; align-items: center; diff --git a/components/dash-core-components/src/components/css/sliders.css b/components/dash-core-components/src/components/css/sliders.css index 0ffc76d211..898fa4d6be 100644 --- a/components/dash-core-components/src/components/css/sliders.css +++ b/components/dash-core-components/src/components/css/sliders.css @@ -7,14 +7,14 @@ touch-action: none; width: 100%; height: 14px; - padding: 18px 0 18px 0; + padding: 17px 0 17px 0; box-sizing: border-box; /* Override Radix's default margin/padding behavior */ --radix-slider-thumb-width: 16px; } .dash-slider-root.has-marks { - padding: 8px 0 28px 0; + padding: 6px 0 28px 0; } .dash-slider-root[data-orientation='vertical'].has-marks { @@ -75,7 +75,7 @@ } .dash-slider-thumb:focus { - outline: 1px solid var(--Dash-Fill-Interactive-Strong); + outline: 2px solid var(--Dash-Fill-Interactive-Strong); } .dash-slider-thumb:focus .dash-slider-tooltip, @@ -210,11 +210,19 @@ .dash-range-slider-input { min-width: 5cqw; /* 5% of container width */ - max-width: 15cqw; /* 15% of container width */ + max-width: 25cqw; /* 25% of container width */ text-align: center; -webkit-appearance: textfield; -moz-appearance: textfield; appearance: textfield; + font-family: inherit; + font-size: inherit; + box-sizing: content-box; + height: 30px; +} + +.dash-range-slider-input:only-of-type { + max-width: 33cqw; } .dash-range-slider-input::selection, @@ -224,7 +232,7 @@ } .dash-range-slider-input:focus { - outline: none; + outline: 1px solid var(--Dash-Fill-Interactive-Strong); } /* Hide the number input spinners */ diff --git a/components/dash-core-components/src/components/css/textarea.css b/components/dash-core-components/src/components/css/textarea.css index d90e695075..3893d4a3fd 100644 --- a/components/dash-core-components/src/components/css/textarea.css +++ b/components/dash-core-components/src/components/css/textarea.css @@ -9,6 +9,16 @@ width: 100%; accent-color: var(--Dash-Fill-Interactive-Strong); outline-color: var(--Dash-Fill-Interactive-Strong); + font-family: inherit; + font-size: inherit; +} + +.dash-textarea:focus-within { + border: 1px solid var(--Dash-Fill-Interactive-Strong); +} + +.dash-textarea:focus-visible { + outline: 1px solid var(--Dash-Fill-Interactive-Strong); } .dash-textarea:disabled { diff --git a/components/dash-core-components/src/fragments/Dropdown.tsx b/components/dash-core-components/src/fragments/Dropdown.tsx index b7186430be..fcf3b41ef4 100644 --- a/components/dash-core-components/src/fragments/Dropdown.tsx +++ b/components/dash-core-components/src/fragments/Dropdown.tsx @@ -283,7 +283,7 @@ const Dropdown = (props: DropdownProps) => { // Don't interfere with the event if the user is using Home/End keys on the search input if ( ['Home', 'End'].includes(e.key) && - document.activeElement instanceof HTMLInputElement + document.activeElement === searchInputRef.current ) { return; } @@ -367,6 +367,7 @@ const Dropdown = (props: DropdownProps) => { const accessibleId = id ?? uuid(); const positioningContainerRef = useRef(null); + const canClearValues = clearable && !disabled && !!sanitizedValues.length; const popover = ( @@ -377,10 +378,20 @@ const Dropdown = (props: DropdownProps) => { disabled={disabled} type="button" onKeyDown={e => { - if (e.key === 'ArrowDown') { + if (['ArrowDown', 'Enter'].includes(e.key)) { e.preventDefault(); + } + }} + onKeyUp={e => { + if (['ArrowDown', 'Enter'].includes(e.key)) { setIsOpen(true); } + if ( + ['Delete', 'Backspace'].includes(e.key) && + canClearValues + ) { + handleClear(); + } }} className={`dash-dropdown ${className ?? ''}`} style={style} @@ -417,7 +428,7 @@ const Dropdown = (props: DropdownProps) => { )} )} - {clearable && !disabled && !!sanitizedValues.length && ( + {canClearValues && ( { diff --git a/components/dash-core-components/src/fragments/RangeSlider.tsx b/components/dash-core-components/src/fragments/RangeSlider.tsx index 2d05887341..3b0323ba11 100644 --- a/components/dash-core-components/src/fragments/RangeSlider.tsx +++ b/components/dash-core-components/src/fragments/RangeSlider.tsx @@ -157,9 +157,8 @@ export default function RangeSlider(props: RangeSliderProps) { ); const totalChars = maxIntegerChars + maxDecimalChars; - const charWidth = 12; - return `${totalChars * charWidth}px`; + return `calc(${totalChars}ch + calc(var(--Dash-Spacing) * 2))`; }, [minMaxValues.min_mark, minMaxValues.max_mark, stepValue]); const valueIsValid = (val: number): boolean => { diff --git a/components/dash-core-components/tests/integration/dropdown/test_a11y.py b/components/dash-core-components/tests/integration/dropdown/test_a11y.py index a90900a66c..d547390ca4 100644 --- a/components/dash-core-components/tests/integration/dropdown/test_a11y.py +++ b/components/dash-core-components/tests/integration/dropdown/test_a11y.py @@ -83,7 +83,7 @@ def send_keys(key): dash_duo.start_server(app) dropdown = dash_duo.find_element("#dropdown") - dropdown.click() + dropdown.send_keys(Keys.ENTER) # Open with Enter key dash_duo.wait_for_element(".dash-dropdown-options") send_keys( @@ -237,14 +237,17 @@ def update_output(value): # Select 3 items by alternating ArrowDown and Spacebar send_keys(Keys.ARROW_DOWN) # Move to first option + sleep(0.05) send_keys(Keys.SPACE) # Select Option 0 dash_duo.wait_for_text_to_equal("#output", "Selected: ['Option 0']") send_keys(Keys.ARROW_DOWN) # Move to second option + sleep(0.05) send_keys(Keys.SPACE) # Select Option 1 dash_duo.wait_for_text_to_equal("#output", "Selected: ['Option 0', 'Option 1']") send_keys(Keys.ARROW_DOWN) # Move to third option + sleep(0.05) send_keys(Keys.SPACE) # Select Option 2 dash_duo.wait_for_text_to_equal( "#output", "Selected: ['Option 0', 'Option 1', 'Option 2']" @@ -253,6 +256,164 @@ def update_output(value): assert dash_duo.get_logs() == [] +def test_a11y007_opens_and_closes_without_races(dash_duo): + def send_keys(key): + actions = ActionChains(dash_duo.driver) + actions.send_keys(key) + actions.perform() + + app = Dash(__name__) + app.layout = Div( + [ + Dropdown( + id="dropdown", + options=[f"Option {i}" for i in range(0, 10)], + value="Option 5", + multi=False, + ), + Div(id="output"), + ] + ) + + def assert_focus_in_dropdown(): + # Verify focus is inside the dropdown + assert dash_duo.driver.execute_script( + """ + const activeElement = document.activeElement; + const dropdownContent = document.querySelector('.dash-dropdown-content'); + return dropdownContent && dropdownContent.contains(activeElement); + """ + ), "Focus must be inside the dropdown when it opens" + + @app.callback( + Output("output", "children"), + Input("dropdown", "value"), + ) + def update_output(value): + return f"Selected: {value}" + + dash_duo.start_server(app) + + # Verify initial value is set + dash_duo.wait_for_text_to_equal("#output", "Selected: Option 5") + + dropdown = dash_duo.find_element("#dropdown") + + # Test repeated open/close to confirm no race conditions or side effects + for i in range(3): + # Open with Enter + dropdown.send_keys(Keys.ENTER) + dash_duo.wait_for_element(".dash-dropdown-options") + assert_focus_in_dropdown() + + # Verify the value is still "Option 5" (not cleared) + dash_duo.wait_for_text_to_equal("#output", "Selected: Option 5") + + # Close with Escape + send_keys(Keys.ESCAPE) + sleep(0.1) + + # Verify the value is still "Option 5" + dash_duo.wait_for_text_to_equal("#output", "Selected: Option 5") + + for i in range(3): + # Open with mouse + dropdown.click() + dash_duo.wait_for_element(".dash-dropdown-options") + assert_focus_in_dropdown() + + # Verify the value is still "Option 5" (not cleared) + dash_duo.wait_for_text_to_equal("#output", "Selected: Option 5") + + # Close with Escape + dropdown.click() + sleep(0.1) + + # Verify the value is still "Option 5" + dash_duo.wait_for_text_to_equal("#output", "Selected: Option 5") + + assert dash_duo.get_logs() == [] + + +def test_a11y008_home_end_pageup_pagedown_navigation(dash_duo): + def send_keys(key): + actions = ActionChains(dash_duo.driver) + actions.send_keys(key) + actions.perform() + + def get_focused_option_text(): + return dash_duo.driver.execute_script( + """ + const focused = document.activeElement; + if (focused && focused.closest('.dash-options-list-option')) { + return focused.closest('.dash-options-list-option').textContent.trim(); + } + return null; + """ + ) + + app = Dash(__name__) + app.layout = Div( + [ + Dropdown( + id="dropdown", + options=[f"Option {i}" for i in range(0, 50)], + multi=True, + ), + ] + ) + + dash_duo.start_server(app) + + dropdown = dash_duo.find_element("#dropdown") + dropdown.send_keys(Keys.ENTER) # Open with Enter key + dash_duo.wait_for_element(".dash-dropdown-options") + + # Navigate from search input to options + send_keys(Keys.ARROW_DOWN) # Move from search to first option + sleep(0.05) + send_keys(Keys.ARROW_DOWN) # Move to second option + sleep(0.05) + send_keys(Keys.ARROW_DOWN) # Move to third option + sleep(0.05) + send_keys(Keys.ARROW_DOWN) # Move to fourth option + sleep(0.05) + assert get_focused_option_text() == "Option 3" + + send_keys(Keys.HOME) # Should go back to search input (index 0) + # Verify we're back at search input + assert dash_duo.driver.execute_script( + "return document.activeElement.type === 'search';" + ) + + # Now arrow down to first option + send_keys(Keys.ARROW_DOWN) + assert get_focused_option_text() == "Option 0" + + # Test End key - should go to last option + send_keys(Keys.END) + assert get_focused_option_text() == "Option 49" + + # Test PageUp - should jump up by 10 + send_keys(Keys.PAGE_UP) + assert get_focused_option_text() == "Option 39" + + # Test PageDown - should jump down by 10 + send_keys(Keys.PAGE_DOWN) + assert get_focused_option_text() == "Option 49" + + # Test PageUp from middle + send_keys(Keys.HOME) # Back to search input (index 0) + send_keys(Keys.PAGE_DOWN) # Jump to index 10 (Option 9) + send_keys(Keys.PAGE_DOWN) # Jump to index 20 (Option 19) + assert get_focused_option_text() == "Option 19" + + send_keys(Keys.PAGE_UP) # Jump to index 10 (Option 9) + assert get_focused_option_text() == "Option 9" + + assert dash_duo.get_logs() == [] + + def elements_are_visible(dash_duo, elements): # Check if the given elements are within the visible viewport of the dropdown elements = elements if isinstance(elements, list) else [elements] diff --git a/components/dash-core-components/tests/integration/dropdown/test_clearable_false.py b/components/dash-core-components/tests/integration/dropdown/test_clearable_false.py index e918c2c597..7d2e3bbcc1 100644 --- a/components/dash-core-components/tests/integration/dropdown/test_clearable_false.py +++ b/components/dash-core-components/tests/integration/dropdown/test_clearable_false.py @@ -41,16 +41,113 @@ def update_value(val): output_text = dash_duo.find_element("#dropdown-value").text dash_duo.find_element("#my-unclearable-dropdown ").click() + dash_duo.wait_for_element(".dash-dropdown-options") # Clicking the selected item should not de-select it. + # Click on the option container instead of the input directly selected_item = dash_duo.find_element( - f'.dash-dropdown-options input[value="{output_text}"]' + f'.dash-dropdown-option:has(input[value="{output_text}"])' ) selected_item.click() assert dash_duo.find_element("#dropdown-value").text == output_text assert dash_duo.get_logs() == [] +def test_ddcf001b_delete_backspace_keys_clearable_false(dash_duo): + from selenium.webdriver.common.keys import Keys + + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Dropdown( + id="my-unclearable-dropdown", + options=[ + {"label": "New York City", "value": "NYC"}, + {"label": "Montreal", "value": "MTL"}, + {"label": "San Francisco", "value": "SF"}, + ], + value="MTL", + clearable=False, + ), + html.Div(id="dropdown-value"), + ] + ) + + @app.callback( + Output("dropdown-value", "children"), + Input("my-unclearable-dropdown", "value"), + ) + def update_value(val): + return str(val) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#dropdown-value", "MTL") + + dropdown = dash_duo.find_element("#my-unclearable-dropdown") + + # Try to clear with Delete key - should not work since clearable=False + dropdown.send_keys(Keys.DELETE) + dash_duo.wait_for_text_to_equal("#dropdown-value", "MTL") + + # Try to clear with Backspace key - should not work since clearable=False + dropdown.send_keys(Keys.BACKSPACE) + dash_duo.wait_for_text_to_equal("#dropdown-value", "MTL") + + assert dash_duo.get_logs() == [] + + +def test_ddcf001c_delete_backspace_keys_clearable_true(dash_duo): + from selenium.webdriver.common.keys import Keys + + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Dropdown( + id="my-clearable-dropdown", + options=[ + {"label": "New York City", "value": "NYC"}, + {"label": "Montreal", "value": "MTL"}, + {"label": "San Francisco", "value": "SF"}, + ], + value="MTL", + clearable=True, + ), + html.Div(id="dropdown-value"), + ] + ) + + @app.callback( + Output("dropdown-value", "children"), + Input("my-clearable-dropdown", "value"), + ) + def update_value(val): + return str(val) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#dropdown-value", "MTL") + + dropdown = dash_duo.find_element("#my-clearable-dropdown") + + # Clear with Delete key - should work since clearable=True + dropdown.send_keys(Keys.DELETE) + dash_duo.wait_for_text_to_equal("#dropdown-value", "None") + + # Set a value again + dropdown.click() + dash_duo.wait_for_element(".dash-dropdown-options") + option = dash_duo.find_element('.dash-dropdown-option:has(input[value="SF"])') + option.click() + dash_duo.wait_for_text_to_equal("#dropdown-value", "SF") + + # Clear with Backspace key - should work since clearable=True + dropdown.send_keys(Keys.BACKSPACE) + dash_duo.wait_for_text_to_equal("#dropdown-value", "None") + + assert dash_duo.get_logs() == [] + + def test_ddcf002_clearable_false_multi(dash_duo): app = Dash(__name__) app.layout = html.Div( 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 a71c70ac50..846c25dfd9 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 +from dash import Dash, Input, Output, html, dcc from selenium.webdriver.common.keys import Keys @@ -121,6 +122,32 @@ def test_inni004_steppers(dash_dcc, debounce_number_app): assert dash_dcc.get_logs() == [] +def test_inni005_stepper_decrement_bug(dash_dcc, input_range_app): + """Test that decrement button works correctly with min/max constraints on initial render.""" + + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Input(id="number", value=17, type="number", min=10, max=23), + html.Div(id="output"), + ] + ) + + @app.callback(Output("output", "children"), [Input("number", "value")]) + def update_output(val): + return val + + dash_dcc.start_server(app) + + decrement_btn = dash_dcc.find_element(".dash-stepper-decrement") + + # Initial value is 17, should be able to decrement to 16 + decrement_btn.click() + dash_dcc.wait_for_text_to_equal("#output", "16") + + assert dash_dcc.get_logs() == [] + + def test_inni010_valid_numbers(dash_dcc, ninput_app): dash_dcc.start_server(ninput_app) for num, op in ( 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 5a3ffc1dad..65f56a8b03 100644 --- a/components/dash-core-components/tests/integration/sliders/test_sliders.py +++ b/components/dash-core-components/tests/integration/sliders/test_sliders.py @@ -61,9 +61,9 @@ def update_output(rng): slider = dash_dcc.find_element("#rangeslider") dash_dcc.click_at_coord_fractions(slider, 0.2, 0.25) - dash_dcc.wait_for_text_to_equal("#out", "You have selected 3-15") + dash_dcc.wait_for_text_to_equal("#out", "You have selected 2-15") dash_dcc.click_at_coord_fractions(slider, 0.51, 0.25) - dash_dcc.wait_for_text_to_equal("#out", "You have selected 3-10") + dash_dcc.wait_for_text_to_equal("#out", "You have selected 2-10") assert dash_dcc.get_logs() == []