diff --git a/CHANGELOG.md b/CHANGELOG.md index 75bf2b7c7..0d82e48c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Fixed + +- Fixed `!important` not applying to `align` https://github.com/Textualize/textual/issues/2420 +- Fixed `!important` not applying to `border` https://github.com/Textualize/textual/issues/2420 +- Fixed `!important` not applying to `content-align` https://github.com/Textualize/textual/issues/2420 +- Fixed `!important` not applying to `outline` https://github.com/Textualize/textual/issues/2420 +- Fixed `!important` not applying to `overflow` https://github.com/Textualize/textual/issues/2420 +- Fixed `!important` not applying to `scrollbar-size` https://github.com/Textualize/textual/issues/2420 +- Fixed `outline-right` not being recognised https://github.com/Textualize/textual/issues/2446 + ### Changed - Setting attributes with a `compute_` method will now raise an `AttributeError` https://github.com/Textualize/textual/issues/2383 diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 4e6ddcc1d..f38506311 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -252,6 +252,23 @@ class StylesBuilder: else: scalar_error() + def _distribute_importance(self, prefix: str, suffixes: tuple[str, ...]) -> None: + """Distribute importance amongst all aspects of the given style. + + Args: + prefix: The prefix of the style. + siffixes: The suffixes to distribute amongst. + + A number of styles can be set with the 'prefix' of the style, + providing the values as a series of parameters; or they can be set + with specific suffixes. Think `border` vs `border-left`, etc. This + method is used to ensure that if the former is set, `!important` is + distributed amongst all the suffixes. + """ + if prefix in self.styles.important: + self.styles.important.remove(prefix) + self.styles.important.update(f"{prefix}_{suffix}" for suffix in suffixes) + def process_box_sizing(self, name: str, tokens: list[Token]) -> None: for token in tokens: name, value, _, _, location, _ = token @@ -304,6 +321,7 @@ class StylesBuilder: ) rules["overflow_x"] = cast(Overflow, overflow_x) rules["overflow_y"] = cast(Overflow, overflow_y) + self._distribute_importance("overflow", ("x", "y")) def process_overflow_x(self, name: str, tokens: list[Token]) -> None: self.styles._rules["overflow_x"] = cast( @@ -486,6 +504,7 @@ class StylesBuilder: rules = self.styles._rules rules["border_top"] = rules["border_right"] = border rules["border_bottom"] = rules["border_left"] = border + self._distribute_importance("border", ("top", "left", "bottom", "right")) def process_border_top(self, name: str, tokens: list[Token]) -> None: self._process_border_edge("top", name, tokens) @@ -508,11 +527,12 @@ class StylesBuilder: rules = self.styles._rules rules["outline_top"] = rules["outline_right"] = border rules["outline_bottom"] = rules["outline_left"] = border + self._distribute_importance("outline", ("top", "left", "bottom", "right")) def process_outline_top(self, name: str, tokens: list[Token]) -> None: self._process_outline("top", name, tokens) - def process_parse_border_right(self, name: str, tokens: list[Token]) -> None: + def process_outline_right(self, name: str, tokens: list[Token]) -> None: self._process_outline("right", name, tokens) def process_outline_bottom(self, name: str, tokens: list[Token]) -> None: @@ -792,6 +812,8 @@ class StylesBuilder: self.styles._rules[f"{name}_horizontal"] = token_horizontal.value # type: ignore self.styles._rules[f"{name}_vertical"] = token_vertical.value # type: ignore + self._distribute_importance(name, ("horizontal", "vertical")) + def process_align_horizontal(self, name: str, tokens: list[Token]) -> None: try: value = self._process_enum(name, tokens, VALID_ALIGN_HORIZONTAL) @@ -859,6 +881,7 @@ class StylesBuilder: scrollbar_size_error(name, token2) self.styles._rules["scrollbar_size_horizontal"] = horizontal self.styles._rules["scrollbar_size_vertical"] = vertical + self._distribute_importance("scrollbar_size", ("horizontal", "vertical")) def process_scrollbar_size_vertical(self, name: str, tokens: list[Token]) -> None: if not tokens: diff --git a/tests/test_style_importance.py b/tests/test_style_importance.py new file mode 100644 index 000000000..fc5547416 --- /dev/null +++ b/tests/test_style_importance.py @@ -0,0 +1,102 @@ +from textual.app import App, ComposeResult +from textual.color import Color +from textual.containers import Container +from textual.css.scalar import Scalar, ScalarOffset + + +class StyleApp(App[None]): + CSS = """ + Container { + border: round green !important; + outline: round green !important; + align: right bottom !important; + content-align: right bottom !important; + offset: 17 23 !important; + overflow: hidden hidden !important; + padding: 10 20 30 40 !important; + scrollbar-size: 23 42 !important; + } + + Container.more-specific { + border: solid red; + outline: solid red; + align: center middle; + content-align: center middle; + offset: 0 0; + overflow: scroll scroll; + padding: 1 2 3 4; + scrollbar-size: 1 2; + } + """ + + def compose(self) -> ComposeResult: + yield Container(classes="more-specific") + + +async def test_border_importance(): + """Border without sides should support !important""" + async with StyleApp().run_test() as pilot: + border = pilot.app.query_one(Container).styles.border + desired = ("round", Color.parse("green")) + assert border.top == desired + assert border.left == desired + assert border.bottom == desired + assert border.right == desired + + +async def test_outline_importance(): + """Outline without sides should support !important""" + async with StyleApp().run_test() as pilot: + outline = pilot.app.query_one(Container).styles.outline + desired = ("round", Color.parse("green")) + assert outline.top == desired + assert outline.left == desired + assert outline.bottom == desired + assert outline.right == desired + + +async def test_align_importance(): + """Align without direction should support !important""" + async with StyleApp().run_test() as pilot: + assert pilot.app.query_one(Container).styles.align == ("right", "bottom") + + +async def test_content_align_importance(): + """Content align without direction should support !important""" + async with StyleApp().run_test() as pilot: + assert pilot.app.query_one(Container).styles.content_align == ( + "right", + "bottom", + ) + + +async def test_offset_importance(): + """Offset without direction should support !important""" + async with StyleApp().run_test() as pilot: + assert pilot.app.query_one(Container).styles.offset == ScalarOffset.from_offset( + (17, 23) + ) + + +async def test_overflow_importance(): + """Overflow without direction should support !important""" + async with StyleApp().run_test() as pilot: + assert pilot.app.query_one(Container).styles.overflow_x == "hidden" + assert pilot.app.query_one(Container).styles.overflow_y == "hidden" + + +async def test_padding_importance(): + """Padding without sides should support !important""" + async with StyleApp().run_test() as pilot: + padding = pilot.app.query_one(Container).styles.padding + assert padding.top == 10 + assert padding.left == 40 + assert padding.bottom == 30 + assert padding.right == 20 + + +async def test_scrollbar_size_importance(): + """Scrollbar size without direction should support !important""" + async with StyleApp().run_test() as pilot: + assert pilot.app.query_one(Container).styles.scrollbar_size_horizontal == 23 + assert pilot.app.query_one(Container).styles.scrollbar_size_vertical == 42