We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
RadioButton still selectable by keyboard when disabled
It appears that when a `RadioButton` is disabled, click events no longer respond as expected, but you can still navigate to it with the keyboard and select it with `space` or `enter`.

![radio-select-disabled](https://github.com/Textualize/textual/assets/11033703/d3e8667f-a704-4d22-8692-c30873dd106a)

```python
from textual.app import App, ComposeResult
from textual.widgets import RadioButton, RadioSet, Static


class Test(App):
    DEFAULT_CSS = """
    RadioButton:disabled {
        color: red;
    }
    """

    def compose(self) -> ComposeResult:
        with RadioSet():
            yield RadioButton("Enabled 1")
            yield RadioButton("Disabled 2", disabled=True)
            yield RadioButton("Disabled 3", disabled=True)
        yield Static("Selected:", id="selected")

    def on_radio_set_changed(self, radio_set: RadioSet.Changed) -> None:
        self.query_one("#selected").update(f"Selected: {radio_set.pressed.label}")


if __name__ == "__main__":
    app = Test()
    app.run()
```

# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.44.1 |
| Rich    | 13.7.0 |

## Python

| Name           | Value                                                     |
|----------------|-----------------------------------------------------------|
| Version        | 3.8.10                                                    |
| Implementation | CPython                                                   |
| Compiler       | Clang 12.0.5 (clang-1205.0.22.9)                          |
| Executable     | /Users/geoff/Documents/projects/test/venv/bin/python |

## Operating System

| Name    | Value                                                                                                 |
|---------|-------------------------------------------------------------------------------------------------------|
| System  | Darwin                                                                                                |
| Release | 22.5.0                                                                                                |
| Version | Darwin Kernel Version 22.5.0: Thu Jun  8 22:22:19 PDT 2023; root:xnu-8796.121.3~7/RELEASE_ARM64_T8103 |

## Terminal

| Name                 | Value           |
|----------------------|-----------------|
| Terminal Application | vscode (1.85.0) |
| TERM                 | xterm-256color  |
| COLORTERM            | truecolor       |
| FORCE_COLOR          | *Not set*       |
| NO_COLOR             | *Not set*       |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=158, height=40 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 158                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 40                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |




--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/textual/widgets/_radio_set.py`
Content:
```
1 """Provides a RadioSet widget, which groups radio buttons."""
2 
3 from __future__ import annotations
4 
5 from typing import ClassVar, Optional
6 
7 import rich.repr
8 
9 from ..binding import Binding, BindingType
10 from ..containers import Container
11 from ..events import Click, Mount
12 from ..message import Message
13 from ..reactive import var
14 from ._radio_button import RadioButton
15 
16 
17 class RadioSet(Container, can_focus=True, can_focus_children=False):
18     """Widget for grouping a collection of radio buttons into a set.
19 
20     When a collection of [`RadioButton`][textual.widgets.RadioButton]s are
21     grouped with this widget, they will be treated as a mutually-exclusive
22     grouping. If one button is turned on, the previously-on button will be
23     turned off.
24     """
25 
26     DEFAULT_CSS = """
27     RadioSet {
28         border: tall transparent;
29         background: $boost;
30         padding: 0 1 0 0;
31         height: auto;
32         width: auto;
33     }
34 
35     RadioSet:focus {
36         border: tall $accent;
37     }
38 
39     /* The following rules/styles mimic similar ToggleButton:focus rules in
40      * ToggleButton. If those styles ever get updated, these should be too.
41      */
42 
43     RadioSet > * {
44         background: transparent;
45         border: none;
46         padding: 0 1;
47     }
48 
49     RadioSet:focus > RadioButton.-selected > .toggle--label {
50         text-style: underline;
51     }
52 
53     RadioSet:focus ToggleButton.-selected > .toggle--button {
54         background: $foreground 25%;
55     }
56 
57     RadioSet:focus > RadioButton.-on.-selected > .toggle--button {
58         background: $foreground 25%;
59     }
60     """
61 
62     BINDINGS: ClassVar[list[BindingType]] = [
63         Binding("down,right", "next_button", "", show=False),
64         Binding("enter,space", "toggle", "Toggle", show=False),
65         Binding("up,left", "previous_button", "", show=False),
66     ]
67     """
68     | Key(s) | Description |
69     | :- | :- |
70     | enter, space | Toggle the currently-selected button. |
71     | left, up | Select the previous radio button in the set. |
72     | right, down | Select the next radio button in the set. |
73     """
74 
75     _selected: var[int | None] = var[Optional[int]](None)
76     """The index of the currently-selected radio button."""
77 
78     @rich.repr.auto
79     class Changed(Message):
80         """Posted when the pressed button in the set changes.
81 
82         This message can be handled using an `on_radio_set_changed` method.
83         """
84 
85         ALLOW_SELECTOR_MATCH = {"pressed"}
86         """Additional message attributes that can be used with the [`on` decorator][textual.on]."""
87 
88         def __init__(self, radio_set: RadioSet, pressed: RadioButton) -> None:
89             """Initialise the message.
90 
91             Args:
92                 pressed: The radio button that was pressed.
93             """
94             super().__init__()
95             self.radio_set = radio_set
96             """A reference to the [`RadioSet`][textual.widgets.RadioSet] that was changed."""
97             self.pressed = pressed
98             """The [`RadioButton`][textual.widgets.RadioButton] that was pressed to make the change."""
99             self.index = radio_set.pressed_index
100             """The index of the [`RadioButton`][textual.widgets.RadioButton] that was pressed to make the change."""
101 
102         @property
103         def control(self) -> RadioSet:
104             """A reference to the [`RadioSet`][textual.widgets.RadioSet] that was changed.
105 
106             This is an alias for [`Changed.radio_set`][textual.widgets.RadioSet.Changed.radio_set]
107             and is used by the [`on`][textual.on] decorator.
108             """
109             return self.radio_set
110 
111         def __rich_repr__(self) -> rich.repr.Result:
112             yield "radio_set", self.radio_set
113             yield "pressed", self.pressed
114             yield "index", self.index
115 
116     def __init__(
117         self,
118         *buttons: str | RadioButton,
119         name: str | None = None,
120         id: str | None = None,
121         classes: str | None = None,
122         disabled: bool = False,
123     ) -> None:
124         """Initialise the radio set.
125 
126         Args:
127             buttons: The labels or [`RadioButton`][textual.widgets.RadioButton]s to group together.
128             name: The name of the radio set.
129             id: The ID of the radio set in the DOM.
130             classes: The CSS classes of the radio set.
131             disabled: Whether the radio set is disabled or not.
132 
133         Note:
134             When a `str` label is provided, a
135             [RadioButton][textual.widgets.RadioButton] will be created from
136             it.
137         """
138         self._pressed_button: RadioButton | None = None
139         """Holds the radio buttons we're responsible for."""
140         super().__init__(
141             *[
142                 (button if isinstance(button, RadioButton) else RadioButton(button))
143                 for button in buttons
144             ],
145             name=name,
146             id=id,
147             classes=classes,
148             disabled=disabled,
149         )
150 
151     def _on_mount(self, _: Mount) -> None:
152         """Perform some processing once mounted in the DOM."""
153 
154         # If there are radio buttons, select the first one.
155         if self._nodes:
156             self._selected = 0
157 
158         # Get all the buttons within us; we'll be doing a couple of things
159         # with that list.
160         buttons = list(self.query(RadioButton))
161 
162         # RadioButtons can have focus, by default. But we're going to take
163         # that over and handle movement between them. So here we tell them
164         # all they can't focus.
165         for button in buttons:
166             button.can_focus = False
167 
168         # It's possible for the user to pass in a collection of radio
169         # buttons, with more than one set to on; they shouldn't, but we
170         # can't stop them. So here we check for that and, for want of a
171         # better approach, we keep the first one on and turn all the others
172         # off.
173         switched_on = [button for button in buttons if button.value]
174         with self.prevent(RadioButton.Changed):
175             for button in switched_on[1:]:
176                 button.value = False
177 
178         # Keep track of which button is initially pressed.
179         if switched_on:
180             self._pressed_button = switched_on[0]
181 
182     def watch__selected(self) -> None:
183         self.query(RadioButton).remove_class("-selected")
184         if self._selected is not None:
185             self._nodes[self._selected].add_class("-selected")
186 
187     def _on_radio_button_changed(self, event: RadioButton.Changed) -> None:
188         """Respond to the value of a button in the set being changed.
189 
190         Args:
191             event: The event.
192         """
193         # We're going to consume the underlying radio button events, making
194         # it appear as if they don't emit their own, as far as the caller is
195         # concerned. As such, stop the event bubbling and also prohibit the
196         # same event being sent out if/when we make a value change in here.
197         event.stop()
198         with self.prevent(RadioButton.Changed):
199             # If the message pertains to a button being clicked to on...
200             if event.radio_button.value:
201                 # If there's a button pressed right now and it's not really a
202                 # case of the user mashing on the same button...
203                 if (
204                     self._pressed_button is not None
205                     and self._pressed_button != event.radio_button
206                 ):
207                     self._pressed_button.value = False
208                 # Make the pressed button this new button.
209                 self._pressed_button = event.radio_button
210                 # Emit a message to say our state has changed.
211                 self.post_message(self.Changed(self, event.radio_button))
212             else:
213                 # We're being clicked off, we don't want that.
214                 event.radio_button.value = True
215 
216     def _on_radio_set_changed(self, event: RadioSet.Changed) -> None:
217         """Handle a change to which button in the set is pressed.
218 
219         This handler ensures that, when a button is pressed, it's also the
220         selected button.
221         """
222         self._selected = event.index
223 
224     async def _on_click(self, _: Click) -> None:
225         """Handle a click on or within the radio set.
226 
227         This handler ensures that focus moves to the clicked radio set, even
228         if there's a click on one of the radio buttons it contains.
229         """
230         self.focus()
231 
232     @property
233     def pressed_button(self) -> RadioButton | None:
234         """The currently-pressed [`RadioButton`][textual.widgets.RadioButton], or `None` if none are pressed."""
235         return self._pressed_button
236 
237     @property
238     def pressed_index(self) -> int:
239         """The index of the currently-pressed [`RadioButton`][textual.widgets.RadioButton], or -1 if none are pressed."""
240         return (
241             self._nodes.index(self._pressed_button)
242             if self._pressed_button is not None
243             else -1
244         )
245 
246     def action_previous_button(self) -> None:
247         """Navigate to the previous button in the set.
248 
249         Note that this will wrap around to the end if at the start.
250         """
251         if self._nodes:
252             if self._selected == 0:
253                 self._selected = len(self.children) - 1
254             elif self._selected is None:
255                 self._selected = 0
256             else:
257                 self._selected -= 1
258 
259     def action_next_button(self) -> None:
260         """Navigate to the next button in the set.
261 
262         Note that this will wrap around to the start if at the end.
263         """
264         if self._nodes:
265             if self._selected is None or self._selected == len(self._nodes) - 1:
266                 self._selected = 0
267             else:
268                 self._selected += 1
269 
270     def action_toggle(self) -> None:
271         """Toggle the state of the currently-selected button."""
272         if self._selected is not None:
273             button = self._nodes[self._selected]
274             assert isinstance(button, RadioButton)
275             button.toggle()
276
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py
--- a/src/textual/widgets/_radio_set.py
+++ b/src/textual/widgets/_radio_set.py
@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import ClassVar, Optional
+from contextlib import suppress
+from typing import ClassVar, Literal, Optional
 
 import rich.repr
 
@@ -151,9 +152,8 @@
     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
+        # If there are radio buttons, select the first available one.
+        self.action_next_button()
 
         # Get all the buttons within us; we'll be doing a couple of things
         # with that list.
@@ -248,24 +248,58 @@
 
         Note that this will wrap around to the end if at the start.
         """
-        if self._nodes:
-            if self._selected == 0:
-                self._selected = len(self.children) - 1
-            elif self._selected is None:
-                self._selected = 0
-            else:
-                self._selected -= 1
+        self._move_selected_button(-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._nodes:
-            if self._selected is None or self._selected == len(self._nodes) - 1:
-                self._selected = 0
-            else:
-                self._selected += 1
+        self._move_selected_button(1)
+
+    def _move_selected_button(self, direction: Literal[-1, 1]) -> None:
+        """Move the selected button to the next or previous one.
+
+        Note that this will wrap around the start/end of the button list.
+
+        We compute the available buttons by ignoring the disabled ones and then
+        we induce an ordering by computing the distance to the currently selected one if
+        we start at the selected button and then start moving in the direction indicated.
+
+        For example, if the direction is `1` and self._selected is 2, we have this:
+        selected:     v
+        buttons:  X X X X X X X
+        indices:  0 1 2 3 4 5 6
+        distance: 5 6 0 1 2 3 4
+
+        Args:
+            direction: `1` to move to the next button and `-1` for the previous.
+        """
+
+        candidate_indices = (
+            index
+            for index, button in enumerate(self.children)
+            if not button.disabled and index != self._selected
+        )
+
+        if self._selected is None:
+            with suppress(StopIteration):
+                self._selected = next(candidate_indices)
+        else:
+            selected = self._selected
+
+            def distance(index: int) -> int:
+                """Induce a distance between the given index and the selected button.
+
+                Args:
+                    index: The index of the button to consider.
+
+                Returns:
+                    The distance between the two buttons.
+                """
+                return direction * (index - selected) % len(self.children)
+
+            self._selected = min(candidate_indices, key=distance, default=None)
 
     def action_toggle(self) -> None:
         """Toggle the state of the currently-selected button."""

{"golden_diff": "diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py\n--- a/src/textual/widgets/_radio_set.py\n+++ b/src/textual/widgets/_radio_set.py\n@@ -2,7 +2,8 @@\n \n from __future__ import annotations\n \n-from typing import ClassVar, Optional\n+from contextlib import suppress\n+from typing import ClassVar, Literal, Optional\n \n import rich.repr\n \n@@ -151,9 +152,8 @@\n     def _on_mount(self, _: Mount) -> None:\n         \"\"\"Perform some processing once mounted in the DOM.\"\"\"\n \n-        # If there are radio buttons, select the first one.\n-        if self._nodes:\n-            self._selected = 0\n+        # If there are radio buttons, select the first available one.\n+        self.action_next_button()\n \n         # Get all the buttons within us; we'll be doing a couple of things\n         # with that list.\n@@ -248,24 +248,58 @@\n \n         Note that this will wrap around to the end if at the start.\n         \"\"\"\n-        if self._nodes:\n-            if self._selected == 0:\n-                self._selected = len(self.children) - 1\n-            elif self._selected is None:\n-                self._selected = 0\n-            else:\n-                self._selected -= 1\n+        self._move_selected_button(-1)\n \n     def action_next_button(self) -> None:\n         \"\"\"Navigate to the next button in the set.\n \n         Note that this will wrap around to the start if at the end.\n         \"\"\"\n-        if self._nodes:\n-            if self._selected is None or self._selected == len(self._nodes) - 1:\n-                self._selected = 0\n-            else:\n-                self._selected += 1\n+        self._move_selected_button(1)\n+\n+    def _move_selected_button(self, direction: Literal[-1, 1]) -> None:\n+        \"\"\"Move the selected button to the next or previous one.\n+\n+        Note that this will wrap around the start/end of the button list.\n+\n+        We compute the available buttons by ignoring the disabled ones and then\n+        we induce an ordering by computing the distance to the currently selected one if\n+        we start at the selected button and then start moving in the direction indicated.\n+\n+        For example, if the direction is `1` and self._selected is 2, we have this:\n+        selected:     v\n+        buttons:  X X X X X X X\n+        indices:  0 1 2 3 4 5 6\n+        distance: 5 6 0 1 2 3 4\n+\n+        Args:\n+            direction: `1` to move to the next button and `-1` for the previous.\n+        \"\"\"\n+\n+        candidate_indices = (\n+            index\n+            for index, button in enumerate(self.children)\n+            if not button.disabled and index != self._selected\n+        )\n+\n+        if self._selected is None:\n+            with suppress(StopIteration):\n+                self._selected = next(candidate_indices)\n+        else:\n+            selected = self._selected\n+\n+            def distance(index: int) -> int:\n+                \"\"\"Induce a distance between the given index and the selected button.\n+\n+                Args:\n+                    index: The index of the button to consider.\n+\n+                Returns:\n+                    The distance between the two buttons.\n+                \"\"\"\n+                return direction * (index - selected) % len(self.children)\n+\n+            self._selected = min(candidate_indices, key=distance, default=None)\n \n     def action_toggle(self) -> None:\n         \"\"\"Toggle the state of the currently-selected button.\"\"\"\n", "issue": "RadioButton still selectable by keyboard when disabled\nIt appears that when a `RadioButton` is disabled, click events no longer respond as expected, but you can still navigate to it with the keyboard and select it with `space` or `enter`.\r\n\r\n![radio-select-disabled](https://github.com/Textualize/textual/assets/11033703/d3e8667f-a704-4d22-8692-c30873dd106a)\r\n\r\n```python\r\nfrom textual.app import App, ComposeResult\r\nfrom textual.widgets import RadioButton, RadioSet, Static\r\n\r\n\r\nclass Test(App):\r\n    DEFAULT_CSS = \"\"\"\r\n    RadioButton:disabled {\r\n        color: red;\r\n    }\r\n    \"\"\"\r\n\r\n    def compose(self) -> ComposeResult:\r\n        with RadioSet():\r\n            yield RadioButton(\"Enabled 1\")\r\n            yield RadioButton(\"Disabled 2\", disabled=True)\r\n            yield RadioButton(\"Disabled 3\", disabled=True)\r\n        yield Static(\"Selected:\", id=\"selected\")\r\n\r\n    def on_radio_set_changed(self, radio_set: RadioSet.Changed) -> None:\r\n        self.query_one(\"#selected\").update(f\"Selected: {radio_set.pressed.label}\")\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    app = Test()\r\n    app.run()\r\n```\r\n\r\n# Textual Diagnostics\r\n\r\n## Versions\r\n\r\n| Name    | Value  |\r\n|---------|--------|\r\n| Textual | 0.44.1 |\r\n| Rich    | 13.7.0 |\r\n\r\n## Python\r\n\r\n| Name           | Value                                                     |\r\n|----------------|-----------------------------------------------------------|\r\n| Version        | 3.8.10                                                    |\r\n| Implementation | CPython                                                   |\r\n| Compiler       | Clang 12.0.5 (clang-1205.0.22.9)                          |\r\n| Executable     | /Users/geoff/Documents/projects/test/venv/bin/python |\r\n\r\n## Operating System\r\n\r\n| Name    | Value                                                                                                 |\r\n|---------|-------------------------------------------------------------------------------------------------------|\r\n| System  | Darwin                                                                                                |\r\n| Release | 22.5.0                                                                                                |\r\n| Version | Darwin Kernel Version 22.5.0: Thu Jun  8 22:22:19 PDT 2023; root:xnu-8796.121.3~7/RELEASE_ARM64_T8103 |\r\n\r\n## Terminal\r\n\r\n| Name                 | Value           |\r\n|----------------------|-----------------|\r\n| Terminal Application | vscode (1.85.0) |\r\n| TERM                 | xterm-256color  |\r\n| COLORTERM            | truecolor       |\r\n| FORCE_COLOR          | *Not set*       |\r\n| NO_COLOR             | *Not set*       |\r\n\r\n## Rich Console options\r\n\r\n| Name           | Value                |\r\n|----------------|----------------------|\r\n| size           | width=158, height=40 |\r\n| legacy_windows | False                |\r\n| min_width      | 1                    |\r\n| max_width      | 158                  |\r\n| is_terminal    | True                 |\r\n| encoding       | utf-8                |\r\n| max_height     | 40                   |\r\n| justify        | None                 |\r\n| overflow       | None                 |\r\n| no_wrap        | False                |\r\n| highlight      | None                 |\r\n| markup         | None                 |\r\n| height         | None                 |\r\n\r\n\r\n\n", "before_files": [{"content": "\"\"\"Provides a RadioSet widget, which groups radio buttons.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import ClassVar, Optional\n\nimport rich.repr\n\nfrom ..binding import Binding, BindingType\nfrom ..containers import Container\nfrom ..events import Click, Mount\nfrom ..message import Message\nfrom ..reactive import var\nfrom ._radio_button import RadioButton\n\n\nclass RadioSet(Container, can_focus=True, can_focus_children=False):\n    \"\"\"Widget for grouping a collection of radio buttons into a set.\n\n    When a collection of [`RadioButton`][textual.widgets.RadioButton]s are\n    grouped with this widget, they will be treated as a mutually-exclusive\n    grouping. If one button is turned on, the previously-on button will be\n    turned off.\n    \"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    RadioSet {\n        border: tall transparent;\n        background: $boost;\n        padding: 0 1 0 0;\n        height: auto;\n        width: auto;\n    }\n\n    RadioSet:focus {\n        border: tall $accent;\n    }\n\n    /* The following rules/styles mimic similar ToggleButton:focus rules in\n     * ToggleButton. If those styles ever get updated, these should be too.\n     */\n\n    RadioSet > * {\n        background: transparent;\n        border: none;\n        padding: 0 1;\n    }\n\n    RadioSet:focus > RadioButton.-selected > .toggle--label {\n        text-style: underline;\n    }\n\n    RadioSet:focus ToggleButton.-selected > .toggle--button {\n        background: $foreground 25%;\n    }\n\n    RadioSet:focus > RadioButton.-on.-selected > .toggle--button {\n        background: $foreground 25%;\n    }\n    \"\"\"\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        Binding(\"down,right\", \"next_button\", \"\", show=False),\n        Binding(\"enter,space\", \"toggle\", \"Toggle\", show=False),\n        Binding(\"up,left\", \"previous_button\", \"\", show=False),\n    ]\n    \"\"\"\n    | Key(s) | Description |\n    | :- | :- |\n    | enter, space | Toggle the currently-selected button. |\n    | left, up | Select the previous radio button in the set. |\n    | right, down | Select the next radio button in the set. |\n    \"\"\"\n\n    _selected: var[int | None] = var[Optional[int]](None)\n    \"\"\"The index of the currently-selected radio button.\"\"\"\n\n    @rich.repr.auto\n    class Changed(Message):\n        \"\"\"Posted when the pressed button in the set changes.\n\n        This message can be handled using an `on_radio_set_changed` method.\n        \"\"\"\n\n        ALLOW_SELECTOR_MATCH = {\"pressed\"}\n        \"\"\"Additional message attributes that can be used with the [`on` decorator][textual.on].\"\"\"\n\n        def __init__(self, radio_set: RadioSet, pressed: RadioButton) -> None:\n            \"\"\"Initialise the message.\n\n            Args:\n                pressed: The radio button that was pressed.\n            \"\"\"\n            super().__init__()\n            self.radio_set = radio_set\n            \"\"\"A reference to the [`RadioSet`][textual.widgets.RadioSet] that was changed.\"\"\"\n            self.pressed = pressed\n            \"\"\"The [`RadioButton`][textual.widgets.RadioButton] that was pressed to make the change.\"\"\"\n            self.index = radio_set.pressed_index\n            \"\"\"The index of the [`RadioButton`][textual.widgets.RadioButton] that was pressed to make the change.\"\"\"\n\n        @property\n        def control(self) -> RadioSet:\n            \"\"\"A reference to the [`RadioSet`][textual.widgets.RadioSet] that was changed.\n\n            This is an alias for [`Changed.radio_set`][textual.widgets.RadioSet.Changed.radio_set]\n            and is used by the [`on`][textual.on] decorator.\n            \"\"\"\n            return self.radio_set\n\n        def __rich_repr__(self) -> rich.repr.Result:\n            yield \"radio_set\", self.radio_set\n            yield \"pressed\", self.pressed\n            yield \"index\", self.index\n\n    def __init__(\n        self,\n        *buttons: str | RadioButton,\n        name: str | None = None,\n        id: str | None = None,\n        classes: str | None = None,\n        disabled: bool = False,\n    ) -> None:\n        \"\"\"Initialise the radio set.\n\n        Args:\n            buttons: The labels or [`RadioButton`][textual.widgets.RadioButton]s to group together.\n            name: The name of the radio set.\n            id: The ID of the radio set in the DOM.\n            classes: The CSS classes of the radio set.\n            disabled: Whether the radio set is disabled or not.\n\n        Note:\n            When a `str` label is provided, a\n            [RadioButton][textual.widgets.RadioButton] will be created from\n            it.\n        \"\"\"\n        self._pressed_button: RadioButton | None = None\n        \"\"\"Holds the radio buttons we're responsible for.\"\"\"\n        super().__init__(\n            *[\n                (button if isinstance(button, RadioButton) else RadioButton(button))\n                for button in buttons\n            ],\n            name=name,\n            id=id,\n            classes=classes,\n            disabled=disabled,\n        )\n\n    def _on_mount(self, _: Mount) -> None:\n        \"\"\"Perform some processing once mounted in the DOM.\"\"\"\n\n        # If there are radio buttons, select the first one.\n        if self._nodes:\n            self._selected = 0\n\n        # Get all the buttons within us; we'll be doing a couple of things\n        # with that list.\n        buttons = list(self.query(RadioButton))\n\n        # RadioButtons can have focus, by default. But we're going to take\n        # that over and handle movement between them. So here we tell them\n        # all they can't focus.\n        for button in buttons:\n            button.can_focus = False\n\n        # It's possible for the user to pass in a collection of radio\n        # buttons, with more than one set to on; they shouldn't, but we\n        # can't stop them. So here we check for that and, for want of a\n        # better approach, we keep the first one on and turn all the others\n        # off.\n        switched_on = [button for button in buttons if button.value]\n        with self.prevent(RadioButton.Changed):\n            for button in switched_on[1:]:\n                button.value = False\n\n        # Keep track of which button is initially pressed.\n        if switched_on:\n            self._pressed_button = switched_on[0]\n\n    def watch__selected(self) -> None:\n        self.query(RadioButton).remove_class(\"-selected\")\n        if self._selected is not None:\n            self._nodes[self._selected].add_class(\"-selected\")\n\n    def _on_radio_button_changed(self, event: RadioButton.Changed) -> None:\n        \"\"\"Respond to the value of a button in the set being changed.\n\n        Args:\n            event: The event.\n        \"\"\"\n        # We're going to consume the underlying radio button events, making\n        # it appear as if they don't emit their own, as far as the caller is\n        # concerned. As such, stop the event bubbling and also prohibit the\n        # same event being sent out if/when we make a value change in here.\n        event.stop()\n        with self.prevent(RadioButton.Changed):\n            # If the message pertains to a button being clicked to on...\n            if event.radio_button.value:\n                # If there's a button pressed right now and it's not really a\n                # case of the user mashing on the same button...\n                if (\n                    self._pressed_button is not None\n                    and self._pressed_button != event.radio_button\n                ):\n                    self._pressed_button.value = False\n                # Make the pressed button this new button.\n                self._pressed_button = event.radio_button\n                # Emit a message to say our state has changed.\n                self.post_message(self.Changed(self, event.radio_button))\n            else:\n                # We're being clicked off, we don't want that.\n                event.radio_button.value = True\n\n    def _on_radio_set_changed(self, event: RadioSet.Changed) -> None:\n        \"\"\"Handle a change to which button in the set is pressed.\n\n        This handler ensures that, when a button is pressed, it's also the\n        selected button.\n        \"\"\"\n        self._selected = event.index\n\n    async def _on_click(self, _: Click) -> None:\n        \"\"\"Handle a click on or within the radio set.\n\n        This handler ensures that focus moves to the clicked radio set, even\n        if there's a click on one of the radio buttons it contains.\n        \"\"\"\n        self.focus()\n\n    @property\n    def pressed_button(self) -> RadioButton | None:\n        \"\"\"The currently-pressed [`RadioButton`][textual.widgets.RadioButton], or `None` if none are pressed.\"\"\"\n        return self._pressed_button\n\n    @property\n    def pressed_index(self) -> int:\n        \"\"\"The index of the currently-pressed [`RadioButton`][textual.widgets.RadioButton], or -1 if none are pressed.\"\"\"\n        return (\n            self._nodes.index(self._pressed_button)\n            if self._pressed_button is not None\n            else -1\n        )\n\n    def action_previous_button(self) -> None:\n        \"\"\"Navigate to the previous button in the set.\n\n        Note that this will wrap around to the end if at the start.\n        \"\"\"\n        if self._nodes:\n            if self._selected == 0:\n                self._selected = len(self.children) - 1\n            elif self._selected is None:\n                self._selected = 0\n            else:\n                self._selected -= 1\n\n    def action_next_button(self) -> None:\n        \"\"\"Navigate to the next button in the set.\n\n        Note that this will wrap around to the start if at the end.\n        \"\"\"\n        if self._nodes:\n            if self._selected is None or self._selected == len(self._nodes) - 1:\n                self._selected = 0\n            else:\n                self._selected += 1\n\n    def action_toggle(self) -> None:\n        \"\"\"Toggle the state of the currently-selected button.\"\"\"\n        if self._selected is not None:\n            button = self._nodes[self._selected]\n            assert isinstance(button, RadioButton)\n            button.toggle()\n", "path": "src/textual/widgets/_radio_set.py"}], "after_files": [{"content": "\"\"\"Provides a RadioSet widget, which groups radio buttons.\"\"\"\n\nfrom __future__ import annotations\n\nfrom contextlib import suppress\nfrom typing import ClassVar, Literal, Optional\n\nimport rich.repr\n\nfrom ..binding import Binding, BindingType\nfrom ..containers import Container\nfrom ..events import Click, Mount\nfrom ..message import Message\nfrom ..reactive import var\nfrom ._radio_button import RadioButton\n\n\nclass RadioSet(Container, can_focus=True, can_focus_children=False):\n    \"\"\"Widget for grouping a collection of radio buttons into a set.\n\n    When a collection of [`RadioButton`][textual.widgets.RadioButton]s are\n    grouped with this widget, they will be treated as a mutually-exclusive\n    grouping. If one button is turned on, the previously-on button will be\n    turned off.\n    \"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    RadioSet {\n        border: tall transparent;\n        background: $boost;\n        padding: 0 1 0 0;\n        height: auto;\n        width: auto;\n    }\n\n    RadioSet:focus {\n        border: tall $accent;\n    }\n\n    /* The following rules/styles mimic similar ToggleButton:focus rules in\n     * ToggleButton. If those styles ever get updated, these should be too.\n     */\n\n    RadioSet > * {\n        background: transparent;\n        border: none;\n        padding: 0 1;\n    }\n\n    RadioSet:focus > RadioButton.-selected > .toggle--label {\n        text-style: underline;\n    }\n\n    RadioSet:focus ToggleButton.-selected > .toggle--button {\n        background: $foreground 25%;\n    }\n\n    RadioSet:focus > RadioButton.-on.-selected > .toggle--button {\n        background: $foreground 25%;\n    }\n    \"\"\"\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        Binding(\"down,right\", \"next_button\", \"\", show=False),\n        Binding(\"enter,space\", \"toggle\", \"Toggle\", show=False),\n        Binding(\"up,left\", \"previous_button\", \"\", show=False),\n    ]\n    \"\"\"\n    | Key(s) | Description |\n    | :- | :- |\n    | enter, space | Toggle the currently-selected button. |\n    | left, up | Select the previous radio button in the set. |\n    | right, down | Select the next radio button in the set. |\n    \"\"\"\n\n    _selected: var[int | None] = var[Optional[int]](None)\n    \"\"\"The index of the currently-selected radio button.\"\"\"\n\n    @rich.repr.auto\n    class Changed(Message):\n        \"\"\"Posted when the pressed button in the set changes.\n\n        This message can be handled using an `on_radio_set_changed` method.\n        \"\"\"\n\n        ALLOW_SELECTOR_MATCH = {\"pressed\"}\n        \"\"\"Additional message attributes that can be used with the [`on` decorator][textual.on].\"\"\"\n\n        def __init__(self, radio_set: RadioSet, pressed: RadioButton) -> None:\n            \"\"\"Initialise the message.\n\n            Args:\n                pressed: The radio button that was pressed.\n            \"\"\"\n            super().__init__()\n            self.radio_set = radio_set\n            \"\"\"A reference to the [`RadioSet`][textual.widgets.RadioSet] that was changed.\"\"\"\n            self.pressed = pressed\n            \"\"\"The [`RadioButton`][textual.widgets.RadioButton] that was pressed to make the change.\"\"\"\n            self.index = radio_set.pressed_index\n            \"\"\"The index of the [`RadioButton`][textual.widgets.RadioButton] that was pressed to make the change.\"\"\"\n\n        @property\n        def control(self) -> RadioSet:\n            \"\"\"A reference to the [`RadioSet`][textual.widgets.RadioSet] that was changed.\n\n            This is an alias for [`Changed.radio_set`][textual.widgets.RadioSet.Changed.radio_set]\n            and is used by the [`on`][textual.on] decorator.\n            \"\"\"\n            return self.radio_set\n\n        def __rich_repr__(self) -> rich.repr.Result:\n            yield \"radio_set\", self.radio_set\n            yield \"pressed\", self.pressed\n            yield \"index\", self.index\n\n    def __init__(\n        self,\n        *buttons: str | RadioButton,\n        name: str | None = None,\n        id: str | None = None,\n        classes: str | None = None,\n        disabled: bool = False,\n    ) -> None:\n        \"\"\"Initialise the radio set.\n\n        Args:\n            buttons: The labels or [`RadioButton`][textual.widgets.RadioButton]s to group together.\n            name: The name of the radio set.\n            id: The ID of the radio set in the DOM.\n            classes: The CSS classes of the radio set.\n            disabled: Whether the radio set is disabled or not.\n\n        Note:\n            When a `str` label is provided, a\n            [RadioButton][textual.widgets.RadioButton] will be created from\n            it.\n        \"\"\"\n        self._pressed_button: RadioButton | None = None\n        \"\"\"Holds the radio buttons we're responsible for.\"\"\"\n        super().__init__(\n            *[\n                (button if isinstance(button, RadioButton) else RadioButton(button))\n                for button in buttons\n            ],\n            name=name,\n            id=id,\n            classes=classes,\n            disabled=disabled,\n        )\n\n    def _on_mount(self, _: Mount) -> None:\n        \"\"\"Perform some processing once mounted in the DOM.\"\"\"\n\n        # If there are radio buttons, select the first available one.\n        self.action_next_button()\n\n        # Get all the buttons within us; we'll be doing a couple of things\n        # with that list.\n        buttons = list(self.query(RadioButton))\n\n        # RadioButtons can have focus, by default. But we're going to take\n        # that over and handle movement between them. So here we tell them\n        # all they can't focus.\n        for button in buttons:\n            button.can_focus = False\n\n        # It's possible for the user to pass in a collection of radio\n        # buttons, with more than one set to on; they shouldn't, but we\n        # can't stop them. So here we check for that and, for want of a\n        # better approach, we keep the first one on and turn all the others\n        # off.\n        switched_on = [button for button in buttons if button.value]\n        with self.prevent(RadioButton.Changed):\n            for button in switched_on[1:]:\n                button.value = False\n\n        # Keep track of which button is initially pressed.\n        if switched_on:\n            self._pressed_button = switched_on[0]\n\n    def watch__selected(self) -> None:\n        self.query(RadioButton).remove_class(\"-selected\")\n        if self._selected is not None:\n            self._nodes[self._selected].add_class(\"-selected\")\n\n    def _on_radio_button_changed(self, event: RadioButton.Changed) -> None:\n        \"\"\"Respond to the value of a button in the set being changed.\n\n        Args:\n            event: The event.\n        \"\"\"\n        # We're going to consume the underlying radio button events, making\n        # it appear as if they don't emit their own, as far as the caller is\n        # concerned. As such, stop the event bubbling and also prohibit the\n        # same event being sent out if/when we make a value change in here.\n        event.stop()\n        with self.prevent(RadioButton.Changed):\n            # If the message pertains to a button being clicked to on...\n            if event.radio_button.value:\n                # If there's a button pressed right now and it's not really a\n                # case of the user mashing on the same button...\n                if (\n                    self._pressed_button is not None\n                    and self._pressed_button != event.radio_button\n                ):\n                    self._pressed_button.value = False\n                # Make the pressed button this new button.\n                self._pressed_button = event.radio_button\n                # Emit a message to say our state has changed.\n                self.post_message(self.Changed(self, event.radio_button))\n            else:\n                # We're being clicked off, we don't want that.\n                event.radio_button.value = True\n\n    def _on_radio_set_changed(self, event: RadioSet.Changed) -> None:\n        \"\"\"Handle a change to which button in the set is pressed.\n\n        This handler ensures that, when a button is pressed, it's also the\n        selected button.\n        \"\"\"\n        self._selected = event.index\n\n    async def _on_click(self, _: Click) -> None:\n        \"\"\"Handle a click on or within the radio set.\n\n        This handler ensures that focus moves to the clicked radio set, even\n        if there's a click on one of the radio buttons it contains.\n        \"\"\"\n        self.focus()\n\n    @property\n    def pressed_button(self) -> RadioButton | None:\n        \"\"\"The currently-pressed [`RadioButton`][textual.widgets.RadioButton], or `None` if none are pressed.\"\"\"\n        return self._pressed_button\n\n    @property\n    def pressed_index(self) -> int:\n        \"\"\"The index of the currently-pressed [`RadioButton`][textual.widgets.RadioButton], or -1 if none are pressed.\"\"\"\n        return (\n            self._nodes.index(self._pressed_button)\n            if self._pressed_button is not None\n            else -1\n        )\n\n    def action_previous_button(self) -> None:\n        \"\"\"Navigate to the previous button in the set.\n\n        Note that this will wrap around to the end if at the start.\n        \"\"\"\n        self._move_selected_button(-1)\n\n    def action_next_button(self) -> None:\n        \"\"\"Navigate to the next button in the set.\n\n        Note that this will wrap around to the start if at the end.\n        \"\"\"\n        self._move_selected_button(1)\n\n    def _move_selected_button(self, direction: Literal[-1, 1]) -> None:\n        \"\"\"Move the selected button to the next or previous one.\n\n        Note that this will wrap around the start/end of the button list.\n\n        We compute the available buttons by ignoring the disabled ones and then\n        we induce an ordering by computing the distance to the currently selected one if\n        we start at the selected button and then start moving in the direction indicated.\n\n        For example, if the direction is `1` and self._selected is 2, we have this:\n        selected:     v\n        buttons:  X X X X X X X\n        indices:  0 1 2 3 4 5 6\n        distance: 5 6 0 1 2 3 4\n\n        Args:\n            direction: `1` to move to the next button and `-1` for the previous.\n        \"\"\"\n\n        candidate_indices = (\n            index\n            for index, button in enumerate(self.children)\n            if not button.disabled and index != self._selected\n        )\n\n        if self._selected is None:\n            with suppress(StopIteration):\n                self._selected = next(candidate_indices)\n        else:\n            selected = self._selected\n\n            def distance(index: int) -> int:\n                \"\"\"Induce a distance between the given index and the selected button.\n\n                Args:\n                    index: The index of the button to consider.\n\n                Returns:\n                    The distance between the two buttons.\n                \"\"\"\n                return direction * (index - selected) % len(self.children)\n\n            self._selected = min(candidate_indices, key=distance, default=None)\n\n    def action_toggle(self) -> None:\n        \"\"\"Toggle the state of the currently-selected button.\"\"\"\n        if self._selected is not None:\n            button = self._nodes[self._selected]\n            assert isinstance(button, RadioButton)\n            button.toggle()\n", "path": "src/textual/widgets/_radio_set.py"}]}