diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index b0fb210989..2d59305ee3 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -21,17 +21,6 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - - # Python 3.9 is on macos-13 but not macos-latest (macos-14-arm64) - # https://github.com/actions/setup-python/issues/696#issuecomment-1637587760 - exclude: - - { python-version: "3.8", os: "macos-latest" } - - { python-version: "3.9", os: "macos-latest" } - - { python-version: "3.11", os: "macos-latest" } - include: - - { python-version: "3.8", os: "macos-13" } - - { python-version: "3.9", os: "macos-13" } - - { python-version: "3.11", os: "macos-13" } defaults: run: shell: bash 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 7b876df31c..83eda636ce 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 @@ -33,17 +33,17 @@ class RadioSet(Container, can_focus=True, can_focus_children=False): height: auto; width: auto; + & > RadioButton { + background: transparent; + border: none; + padding: 0 1; + } + & > RadioButton.-selected { color: $text; background: $highlight-cursor-blurred; } - & > * { - background: transparent; - border: none; - padding: 0; - } - & .toggle--button { color: $surface; background: $foreground 15%; @@ -84,8 +84,6 @@ class RadioSet(Container, can_focus=True, can_focus_children=False): } } } - - """ BINDINGS: ClassVar[list[BindingType]] = [ @@ -215,6 +213,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. @@ -303,3 +302,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 08c8cb9330..7733769b74 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: