Skip to content

Commit e27c41c

Browse files
authored
fix(text area)!: stop escape shifting focus if default tab behaviour (#4125)
* fix(text area): stop escape shifting focus if default tab behaviour * fix recent update to changelog * address review feedback * update changelog
1 parent 5d6c61a commit e27c41c

File tree

4 files changed

+75
-9
lines changed

4 files changed

+75
-9
lines changed

CHANGELOG.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8-
## [0.48.2] - 2024-02-02
9-
10-
### Fixed
11-
12-
- Fixed a hang in the Linux driver when connected to a pipe https://github.com/Textualize/textual/issues/4104
13-
- Fixed broken `OptionList` `Option.id` mappings https://github.com/Textualize/textual/issues/4101
8+
## Unreleased
149

1510
### Added
1611

@@ -20,6 +15,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2015
- Added DOMNode.action_toggle https://github.com/Textualize/textual/pull/4075
2116
- Added Worker.cancelled_event https://github.com/Textualize/textual/pull/4075
2217

18+
### Fixed
19+
20+
- Breaking change: `TextArea` will not use `Escape` to shift focus if the `tab_behaviour` is the default https://github.com/Textualize/textual/issues/4110
21+
22+
## [0.48.2] - 2024-02-02
23+
24+
### Fixed
25+
26+
- Fixed a hang in the Linux driver when connected to a pipe https://github.com/Textualize/textual/issues/4104
27+
- Fixed broken `OptionList` `Option.id` mappings https://github.com/Textualize/textual/issues/4101
28+
2329
### Changed
2430

2531
- Breaking change: keyboard navigation in `RadioSet`, `ListView`, `OptionList`, and `SelectionList`, no longer allows highlighting disabled items https://github.com/Textualize/textual/issues/3881

docs/widgets/text_area.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,13 @@ This immediately updates the appearance of the `TextArea`:
283283
```{.textual path="docs/examples/widgets/text_area_custom_theme.py" columns="42" lines="8"}
284284
```
285285

286-
### Tab behaviour
286+
### Tab and Escape behaviour
287287

288288
Pressing the ++tab++ key will shift focus to the next widget in your application by default.
289289
This matches how other widgets work in Textual.
290+
290291
To have ++tab++ insert a `\t` character, set the `tab_behaviour` attribute to the string value `"indent"`.
292+
While in this mode, you can shift focus by pressing the ++escape++ key.
291293

292294
### Indentation
293295

src/textual/widgets/_text_area.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@ class TextArea(ScrollView, can_focus=True):
153153
"""
154154

155155
BINDINGS = [
156-
Binding("escape", "screen.focus_next", "Shift Focus", show=False),
157156
# Cursor movement
158157
Binding("up", "cursor_up", "cursor up", show=False),
159158
Binding("down", "cursor_down", "cursor down", show=False),
@@ -213,7 +212,6 @@ class TextArea(ScrollView, can_focus=True):
213212
"""
214213
| Key(s) | Description |
215214
| :- | :- |
216-
| escape | Focus on the next item. |
217215
| up | Move the cursor up. |
218216
| down | Move the cursor down. |
219217
| left | Move the cursor left. |
@@ -1213,6 +1211,11 @@ async def _on_key(self, event: events.Key) -> None:
12131211
"enter": "\n",
12141212
}
12151213
if self.tab_behaviour == "indent":
1214+
if key == "escape":
1215+
event.stop()
1216+
event.prevent_default()
1217+
self.screen.focus_next()
1218+
return
12161219
if self.indent_type == "tabs":
12171220
insert_values["tab"] = "\t"
12181221
else:
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from textual.app import App, ComposeResult
2+
from textual.screen import ModalScreen
3+
from textual.widgets import Button, TextArea
4+
5+
6+
class TextAreaDialog(ModalScreen):
7+
BINDINGS = [("escape", "dismiss")]
8+
9+
def compose(self) -> ComposeResult:
10+
yield TextArea(
11+
tab_behaviour="focus", # the default
12+
)
13+
yield Button("Submit")
14+
15+
16+
class TextAreaDialogApp(App):
17+
def on_mount(self) -> None:
18+
self.push_screen(TextAreaDialog())
19+
20+
21+
async def test_escape_key_when_tab_behaviour_is_focus():
22+
"""Regression test for https://github.com/Textualize/textual/issues/4110
23+
24+
When the `tab_behaviour` of TextArea is the default to shift focus,
25+
pressing <Escape> should not shift focus but instead skip and allow any
26+
parent bindings to run.
27+
"""
28+
29+
app = TextAreaDialogApp()
30+
async with app.run_test() as pilot:
31+
# Sanity check
32+
assert isinstance(pilot.app.screen, TextAreaDialog)
33+
assert isinstance(pilot.app.focused, TextArea)
34+
35+
# Pressing escape should dismiss the dialog screen, not focus the button
36+
await pilot.press("escape")
37+
assert not isinstance(pilot.app.screen, TextAreaDialog)
38+
39+
40+
async def test_escape_key_when_tab_behaviour_is_indent():
41+
"""When the `tab_behaviour` of TextArea is indent rather than switch focus,
42+
pressing <Escape> should instead shift focus.
43+
"""
44+
45+
app = TextAreaDialogApp()
46+
async with app.run_test() as pilot:
47+
# Sanity check
48+
assert isinstance(pilot.app.screen, TextAreaDialog)
49+
assert isinstance(pilot.app.focused, TextArea)
50+
51+
pilot.app.query_one(TextArea).tab_behaviour = "indent"
52+
# Pressing escape should focus the button, not dismiss the dialog screen
53+
await pilot.press("escape")
54+
assert isinstance(pilot.app.screen, TextAreaDialog)
55+
assert isinstance(pilot.app.focused, Button)

0 commit comments

Comments
 (0)