From b36afd80a7deb43ec2f6a8d8aafb8a6ba21c978d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 10:13:30 +0100 Subject: [PATCH 01/10] Highlight a RadioSet when focus is within it --- src/textual/widgets/_radio_set.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index e06f7a715a..8599c178c7 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -26,6 +26,10 @@ class RadioSet(Container): width: auto; } + RadioSet:focus-within { + border: round $accent; + } + App.-light-mode RadioSet { border: round #CCC; } From 59506f3292bca186ebb5e6c5a0a61853f6c8371d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 11:16:20 +0100 Subject: [PATCH 02/10] Turn a RadioSet into a single focus switching site With this commit a RadioSet becomes something you can tab into and out of with just one keypress; navigation of the buttons within moves to being done with the cursor keys instead. See #2368. --- src/textual/widgets/_radio_set.py | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index 8599c178c7..a05f1966d2 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -2,8 +2,11 @@ from __future__ import annotations +from typing import ClassVar + import rich.repr +from ..binding import Binding, BindingType from ..containers import Container from ..events import Mount from ..message import Message @@ -35,6 +38,21 @@ class RadioSet(Container): } """ + BINDINGS: ClassVar[list[BindingType]] = [ + Binding("down,right", "next_button", "", show=False), + Binding("shift+tab", "breakout_previous", "", show=False), + Binding("tab", "breakout_next", "", show=False), + Binding("up,left", "previous_button", "", show=False), + ] + """ + | Key(s) | Description | + | :- | :- | + | left, up | Select the previous radio button in the set. | + | right, down | Select the next radio button in the set. | + | shift+tab | Move focus to the previous focusable widget relative to the set. | + | tab | Move focus to the next focusable widget relative to the set. | + """ + @rich.repr.auto class Changed(Message, bubble=True): """Posted when the pressed button in the set changes. @@ -155,3 +173,37 @@ def pressed_index(self) -> int: if self._pressed_button is not None else -1 ) + + def action_previous_button(self) -> None: + """Navigate to the previous button in the set. + + Note that this will wrap around to the end if at the start. + """ + if self.children: + if self.screen.focused == self.children[0]: + self.screen.set_focus(self.children[-1]) + else: + self.screen.focus_previous() + + def action_next_button(self) -> None: + """Navigate to the next button in the set. + + Note that this will wrap around to the start if at the end. + """ + if self.children: + if self.screen.focused == self.children[-1]: + self.screen.set_focus(self.children[0]) + else: + self.screen.focus_next() + + def action_breakout_previous(self) -> None: + """Break out of the radio set to the previous widget in the focus chain.""" + if self.children: + self.screen.set_focus(self.children[0]) + self.screen.focus_previous() + + def action_breakout_next(self) -> None: + """Break out of the radio set to the next widget in the focus chain.""" + if self.children: + self.screen.set_focus(self.children[-1]) + self.screen.focus_next() From e16493bf812d9fd2bb9000a4314e88d0a373c8a3 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 11:27:23 +0100 Subject: [PATCH 03/10] Update the snapshot tests This is necessary now that a focused RadioSet has acquired a border colour similar to that if a focused Input. --- .../__snapshots__/test_snapshots.ambr | 243 +++++++++--------- 1 file changed, 122 insertions(+), 121 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 797ec93b54..9a1b9d9a50 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -19858,137 +19858,137 @@ font-weight: 700; } - .terminal-1209678307-matrix { + .terminal-2550674499-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1209678307-title { + .terminal-2550674499-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1209678307-r1 { fill: #e1e1e1 } - .terminal-1209678307-r2 { fill: #c5c8c6 } - .terminal-1209678307-r3 { fill: #666666 } - .terminal-1209678307-r4 { fill: #3d3d3d } - .terminal-1209678307-r5 { fill: #1e1e1e;font-weight: bold } - .terminal-1209678307-r6 { fill: #515151 } - .terminal-1209678307-r7 { fill: #e1e1e1;text-decoration: underline; } - .terminal-1209678307-r8 { fill: #4ebf71;font-weight: bold } + .terminal-2550674499-r1 { fill: #e1e1e1 } + .terminal-2550674499-r2 { fill: #c5c8c6 } + .terminal-2550674499-r3 { fill: #0178d4 } + .terminal-2550674499-r4 { fill: #3d3d3d } + .terminal-2550674499-r5 { fill: #1e1e1e;font-weight: bold } + .terminal-2550674499-r6 { fill: #515151 } + .terminal-2550674499-r7 { fill: #e1e1e1;text-decoration: underline; } + .terminal-2550674499-r8 { fill: #4ebf71;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - + - - - - - - - - ─────────────────────────────────────── - Battlestar Galactica - Dune 1984 - Dune 2021 - Serenity - Star Trek: The Motion Picture - Star Wars: A New Hope - The Last Starfighter - Total Recall 👉 🔴 - Wing Commander - ─────────────────────────────────────── - - - - - - + + + + + + + + ─────────────────────────────────────── + Battlestar Galactica + Dune 1984 + Dune 2021 + Serenity + Star Trek: The Motion Picture + Star Wars: A New Hope + The Last Starfighter + Total Recall 👉 🔴 + Wing Commander + ─────────────────────────────────────── + + + + + + @@ -20019,138 +20019,139 @@ font-weight: 700; } - .terminal-4238820762-matrix { + .terminal-3895672826-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4238820762-title { + .terminal-3895672826-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4238820762-r1 { fill: #e1e1e1 } - .terminal-4238820762-r2 { fill: #c5c8c6 } - .terminal-4238820762-r3 { fill: #666666 } - .terminal-4238820762-r4 { fill: #3d3d3d } - .terminal-4238820762-r5 { fill: #1e1e1e;font-weight: bold } - .terminal-4238820762-r6 { fill: #4ebf71;font-weight: bold } - .terminal-4238820762-r7 { fill: #cc555a;font-weight: bold;font-style: italic; } - .terminal-4238820762-r8 { fill: #515151 } - .terminal-4238820762-r9 { fill: #e1e1e1;text-decoration: underline; } + .terminal-3895672826-r1 { fill: #e1e1e1 } + .terminal-3895672826-r2 { fill: #c5c8c6 } + .terminal-3895672826-r3 { fill: #0178d4 } + .terminal-3895672826-r4 { fill: #666666 } + .terminal-3895672826-r5 { fill: #3d3d3d } + .terminal-3895672826-r6 { fill: #1e1e1e;font-weight: bold } + .terminal-3895672826-r7 { fill: #4ebf71;font-weight: bold } + .terminal-3895672826-r8 { fill: #cc555a;font-weight: bold;font-style: italic; } + .terminal-3895672826-r9 { fill: #515151 } + .terminal-3895672826-r10 { fill: #e1e1e1;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - + - - - - - - - - ────────────────────────────────────────────────────────────────────── - Battlestar GalacticaAmanda - Dune 1984Connor MacLeod - Dune 2021Duncan MacLeod - SerenityHeather MacLeod - Star Trek: The Motion PictureJoe Dawson - Star Wars: A New HopeKurgan, The - The Last StarfighterMethos - Total Recall 👉 🔴Rachel Ellenstein - Wing CommanderRamírez - ────────────────────────────────────────────────────────────────────── - - - - - - + + + + + + + + ────────────────────────────────────────────────────────────────────── + Battlestar GalacticaAmanda + Dune 1984Connor MacLeod + Dune 2021Duncan MacLeod + SerenityHeather MacLeod + Star Trek: The Motion PictureJoe Dawson + Star Wars: A New HopeKurgan, The + The Last StarfighterMethos + Total Recall 👉 🔴Rachel Ellenstein + Wing CommanderRamírez + ────────────────────────────────────────────────────────────────────── + + + + + + From a6a373161f07d5b37b0dfddc9c26dd514aa37546 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 12:06:46 +0100 Subject: [PATCH 04/10] Add some unit testing of the new navigation --- tests/toggles/test_radioset.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index 198fb7b303..b44722787d 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -52,6 +52,34 @@ async def test_radio_sets_toggle(): ] +async def test_radioset_inner_navigation(): + """Using the cursor keys should navigate between buttons in a set.""" + async with RadioSetApp().run_test() as pilot: + assert pilot.app.screen.focused is None + await pilot.press("tab") + assert ( + pilot.app.screen.focused == pilot.app.query_one("#from_buttons").children[0] + ) + for key, landing in (("down", 1), ("up", 0), ("right", 1), ("left", 0)): + await pilot.press(key) + assert ( + pilot.app.screen.focused + == pilot.app.query_one("#from_buttons").children[landing] + ) + + +async def test_radioset_breakout_navigation(): + """Shift/Tabbing while in a radioset should move to the previous/next focsuable after the set itself.""" + async with RadioSetApp().run_test() as pilot: + assert pilot.app.screen.focused is None + await pilot.press("tab") + assert pilot.app.screen.focused.parent is pilot.app.query_one("#from_buttons") + await pilot.press("tab") + assert pilot.app.screen.focused.parent is pilot.app.query_one("#from_strings") + await pilot.press("shift+tab") + assert pilot.app.screen.focused.parent is pilot.app.query_one("#from_buttons") + + class BadRadioSetApp(App[None]): def compose(self) -> ComposeResult: with RadioSet(): From c45f9358ee43559bc015ca61501667343ea77e5c Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 15:00:44 +0100 Subject: [PATCH 05/10] Change RadioSet so it's less a container of widgets and more a widget Initially we went with a RadioSet being a simple container of RadioButtons, with the user navigating the RadioButtons like you would any other set of widgets. This was fine but it became pretty clear pretty quickly that having to tab through a non-trivial collection of buttons in a set to get to the next widget wasn't ideal. This commit, satisfying #2368, takes over the navigation of the buttons within the container, makes the container itself a focusable widget, and sets up some new bindings to allow a more natural and efficient interaction with the set. --- docs/examples/widgets/radio_button.py | 2 +- docs/examples/widgets/radio_set.py | 7 +- docs/examples/widgets/radio_set_changed.py | 7 +- src/textual/widgets/_radio_set.py | 108 ++++++-- .../__snapshots__/test_snapshots.ambr | 248 +++++++++--------- tests/toggles/test_radioset.py | 13 +- 6 files changed, 215 insertions(+), 170 deletions(-) diff --git a/docs/examples/widgets/radio_button.py b/docs/examples/widgets/radio_button.py index 64b7e7e41a..316d89100d 100644 --- a/docs/examples/widgets/radio_button.py +++ b/docs/examples/widgets/radio_button.py @@ -20,7 +20,7 @@ def compose(self) -> ComposeResult: yield RadioButton("Wing Commander") def on_mount(self) -> None: - self.query_one("#focus_me", RadioButton).focus() + self.query_one(RadioSet).focus() if __name__ == "__main__": diff --git a/docs/examples/widgets/radio_set.py b/docs/examples/widgets/radio_set.py index 7f8ccf636d..c09c9d6be1 100644 --- a/docs/examples/widgets/radio_set.py +++ b/docs/examples/widgets/radio_set.py @@ -9,7 +9,7 @@ class RadioChoicesApp(App[None]): def compose(self) -> ComposeResult: with Horizontal(): # A RadioSet built up from RadioButtons. - with RadioSet(): + with RadioSet(id="focus_me"): yield RadioButton("Battlestar Galactica") yield RadioButton("Dune 1984") yield RadioButton("Dune 2021") @@ -18,8 +18,7 @@ def compose(self) -> ComposeResult: yield RadioButton("Star Wars: A New Hope") yield RadioButton("The Last Starfighter") yield RadioButton( - "Total Recall :backhand_index_pointing_right: :red_circle:", - id="focus_me", + "Total Recall :backhand_index_pointing_right: :red_circle:" ) yield RadioButton("Wing Commander") # A RadioSet built up from a collection of strings. @@ -36,7 +35,7 @@ def compose(self) -> ComposeResult: ) def on_mount(self) -> None: - self.query_one("#focus_me", RadioButton).focus() + self.query_one("#focus_me").focus() if __name__ == "__main__": diff --git a/docs/examples/widgets/radio_set_changed.py b/docs/examples/widgets/radio_set_changed.py index c817b6e6f7..4af563c391 100644 --- a/docs/examples/widgets/radio_set_changed.py +++ b/docs/examples/widgets/radio_set_changed.py @@ -9,7 +9,7 @@ class RadioSetChangedApp(App[None]): def compose(self) -> ComposeResult: with VerticalScroll(): with Horizontal(): - with RadioSet(): + with RadioSet(id="focus_me"): yield RadioButton("Battlestar Galactica") yield RadioButton("Dune 1984") yield RadioButton("Dune 2021") @@ -18,8 +18,7 @@ def compose(self) -> ComposeResult: yield RadioButton("Star Wars: A New Hope") yield RadioButton("The Last Starfighter") yield RadioButton( - "Total Recall :backhand_index_pointing_right: :red_circle:", - id="focus_me", + "Total Recall :backhand_index_pointing_right: :red_circle:" ) yield RadioButton("Wing Commander") with Horizontal(): @@ -28,7 +27,7 @@ def compose(self) -> ComposeResult: yield Label(id="index") def on_mount(self) -> None: - self.query_one("#focus_me", RadioButton).focus() + self.query_one(RadioSet).focus() def on_radio_set_changed(self, event: RadioSet.Changed) -> None: self.query_one("#pressed", Label).update( diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index a05f1966d2..4003686b46 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -2,18 +2,19 @@ from __future__ import annotations -from typing import ClassVar +from typing import ClassVar, Optional import rich.repr from ..binding import Binding, BindingType from ..containers import Container -from ..events import Mount +from ..events import Click, Mount from ..message import Message +from ..reactive import var from ._radio_button import RadioButton -class RadioSet(Container): +class RadioSet(Container, 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 @@ -29,30 +30,47 @@ class RadioSet(Container): width: auto; } - RadioSet:focus-within { + RadioSet:focus { border: round $accent; } App.-light-mode RadioSet { border: round #CCC; } + + /* The following rules/styles mimic similar ToggleButton:focus rules in + * ToggleButton. If those styles ever get updated, these should be too. + */ + + RadioSet:focus > RadioButton.-selected > .toggle--label { + text-style: underline; + } + + RadioSet:focus ToggleButton.-selected > .toggle--button { + background: $foreground 25%; + } + + RadioSet:focus > RadioButton.-on.-selected > .toggle--button { + background: $foreground 25%; + } """ BINDINGS: ClassVar[list[BindingType]] = [ Binding("down,right", "next_button", "", show=False), - Binding("shift+tab", "breakout_previous", "", show=False), - Binding("tab", "breakout_next", "", show=False), + Binding("enter,space", "toggle", "Toggle", show=False), Binding("up,left", "previous_button", "", show=False), ] """ | Key(s) | Description | | :- | :- | + | enter, space | Toggle the currently-selected button. | | left, up | Select the previous radio button in the set. | | right, down | Select the next radio button in the set. | - | shift+tab | Move focus to the previous focusable widget relative to the set. | - | tab | Move focus to the next focusable widget relative to the set. | """ + _selected: var[int | None] = var[Optional[int]](None) + """The index of the currently-selected radio button.""" + @rich.repr.auto class Changed(Message, bubble=True): """Posted when the pressed button in the set changes. @@ -117,12 +135,26 @@ def __init__( def _on_mount(self, _: Mount) -> None: """Perform some processing once mounted in the DOM.""" + # If there are radio buttons, select the first one. + if self._nodes: + self._selected = 0 + + # Get all the buttons within us; we'll be doing a couple of things + # with that list. + buttons = list(self.query(RadioButton)) + + # RadioButtons can have focus, by default. But we're going to take + # that over and handle movement between them. So here we tell them + # all they can't focus. + for button in buttons: + button.can_focus = False + # It's possible for the user to pass in a collection of radio # buttons, with more than one set to on; they shouldn't, but we # can't stop them. So here we check for that and, for want of a # better approach, we keep the first one on and turn all the others # off. - switched_on = [button for button in self.query(RadioButton) if button.value] + switched_on = [button for button in buttons if button.value] with self.prevent(RadioButton.Changed): for button in switched_on[1:]: button.value = False @@ -131,6 +163,11 @@ def _on_mount(self, _: Mount) -> None: if switched_on: self._pressed_button = switched_on[0] + def watch__selected(self) -> None: + self.query(RadioButton).remove_class("-selected") + if self._selected is not None: + self._nodes[self._selected].add_class("-selected") + def _on_radio_button_changed(self, event: RadioButton.Changed) -> None: """Respond to the value of a button in the set being changed. @@ -160,6 +197,22 @@ def _on_radio_button_changed(self, event: RadioButton.Changed) -> None: # We're being clicked off, we don't want that. event.radio_button.value = True + def _on_radio_set_changed(self, event: RadioSet.Changed) -> None: + """Handle a change to which button in the set is pressed. + + This handler ensures that, when a button is pressed, it's also the + selected button. + """ + self._selected = event.index + + async def _on_click(self, _: Click) -> None: + """Handle a click on or within the radio set. + + This handler ensures that focus moves to the clicked radio set, even + if there's a click on one of the radio buttons it contains. + """ + self.focus() + @property def pressed_button(self) -> RadioButton | None: """The currently-pressed [`RadioButton`][textual.widgets.RadioButton], or `None` if none are pressed.""" @@ -179,31 +232,28 @@ def action_previous_button(self) -> None: Note that this will wrap around to the end if at the start. """ - if self.children: - if self.screen.focused == self.children[0]: - self.screen.set_focus(self.children[-1]) + if self._nodes: + if self._selected == 0: + self._selected = len(self.children) - 1 + elif self._selected is None: + self._selected = 0 else: - self.screen.focus_previous() + self._selected -= 1 def action_next_button(self) -> None: """Navigate to the next button in the set. Note that this will wrap around to the start if at the end. """ - if self.children: - if self.screen.focused == self.children[-1]: - self.screen.set_focus(self.children[0]) + if self._nodes: + if self._selected is None or self._selected == len(self._nodes) - 1: + self._selected = 0 else: - self.screen.focus_next() - - def action_breakout_previous(self) -> None: - """Break out of the radio set to the previous widget in the focus chain.""" - if self.children: - self.screen.set_focus(self.children[0]) - self.screen.focus_previous() - - def action_breakout_next(self) -> None: - """Break out of the radio set to the next widget in the focus chain.""" - if self.children: - self.screen.set_focus(self.children[-1]) - self.screen.focus_next() + self._selected += 1 + + def action_toggle(self) -> None: + """Toggle the state of the currently-selected button.""" + if self._selected is not None: + button = self._nodes[self._selected] + assert isinstance(button, RadioButton) + button.toggle() diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 9a1b9d9a50..22bbbdafd7 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -19858,137 +19858,137 @@ font-weight: 700; } - .terminal-2550674499-matrix { + .terminal-1099969603-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2550674499-title { + .terminal-1099969603-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2550674499-r1 { fill: #e1e1e1 } - .terminal-2550674499-r2 { fill: #c5c8c6 } - .terminal-2550674499-r3 { fill: #0178d4 } - .terminal-2550674499-r4 { fill: #3d3d3d } - .terminal-2550674499-r5 { fill: #1e1e1e;font-weight: bold } - .terminal-2550674499-r6 { fill: #515151 } - .terminal-2550674499-r7 { fill: #e1e1e1;text-decoration: underline; } - .terminal-2550674499-r8 { fill: #4ebf71;font-weight: bold } + .terminal-1099969603-r1 { fill: #e1e1e1 } + .terminal-1099969603-r2 { fill: #c5c8c6 } + .terminal-1099969603-r3 { fill: #0178d4 } + .terminal-1099969603-r4 { fill: #515151 } + .terminal-1099969603-r5 { fill: #1e1e1e;font-weight: bold } + .terminal-1099969603-r6 { fill: #e1e1e1;text-decoration: underline; } + .terminal-1099969603-r7 { fill: #3d3d3d } + .terminal-1099969603-r8 { fill: #4ebf71;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - - - - - - - - - - ─────────────────────────────────────── - Battlestar Galactica - Dune 1984 - Dune 2021 - Serenity - Star Trek: The Motion Picture - Star Wars: A New Hope - The Last Starfighter - Total Recall 👉 🔴 - Wing Commander - ─────────────────────────────────────── - - - - - - + + + + + + + + + + ─────────────────────────────────────── + Battlestar Galactica + Dune 1984 + Dune 2021 + Serenity + Star Trek: The Motion Picture + Star Wars: A New Hope + The Last Starfighter + Total Recall 👉 🔴 + Wing Commander + ─────────────────────────────────────── + + + + + + @@ -20019,139 +20019,139 @@ font-weight: 700; } - .terminal-3895672826-matrix { + .terminal-4163780602-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3895672826-title { + .terminal-4163780602-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3895672826-r1 { fill: #e1e1e1 } - .terminal-3895672826-r2 { fill: #c5c8c6 } - .terminal-3895672826-r3 { fill: #0178d4 } - .terminal-3895672826-r4 { fill: #666666 } - .terminal-3895672826-r5 { fill: #3d3d3d } - .terminal-3895672826-r6 { fill: #1e1e1e;font-weight: bold } - .terminal-3895672826-r7 { fill: #4ebf71;font-weight: bold } - .terminal-3895672826-r8 { fill: #cc555a;font-weight: bold;font-style: italic; } - .terminal-3895672826-r9 { fill: #515151 } - .terminal-3895672826-r10 { fill: #e1e1e1;text-decoration: underline; } + .terminal-4163780602-r1 { fill: #e1e1e1 } + .terminal-4163780602-r2 { fill: #c5c8c6 } + .terminal-4163780602-r3 { fill: #0178d4 } + .terminal-4163780602-r4 { fill: #666666 } + .terminal-4163780602-r5 { fill: #515151 } + .terminal-4163780602-r6 { fill: #1e1e1e;font-weight: bold } + .terminal-4163780602-r7 { fill: #e1e1e1;text-decoration: underline; } + .terminal-4163780602-r8 { fill: #3d3d3d } + .terminal-4163780602-r9 { fill: #4ebf71;font-weight: bold } + .terminal-4163780602-r10 { fill: #cc555a;font-weight: bold;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - - - - - - - - - - ────────────────────────────────────────────────────────────────────── - Battlestar GalacticaAmanda - Dune 1984Connor MacLeod - Dune 2021Duncan MacLeod - SerenityHeather MacLeod - Star Trek: The Motion PictureJoe Dawson - Star Wars: A New HopeKurgan, The - The Last StarfighterMethos - Total Recall 👉 🔴Rachel Ellenstein - Wing CommanderRamírez - ────────────────────────────────────────────────────────────────────── - - - - - - + + + + + + + + + + ────────────────────────────────────────────────────────────────────── + Battlestar GalacticaAmanda + Dune 1984Connor MacLeod + Dune 2021Duncan MacLeod + SerenityHeather MacLeod + Star Trek: The Motion PictureJoe Dawson + Star Wars: A New HopeKurgan, The + The Last StarfighterMethos + Total Recall 👉 🔴Rachel Ellenstein + Wing CommanderRamírez + ────────────────────────────────────────────────────────────────────── + + + + + + diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index b44722787d..dc9de5b1b2 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -57,13 +57,10 @@ async def test_radioset_inner_navigation(): async with RadioSetApp().run_test() as pilot: assert pilot.app.screen.focused is None await pilot.press("tab") - assert ( - pilot.app.screen.focused == pilot.app.query_one("#from_buttons").children[0] - ) for key, landing in (("down", 1), ("up", 0), ("right", 1), ("left", 0)): - await pilot.press(key) + await pilot.press(key, "enter") assert ( - pilot.app.screen.focused + pilot.app.query_one("#from_buttons", RadioSet).pressed_button == pilot.app.query_one("#from_buttons").children[landing] ) @@ -73,11 +70,11 @@ async def test_radioset_breakout_navigation(): async with RadioSetApp().run_test() as pilot: assert pilot.app.screen.focused is None await pilot.press("tab") - assert pilot.app.screen.focused.parent is pilot.app.query_one("#from_buttons") + assert pilot.app.screen.focused is pilot.app.query_one("#from_buttons") await pilot.press("tab") - assert pilot.app.screen.focused.parent is pilot.app.query_one("#from_strings") + assert pilot.app.screen.focused is pilot.app.query_one("#from_strings") await pilot.press("shift+tab") - assert pilot.app.screen.focused.parent is pilot.app.query_one("#from_buttons") + assert pilot.app.screen.focused is pilot.app.query_one("#from_buttons") class BadRadioSetApp(App[None]): From 0d5db5869277617846a4c4e89cb8d79d5a1ff7e4 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 15:09:35 +0100 Subject: [PATCH 06/10] Update the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5f08ad046..054fe495ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Breaking change: `HorizontalScroll` no longer shows a required vertical scrollbar by default - Breaking change: Renamed `App.action_add_class_` to `App.action_add_class` - Breaking change: Renamed `App.action_remove_class_` to `App.action_remove_class` +- Breaking change: `RadioSet` is now a single focusable widget ### Added From f2c56e7cd510c64354bfa939c423a844d7cf40bb Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 15:17:02 +0100 Subject: [PATCH 07/10] Link the RadioSet CHANGELOG entry to the new PR --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 054fe495ee..917ac0ea8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Breaking change: `HorizontalScroll` no longer shows a required vertical scrollbar by default - Breaking change: Renamed `App.action_add_class_` to `App.action_add_class` - Breaking change: Renamed `App.action_remove_class_` to `App.action_remove_class` -- Breaking change: `RadioSet` is now a single focusable widget +- Breaking change: `RadioSet` is now a single focusable widget https://github.com/Textualize/textual/pull/2372 ### Added From 88926a4bb8e000bff2b9855f31a09af387898dc9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 15:27:43 +0100 Subject: [PATCH 08/10] Force a wee wait when testing the demo This keeps randomly failing in Windows in CI; multiple subsequent runs gets it going in the end, normally one further fail at a time. So let's throw a wee wait on the end and see if that helps. --- tests/snapshot_tests/test_snapshots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 16001300b4..7cc85ef02e 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -295,7 +295,7 @@ def test_demo(snap_compare): """Test the demo app (python -m textual)""" assert snap_compare( Path("../../src/textual/demo.py"), - press=["down", "down", "down"], + press=["down", "down", "down", "wait:250"], terminal_size=(100, 30), ) From db4528764698be1e0a0fdcbffd1347da80f9c8d1 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 15:41:02 +0100 Subject: [PATCH 09/10] Lower the wait for the demo snapshot test 250 worked; so let's try it lower. --- tests/snapshot_tests/test_snapshots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 7cc85ef02e..c44558a210 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -295,7 +295,7 @@ def test_demo(snap_compare): """Test the demo app (python -m textual)""" assert snap_compare( Path("../../src/textual/demo.py"), - press=["down", "down", "down", "wait:250"], + press=["down", "down", "down", "wait:100"], terminal_size=(100, 30), ) From 7a536c790c14697ef4e51af39bc6e5e35e1ede23 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 25 Apr 2023 15:49:43 +0100 Subject: [PATCH 10/10] Demo snapshot test back up to a 250 wait Waiting 100 resulted in a fail, so let's bump back up again. --- tests/snapshot_tests/test_snapshots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index c44558a210..7cc85ef02e 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -295,7 +295,7 @@ def test_demo(snap_compare): """Test the demo app (python -m textual)""" assert snap_compare( Path("../../src/textual/demo.py"), - press=["down", "down", "down", "wait:100"], + press=["down", "down", "down", "wait:250"], terminal_size=(100, 30), )