diff --git a/docs/guide/events.md b/docs/guide/events.md
index 9c4b54b49..77e40ce4e 100644
--- a/docs/guide/events.md
+++ b/docs/guide/events.md
@@ -51,14 +51,40 @@ Let's explore how Textual decides what method to call for a given event.
### Default behaviors
-You may be familiar with using Python's [super](https://docs.python.org/3/library/functions.html#super) function to call a function defined in a base class. You will not have to do this for Textual event handlers as Textual will automatically call any handler methods defined in the base class *after* the current handler has run. This allows textual to run any default behavior for the given event.
+You may be familiar with Python's [super](https://docs.python.org/3/library/functions.html#super) function to call a function defined in a base class. You will not have to do this for Textual event handlers as Textual will automatically call any handler methods defined in the base class.
-For instance if a widget defines an `on_key` handler it will run when the user hits a key. Textual will also run `Widget.on_key`, which allows Textual to respond to any key bindings. This is generally desirable, but you can prevent Textual from running the base class handler by calling [prevent_default()][textual.message.Message.prevent_default] on the event object.
+For instance if you define a custom widget, Textual will call its `on_key` handler when you hit a key. Textual will also run any `on_key` methods found in the widget's base classes, including `Widget.on_key` where key bindings are processed. Without this behavior, you would have to remember to call `super().on_key(event)` or key bindings would break.
+
+If you don't want this behavior you can call [prevent_default()][textual.message.Message.prevent_default] on the event object. This tells Textual not to call any handlers on base classes.
-For the case of key events, you may want to prevent the default behavior for keys that you handle by calling `event.prevent_default()`, but allow the base class to handle all other keys.
### Bubbling
+Messages have a `bubble` attribute. If this is set to `True` then events will be sent to their parent widget. Input events typically bubble so that a widget will have the opportunity to process events after its children.
+
+The following diagram shows an (abbreviated) DOM for a UI with a container and two buttons. With the "No" button [focused](#) it will receive the key event first.
+
+
+
+The App class is always the root of the DOM, so there is no where for the event to bubble to.
+
+#### Stopping bubbling
+
+Event handlers may stop this bubble behavior by calling the [stop()][textual.message.Message.stop] method on the event or message. You might want to do this if a widget has responded to the event in an authoritative way. For instance if a text input widget as responded to a key event you probably do not want it to also invoke a key binding.
diff --git a/docs/images/events/bubble1.excalidraw.svg b/docs/images/events/bubble1.excalidraw.svg
new file mode 100644
index 000000000..f4b79d101
--- /dev/null
+++ b/docs/images/events/bubble1.excalidraw.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/docs/images/events/bubble2.excalidraw.svg b/docs/images/events/bubble2.excalidraw.svg
new file mode 100644
index 000000000..e3e4d2079
--- /dev/null
+++ b/docs/images/events/bubble2.excalidraw.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/docs/images/events/bubble3.excalidraw.svg b/docs/images/events/bubble3.excalidraw.svg
new file mode 100644
index 000000000..929c598e3
--- /dev/null
+++ b/docs/images/events/bubble3.excalidraw.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/src/textual/message.py b/src/textual/message.py
index 63be90749..a936af780 100644
--- a/src/textual/message.py
+++ b/src/textual/message.py
@@ -71,10 +71,11 @@ class Message:
@property
def handler_name(self) -> str:
+ """The name of the handler associated with this message."""
# Property to make it read only
return self._handler_name
- def set_forwarded(self) -> None:
+ def _set_forwarded(self) -> None:
"""Mark this event as being forwarded."""
self._forwarded = True
@@ -90,7 +91,8 @@ class Message:
return False
def prevent_default(self, prevent: bool = True) -> Message:
- """Suppress the default action.
+ """Suppress the default action(s). This will prevent handlers in any base classes
+ from being called.
Args:
prevent (bool, optional): True if the default action should be suppressed,
diff --git a/src/textual/scroll_view.py b/src/textual/scroll_view.py
index cf61f13e2..dc0842ce1 100644
--- a/src/textual/scroll_view.py
+++ b/src/textual/scroll_view.py
@@ -71,6 +71,22 @@ class ScrollView(Widget):
def watch_virtual_size(self, virtual_size: Size) -> None:
self._scroll_update(virtual_size)
+ def watch_show_horizontal_scrollbar(self, value: bool) -> None:
+ """Watch function for show_horizontal_scrollbar attribute.
+
+ Args:
+ value (bool): Show horizontal scrollbar flag.
+ """
+ self.refresh(layout=True)
+
+ def watch_show_vertical_scrollbar(self, value: bool) -> None:
+ """Watch function for show_vertical_scrollbar attribute.
+
+ Args:
+ value (bool): Show vertical scrollbar flag.
+ """
+ self.refresh(layout=True)
+
def _size_updated(
self, size: Size, virtual_size: Size, container_size: Size
) -> None:
diff --git a/src/textual/widget.py b/src/textual/widget.py
index 861dcbe54..cfa3d2f28 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -234,10 +234,10 @@ class Widget(DOMNode):
Args:
value (bool): Show horizontal scrollbar flag.
"""
- self.refresh(layout=True)
- # if not value:
- # # reset the scroll position if the scrollbar is hidden.
- # self.scroll_to(0, 0, animate=False)
+ # self.refresh(layout=True)
+ if not value:
+ # reset the scroll position if the scrollbar is hidden.
+ self.scroll_to(0, 0, animate=False)
def watch_show_vertical_scrollbar(self, value: bool) -> None:
"""Watch function for show_vertical_scrollbar attribute.
@@ -245,10 +245,10 @@ class Widget(DOMNode):
Args:
value (bool): Show vertical scrollbar flag.
"""
- self.refresh(layout=True)
- # if not value:
- # # reset the scroll position if the scrollbar is hidden.
- # self.scroll_to(0, 0, animate=False)
+ # self.refresh(layout=True)
+ if not value:
+ # reset the scroll position if the scrollbar is hidden.
+ self.scroll_to(0, 0, animate=False)
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
"""Mount child widgets (making this widget a container).
@@ -507,7 +507,7 @@ class Widget(DOMNode):
elif overflow_y == "auto":
show_vertical = self.virtual_size.height > height
- if show_vertical and not show_horizontal:
+ if show_vertical and not show_horizontal and overflow_x == "auto":
show_horizontal = (
self.virtual_size.width + styles.scrollbar_size_vertical > width
)
@@ -853,7 +853,7 @@ class Widget(DOMNode):
y (int | None, optional): Y coordinate (row) to scroll to, or None for no change. Defaults to None.
animate (bool, optional): Animate to new scroll position. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if the scroll position changed, otherwise False.
@@ -895,8 +895,8 @@ class Widget(DOMNode):
scroll_y = self.scroll_y
self.scroll_target_y = self.scroll_y = y
scrolled_y = scroll_y != self.scroll_y
- if scrolled_x or scrolled_y:
- self.refresh(repaint=False, layout=True)
+ # if scrolled_x or scrolled_y:
+ # self.refresh(repaint=False, layout=False)
return scrolled_x or scrolled_y
@@ -916,7 +916,7 @@ class Widget(DOMNode):
y (int | None, optional): Y distance (rows) to scroll, or ``None`` for no change. Defaults to None.
animate (bool, optional): Animate to new scroll position. Defaults to False.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if the scroll position changed, otherwise False.
@@ -941,7 +941,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -962,7 +962,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -986,7 +986,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1008,7 +1008,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1030,7 +1030,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1052,7 +1052,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1074,7 +1074,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1099,7 +1099,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1124,7 +1124,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1151,7 +1151,7 @@ class Widget(DOMNode):
Args:
animate (bool, optional): Animate scroll. Defaults to True.
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
- duration (float | None, optional): Duration of animation, if animate is True and speed is False.
+ duration (float | None, optional): Duration of animation, if animate is True and speed is None.
Returns:
bool: True if any scrolling was done.
@@ -1408,8 +1408,8 @@ class Widget(DOMNode):
self._container_size = container_size
if self.is_scrollable:
self._scroll_update(virtual_size)
- self.refresh(layout=True)
- self.scroll_to(self.scroll_x, self.scroll_y)
+ # self.refresh(layout=True)
+ # self.scroll_to(self.scroll_x, self.scroll_y)
else:
self.refresh()