Repository: Textualize/textual

Issue:
UI doesn't react to visibility flag change
I have an app that has a dialogue box that is invisible unless someone clicks a button. When they do, I want the dialogue to appear instantly so they can confirm their action. The dialogue is in a "higher" layer than the rest of the app, and it starts invisible based on CSS. 

When they click a button, I change it to visible, but it doesn't appear until something else makes the window redraw. For eg, if i resize the window, it appears instantly. 

starting CSS: 
```
#confirm_panel {
    width: 30%;
    height: 16%;
    border: lime double;
    overflow: hidden;
    layer: above;
    offset: 50 20;
    visibility: hidden;
}
```
Change to visible. I have tried it via styles and via the new visible flag. Both have the same behavior: 
```
        self.query_one('#confirm_panel').styles.visibility = 'visible'
```

Using 0.6.0



Discussion:
OK, the solution is to call `Widget.refresh(layout=True)`. It seems that visibility should be reactive? I'm setting it on a `Container` here. 
At first glance this looks like it may be a bug lower down. I can't remember off the top of my head if setting styles via code like that are fully reactive, but setting them via class changes in a stylesheet should work. With that in mind:

```python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Label, Button
from textual.containers import Vertical

class ShowHideApp( App[ None ] ):

    CSS = """
    Horizontal {
        height: 1fr;
        border: solid red;
    }
    Button {
       width: 100%;
       height: 1fr;
    }
    Label {
        width: 100%;
        height: 9fr;
        content-align: center middle;
        background: #888800;
        visibility: visible;
    }

    .hidden {
        visibility: hidden;
    }
    """

    def compose(self) -> ComposeResult:
        yield Header()
        yield Vertical(
            Vertical(
                Button( "Toggle show/hide via code", id="via-code" ),
                Label( "I can be toggled via code; press the button", id="via-code-label" )
            ),
            Vertical(
                Button( "Toggle show/hide via classes", id="via-classes" ),
                Label( "I can be toggled via classes; press the button", id="via-classes-label" )
            )
        )
        yield Footer()

    def on_button_pressed( self, event: Button.Pressed ) -> None:
        if event.button.id == "via-code":
            label = self.query_one( "#via-code-label" )
            label.styles.visibility = "visible" if label.styles.visibility == "hidden" else "hidden"
            self.refresh(layout=True)
        elif event.button.id == "via-classes":
            self.query_one( "#via-classes-label" ).toggle_class( "hidden" )

if __name__ == "__main__":
    ShowHideApp().run()
```

Note that the `via-classes` version doesn't work. Unless, that is, I move the `refresh` to the end of `on_button_pressed` rather than just in the `via-code` version.

I'll label this as a bug and, if nobody gets to it sooner, I'll have a proper look in the morning.

Resolving patch:
diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py
--- a/src/textual/css/styles.py
+++ b/src/textual/css/styles.py
@@ -209,7 +209,7 @@ class StylesBase(ABC):
     node: DOMNode | None = None
 
     display = StringEnumProperty(VALID_DISPLAY, "block", layout=True)
-    visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
+    visibility = StringEnumProperty(VALID_VISIBILITY, "visible", layout=True)
     layout = LayoutProperty()
 
     auto_color = BooleanProperty(default=False)


Test patch:
diff --git a/tests/test_visibility_change.py b/tests/test_visibility_change.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visibility_change.py
@@ -0,0 +1,46 @@
+"""See https://github.com/Textualize/textual/issues/1355 as the motivation for these tests."""
+
+from textual.app import App, ComposeResult
+from textual.containers import Vertical
+from textual.widget import Widget
+
+
+class VisibleTester(App[None]):
+    """An app for testing visibility changes."""
+
+    CSS = """
+    Widget {
+        height: 1fr;
+    }
+    .hidden {
+        visibility: hidden;
+    }
+    """
+
+    def compose(self) -> ComposeResult:
+        yield Vertical(
+            Widget(id="keep"), Widget(id="hide-via-code"), Widget(id="hide-via-css")
+        )
+
+
+async def test_visibility_changes() -> None:
+    """Test changing visibility via code and CSS."""
+    async with VisibleTester().run_test() as pilot:
+        assert len(pilot.app.screen.visible_widgets) == 5
+        assert pilot.app.query_one("#keep").visible is True
+        assert pilot.app.query_one("#hide-via-code").visible is True
+        assert pilot.app.query_one("#hide-via-css").visible is True
+
+        pilot.app.query_one("#hide-via-code").styles.visibility = "hidden"
+        await pilot.pause(0)
+        assert len(pilot.app.screen.visible_widgets) == 4
+        assert pilot.app.query_one("#keep").visible is True
+        assert pilot.app.query_one("#hide-via-code").visible is False
+        assert pilot.app.query_one("#hide-via-css").visible is True
+
+        pilot.app.query_one("#hide-via-css").set_class(True, "hidden")
+        await pilot.pause(0)
+        assert len(pilot.app.screen.visible_widgets) == 3
+        assert pilot.app.query_one("#keep").visible is True
+        assert pilot.app.query_one("#hide-via-code").visible is False
+        assert pilot.app.query_one("#hide-via-css").visible is False