Repository: textualize/textual
Issue: Disable scrolling of parent when Select widget is focused

### Discussed in https://github.com/Textualize/textual/discussions/2953

<div type='discussions-op-text'>

<sup>Originally posted by **grimnexo** July 16, 2023</sup>
Hi All,

I have been using textual for the past few days and I really enjoy it and am excited to design applications with it for my day-to-day use. In learning the package I have run into a question (two included here technically) that I can't seem to find any information on. (Keep in mind I am definitely _not_ a master programmer as I have mostly worked with scripting to this point)

I've taken the package demo code and reduced it in order to reproduce the issue I am running into in an easily identifiable manner:

When the Select widget is clicked on by the user it scrolls the parent container to center Select widget on the screen -- and I can't find out how to disable that behavior.

The Select widget should have space on screen to display without scrolling, but no matter where the Select widget is, it scrolls to center it on screen rather that offset by the parent container's sizing

I also had a secondary related question for the Select widget such as how to make it _lose_ focus when the click is anywhere else on the screen - if you open it then use a quick access link you'll see that it stays opened at the bottom of the screen until a choice is made.

The issue:
![SelectWidgetScrollingIssue](https://github.com/Textualize/textual/assets/57557037/7d8184df-ea52-4d8a-a172-c09e0c455c2d)

The code:
<details>
<summary> The Python code [yourchoice].py </summary>

```PYTHON
from __future__ import annotations
from rich.console import RenderableType
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container, ScrollableContainer
from textual.widgets import Button, Footer, Header, Select, Static, TextLog, Placeholder, Markdown

__ISSUE_MD__ = """
## Question #1 -- How do I make selecting the option list _NOT_ scroll the parent container
This issue is that even when the Select widget has space on screen to display, it still scrolls which mis-aligns 
the parent container on the screen. 

I am trying to find a way to keep the section aligned while still having the drop-down expand if needed 
to a certain degree, maybe _X_ items long or a max-height value

## Question #2 - What's the best way for the Select widget to _lose_ focus when the user clicks elsewhere?
Example:
    If you open the dropdown then click a quick-access link on the left the dropdown menu doesn't naturally close

What's the ideal way to close the list when _anything_ else is clicked?
(I am assuming by overriding the on_click function and checking for focus somehow)

"""

WELCOME_MD = """
## Textual Layout Issue / Question
**Hello**! Textual is amazing and I'm having so much fun learning it!
There are _so_ _many_ potential projects I can use it for!
"""

WIDGETS_MD = """
Textual widgets are _amazingly_ powerful interactive components.
I've been working on building my own -> That's where I found this and couldn't figure it
out
"""


class Welcome(Container):
    def compose(self) -> ComposeResult:
        yield Markdown(WELCOME_MD)
        yield Button("Start", variant="success")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.app.add_note("[b magenta]Start!")
        self.app.query_one(".location-first").scroll_visible(duration=0.5, top=True)


class Title(Static):
    pass


class Body(ScrollableContainer):
    pass


class QuickAccess(Container):
    pass


class AboveFold(Container):
    pass


class Section(Container):
    pass


class SectionTitle(Static):
    pass


class Column(Container):
    pass


class TextContent(Static):
    pass


class LocationLink(Static):
    def __init__(self, label: str, reveal: str) -> None:
        super().__init__(label)
        self.reveal = reveal

    def on_click(self) -> None:
        self.app.query_one(self.reveal).scroll_visible(top=True, duration=0.5)
        self.app.add_note(f"Scrolling to [b]{self.reveal}[/b]")


class DemoApp(App[None]):
    CSS_PATH = "demo.css"
    TITLE = "Question - Is this a bug?"
    BINDINGS = [
        ("f1", "app.toggle_class('TextLog', '-hidden')", "Notes"),
        Binding("ctrl+c,ctrl+q", "app.quit", "Quit", show=True),
    ]

    # Overloaded the __init__ section so that we could enable watching the CSS file
    def __init__(self):
        super().__init__(watch_css=True)

    # I lef the note section in for debugging, but wasn't sure what exactly to log while modifying the CSS
    def add_note(self, renderable: RenderableType) -> None:
        self.query_one(TextLog).write(renderable)

    def compose(self) -> ComposeResult:
        yield Container(
            Header(show_clock=False),
            TextLog(classes="-hidden", wrap=False, highlight=True, markup=True),
            Body(
                QuickAccess(
                    LocationLink("Home", ".location-top"),
                    LocationLink("Bugged Section", ".location-widgets"),
                ),
                AboveFold(Welcome(), classes="location-top"),
                Column(
                    Section(
                        SectionTitle("Example Section Title"),
                        Markdown(WIDGETS_MD),
                        Placeholder("-- Example space of n+ other widgets and their children --"),
                        Select([("Example Selectable Option", 1)],
                               prompt="Please choose an option.. (And watch it scroll)"),
                        Markdown(__ISSUE_MD__, id="issue-markdown"),
                        id="bugged-section"
                    ),
                    classes="location-widgets location-first",
                )
            )
        )
        yield Footer()

    def on_mount(self) -> None:
        self.add_note("?? Question ?? is running")
        self.query_one("Welcome Button", Button).focus()


app = DemoApp()
if __name__ == "__main__":
    app.run()

```

</details>
<details>
<summary>The reduced CSS code: demo.css</summary>

```CSS
* {transition: background 500ms in_out_cubic, color 500ms in_out_cubic;}
Screen {
    layers: base overlay notes notifications;
    overflow: hidden;
}
Body {
    height: 100%;
    overflow-y: scroll;
    width: 100%;
    background: $surface;

}
AboveFold {
    width: 100%;
    height: 100%;
    align: center middle;
}
Welcome {
    background: $boost;
    height: auto;
    max-width: 100;
    min-width: 40;
    border: wide $primary;
    padding: 1 2;
    margin: 1 2;
    box-sizing: border-box;
}
Welcome Button {
    width: 100%;
    margin-top: 1;
}
Column {
    height: auto;
    min-height: 100vh;
    align: center top;
    overflow: hidden;
}
Screen>Container {
    height: 100%;
    overflow: hidden;
}
TextLog {
    background: $surface;
    color: $text;
    height: 50vh;
    dock: bottom;
    layer: notes;
    border-top: hkey $primary;
    offset-y: 0;
    transition: offset 400ms in_out_cubic;
    padding: 0 1 1 1;
}
TextLog:focus {
    offset: 0 0 !important;
}
TextLog.-hidden {
    offset-y: 100%;
}
Section {
    height: auto;
    min-width: 40;
    margin: 1 2 4 2;

}
SectionTitle {
    padding: 1 2;
    background: $boost;
    text-align: center;
    text-style: bold;
}
QuickAccess {
    width: 30;
    dock: left;
}
LocationLink {
    margin: 1 0 0 1;
    height: 1;
    padding: 1 2;
    background: $boost;
    color: $text;
    box-sizing: content-box;
    content-align: center middle;
}
LocationLink:hover {
    background: $accent;
    color: $text;
    text-style: bold;
}
Message {
    margin: 0 1;
}
Tree {
    margin: 1 0;
}
Window>Static {
    width: auto;
}

#issue-markdown {
    margin: 4 4 4 4;
    dock: bottom;
}

#bugged-section {
    /* --
    Here we set the height to be below the center and this demonstrates the issue:
        Action: [ User focuses the Select object ]
        Result:
            The entire parent container scrolls when it has space on screen already and should not need to
    */
    height: auto;
    border: red;
}

Placeholder {
    /* -- This placeholder is meant to simulate other widgets of a collective X height -- */
    margin: 1 1 1 1;
    height: 10vh;
}

Select {
    /* Nothing done in this section - I've tried various fixes with no luck */
}

```
</details>
 
I have been looking for a while to see if I could find references to this or similar issues, but I didn't have any luck or was not putting two and two together.

If anyone is aware of other discussions related to this issue please point me towards them!

Thank you in advance for any insights you can provide!</div>

Changed files:
src/textual/screen.py

diff --git a/src/textual/screen.py b/src/textual/screen.py
index 20848fa66..450717c8f 100644
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -939,6 +939,25 @@ class Screen(Generic[ScreenResultType], Widget):
         """
         self.dismiss(result)
 
+    def can_view(self, widget: Widget) -> bool:
+        """Check if a given widget is in the current view (scrollable area).
+
+        Note: This doesn't necessarily equate to a widget being visible.
+        There are other reasons why a widget may not be visible.
+
+        Args:
+            widget: A widget that is a descendant of self.
+
+        Returns:
+            True if the entire widget is in view, False if it is partially visible or not in view.
+        """
+        # If the widget is one that overlays the screen...
+        if widget.styles.overlay == "screen":
+            # ...simply check if it's within the screen's region.
+            return widget.region in self.region
+        # Failing that fall back to normal checking.
+        return super().can_view(widget)
+
 
 @rich.repr.auto
 class ModalScreen(Screen[ScreenResultType]):