diff --git a/CHANGELOG.md b/CHANGELOG.md index 2471d5ed43..33fa73d917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### Fixed + +- Fixed `RadioSet` not being scrollable https://github.com/Textualize/textual/issues/5100 + ## [0.83.0] - 2024-10-10 ### Added diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index d371acd661..68ac5dc343 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -9,14 +9,14 @@ from textual import _widget_navigation from textual.binding import Binding, BindingType -from textual.containers import Container +from textual.containers import VerticalScroll from textual.events import Click, Mount from textual.message import Message from textual.reactive import var from textual.widgets._radio_button import RadioButton -class RadioSet(Container, can_focus=True, can_focus_children=False): +class RadioSet(VerticalScroll, can_focus=True, can_focus_children=False): """Widget for grouping a collection of radio buttons into a set. When a collection of [`RadioButton`][textual.widgets.RadioButton]s are @@ -42,7 +42,7 @@ class RadioSet(Container, can_focus=True, can_focus_children=False): * ToggleButton. If those styles ever get updated, these should be too. */ - RadioSet > * { + RadioSet > RadioButton { background: transparent; border: none; padding: 0 1; @@ -188,6 +188,7 @@ def watch__selected(self) -> None: self.query(RadioButton).remove_class("-selected") if self._selected is not None: self._nodes[self._selected].add_class("-selected") + self._scroll_to_selected() def _on_radio_button_changed(self, event: RadioButton.Changed) -> None: """Respond to the value of a button in the set being changed. @@ -276,3 +277,9 @@ def action_toggle_button(self) -> None: button = self._nodes[self._selected] assert isinstance(button, RadioButton) button.toggle() + + def _scroll_to_selected(self) -> None: + """Ensure that the selected button is in view.""" + if self._selected is not None: + button = self._nodes[self._selected] + self.call_after_refresh(self.scroll_to_widget, button, animate=False) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_radio_set_is_scrollable.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_radio_set_is_scrollable.svg new file mode 100644 index 0000000000..7ae6c2fffc --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_radio_set_is_scrollable.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RadioSetApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is option #7 + This is option #8 +This is option #9 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index e35b3e9393..7f7d4708ac 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -8,27 +8,28 @@ from textual import events, on from textual.app import App, ComposeResult from textual.binding import Binding, Keymap -from textual.containers import Center, Grid, Middle, Vertical -from textual.binding import Binding -from textual.containers import Vertical, VerticalScroll +from textual.containers import Center, Grid, Middle, Vertical, VerticalScroll from textual.pilot import Pilot from textual.renderables.gradient import LinearGradient from textual.screen import ModalScreen, Screen from textual.widgets import ( Button, - Header, DataTable, - Input, - RichLog, - TextArea, Footer, + Header, + Input, + Label, Log, OptionList, Placeholder, + ProgressBar, + RadioSet, + RichLog, SelectionList, + Static, + Switch, + TextArea, ) -from textual.widgets import ProgressBar, Label, Switch -from textual.widgets import Static from textual.widgets.text_area import BUILTIN_LANGUAGES, Selection, TextAreaTheme # These paths should be relative to THIS directory. @@ -331,6 +332,23 @@ def test_radio_set_example(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "radio_set.py") +def test_radio_set_is_scrollable(snap_compare): + """Regression test for https://github.com/Textualize/textual/issues/5100""" + + class RadioSetApp(App): + CSS = """ + RadioSet { + height: 5; + } + """ + + def compose(self) -> ComposeResult: + yield RadioSet(*[(f"This is option #{n}") for n in range(10)]) + + app = RadioSetApp() + assert snap_compare(app, press=["up"]) + + def test_content_switcher_example_initial(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "content_switcher.py") @@ -652,7 +670,8 @@ def test_richlog_width(snap_compare): def test_richlog_min_width(snap_compare): """The available space of this RichLog is less than the minimum width, so written content should be rendered at `min_width`. This snapshot should show the renderable - clipping at the right edge, as there's not enough space to satisfy the minimum width.""" + clipping at the right edge, as there's not enough space to satisfy the minimum width. + """ class RichLogMinWidth20(App[None]): def compose(self) -> ComposeResult: