diff --git a/.gitignore b/.gitignore index b3307c9311..7b0952e215 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,7 @@ sandbox/ # Used by mkdocs-material social plugin .cache + +# Wing Pro configuration files +*.wp[ru] + diff --git a/docs/widgets/input.md b/docs/widgets/input.md index 1a7b8aa65f..dc2c2403b8 100644 --- a/docs/widgets/input.md +++ b/docs/widgets/input.md @@ -114,7 +114,7 @@ as seen for `Palindrome` in the example above. #### Validate Empty -If you set `valid_empty=True` then empty values will bypass any validators, and empty values will be considered valid. +Empty values _always_ bypass validation, and are considered valid only when `valid_empty=True`. ## Reactive Attributes diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 715ab0f5f0..9a77922ffa 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -149,7 +149,7 @@ class Input(ScrollView): | ctrl+k | Delete everything to the right of the cursor. | | ctrl+x | Cut selected text. | | ctrl+c | Copy selected text. | - | ctrl+v | Paste text from the clipboard. | + | ctrl+v | Paste text from the clipboard. | """ COMPONENT_CLASSES: ClassVar[set[str]] = { @@ -187,9 +187,9 @@ class Input(ScrollView): } &:focus { - border: tall $border; + border: tall $border; background-tint: $foreground 5%; - + } &>.input--cursor { background: $input-cursor-background; @@ -207,12 +207,12 @@ class Input(ScrollView): } &.-invalid:focus { border: tall $error; - } + } &:ansi { background: ansi_default; color: ansi_default; - &>.input--cursor { + &>.input--cursor { text-style: reverse; } &>.input--placeholder, &>.input--suggestion { @@ -224,8 +224,8 @@ class Input(ScrollView): } &.-invalid:focus { border: tall ansi_red; - } - + } + } } @@ -553,13 +553,13 @@ def _watch_valid_empty(self) -> None: def validate(self, value: str) -> ValidationResult | None: """Run all the validators associated with this Input on the supplied value. - Runs all validators, combines the result into one. If any of the validators - failed, the combined result will be a failure. If no validators are present, - None will be returned. This also sets the `-invalid` CSS class on the Input - if the validation fails, and sets the `-valid` CSS class on the Input if - the validation succeeds. + Runs all validators, combines the result into one. If any of the + validators failed, the combined result will be a failure. If no + validators are present, or the value is empty, None will be returned. + After validation the result is reflected in the DOM, and the + Input will either have the `-valid` or `-invalid` CSS class. - Returns: + Returns: A ValidationResult indicating whether *all* validators succeeded or not. That is, if *any* validator fails, the result will be an unsuccessful validation. @@ -571,13 +571,14 @@ def set_classes() -> None: self.set_class(not valid, "-invalid") self.set_class(valid, "-valid") - # If no validators are supplied, and therefore no validation occurs, we return None. - if not self.validators: - self._valid = True + # Empty values never run the validators. + if not value: + self._valid = self.valid_empty set_classes() return None - if self.valid_empty and not value: + # If no validators are supplied, and therefore no validation occurs, we return None. + if not self.validators: self._valid = True set_classes() return None diff --git a/tests/input/test_input_validation.py b/tests/input/test_input_validation.py index 3ea17191b5..7c9a8a246d 100644 --- a/tests/input/test_input_validation.py +++ b/tests/input/test_input_validation.py @@ -222,3 +222,24 @@ async def test_valid_empty(): assert input.has_class("-valid") assert not input.has_class("-invalid") + + +async def test_invalid_empty(): + + class InvalidEmptyApp(App): + + def compose(self) -> ComposeResult: + yield Input(valid_empty=False, id="test-in", value="x") + + app = InvalidEmptyApp() + async with app.run_test() as pilot: + input = app.query_one(Input) + + assert input.has_class('-valid') + assert not input.has_class('-invalid') + + await pilot.press('backspace') + + assert input.has_class('-invalid') + assert not input.has_class('-valid') + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index fea3b0c5a4..b2fde34b33 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -3532,7 +3532,7 @@ class FocusWithinTransparentApp(App[None]): """ def compose(self) -> ComposeResult: - yield Input(placeholder="This is here to escape to") + yield Input(placeholder="This is here to escape to", valid_empty=True) with Panel(): yield OptionList(*["This is an option" for _ in range(30)]) yield Input(placeholder="Escape out via here for the bug")