mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
* Add regression tests for #3053 * Traverse invisible containers when computing focus chain. At the moment, we were completely bypassing invisible containers which meant that their visible children wouldn't be included in the focus chain. * Make note of removed property. * Add regression test for #3071. * Fix #3071. * Fix regression test for #3053. * Optimize computation of focus chain. Computing the focus chain was relying on the property 'visible' of nodes which may traverse the DOM up to find the visibility of a given node. Instead, we cache the visibility of the nodes we traverse and keep them in a stack, saving some of that computation. Related issues: #3071 Related comments: https://github.com/Textualize/textual/pull/3070#issuecomment-1669683285 * Make test more robust. * Make test more robust. * Short-circuit disabled portions of DOM. If a node is disabled, we will not be focusable, nor will its children, so we can skip it altogether. Related review comment: https://github.com/Textualize/textual/pull/3070/files#r1300292492 * Simplify traversal. The traversal code could be simplified after reordering some lines of code. We also get rid of the visibility stack and instead keep everything in the same stack. Related comments: https://github.com/Textualize/textual/pull/3070#pullrequestreview-1587295458
312 lines
9.4 KiB
Python
312 lines
9.4 KiB
Python
import pytest
|
|
|
|
from textual.app import App
|
|
from textual.containers import Container
|
|
from textual.screen import Screen
|
|
from textual.widget import Widget
|
|
from textual.widgets import Button
|
|
|
|
|
|
class Focusable(Widget, can_focus=True):
|
|
pass
|
|
|
|
|
|
class NonFocusable(Widget, can_focus=False, can_focus_children=False):
|
|
pass
|
|
|
|
|
|
class ChildrenFocusableOnly(Widget, can_focus=False, can_focus_children=True):
|
|
pass
|
|
|
|
|
|
@pytest.fixture
|
|
def screen() -> Screen:
|
|
app = App()
|
|
app._set_active()
|
|
app.push_screen(Screen())
|
|
|
|
screen = app.screen
|
|
|
|
# The classes even/odd alternate along the focus chain.
|
|
# The classes in/out identify nested widgets.
|
|
screen._add_children(
|
|
Focusable(id="foo", classes="a"),
|
|
NonFocusable(id="bar"),
|
|
Focusable(Focusable(id="Paul", classes="c"), id="container1", classes="b"),
|
|
NonFocusable(Focusable(id="Jessica", classes="a"), id="container2"),
|
|
Focusable(id="baz", classes="b"),
|
|
ChildrenFocusableOnly(Focusable(id="child", classes="c")),
|
|
)
|
|
|
|
return screen
|
|
|
|
|
|
def test_focus_chain():
|
|
app = App()
|
|
app._set_active()
|
|
app.push_screen(Screen())
|
|
|
|
screen = app.screen
|
|
|
|
# Check empty focus chain
|
|
assert not screen.focus_chain
|
|
|
|
app.screen._add_children(
|
|
Focusable(id="foo"),
|
|
NonFocusable(id="bar"),
|
|
Focusable(Focusable(id="Paul"), id="container1"),
|
|
NonFocusable(Focusable(id="Jessica"), id="container2"),
|
|
Focusable(id="baz"),
|
|
ChildrenFocusableOnly(Focusable(id="child")),
|
|
)
|
|
|
|
focus_chain = [widget.id for widget in screen.focus_chain]
|
|
assert focus_chain == ["foo", "container1", "Paul", "baz", "child"]
|
|
|
|
|
|
def test_focus_next_and_previous(screen: Screen):
|
|
assert screen.focus_next().id == "foo"
|
|
assert screen.focus_next().id == "container1"
|
|
assert screen.focus_next().id == "Paul"
|
|
assert screen.focus_next().id == "baz"
|
|
assert screen.focus_next().id == "child"
|
|
|
|
assert screen.focus_previous().id == "baz"
|
|
assert screen.focus_previous().id == "Paul"
|
|
assert screen.focus_previous().id == "container1"
|
|
assert screen.focus_previous().id == "foo"
|
|
|
|
|
|
def test_focus_next_wrap_around(screen: Screen):
|
|
"""Ensure focusing the next widget wraps around the focus chain."""
|
|
screen.set_focus(screen.query_one("#child"))
|
|
assert screen.focused.id == "child"
|
|
|
|
assert screen.focus_next().id == "foo"
|
|
|
|
|
|
def test_focus_previous_wrap_around(screen: Screen):
|
|
"""Ensure focusing the previous widget wraps around the focus chain."""
|
|
screen.set_focus(screen.query_one("#foo"))
|
|
assert screen.focused.id == "foo"
|
|
|
|
assert screen.focus_previous().id == "child"
|
|
|
|
|
|
def test_wrap_around_selector(screen: Screen):
|
|
"""Ensure moving focus in both directions wraps around the focus chain."""
|
|
screen.set_focus(screen.query_one("#foo"))
|
|
assert screen.focused.id == "foo"
|
|
|
|
assert screen.focus_previous("#Paul").id == "Paul"
|
|
assert screen.focus_next("#foo").id == "foo"
|
|
|
|
|
|
def test_no_focus_empty_selector(screen: Screen):
|
|
"""Ensure focus is cleared when selector matches nothing."""
|
|
assert screen.focus_next("#bananas") is None
|
|
assert screen.focus_previous("#bananas") is None
|
|
|
|
screen.set_focus(screen.query_one("#foo"))
|
|
assert screen.focused is not None
|
|
assert screen.focus_next("bananas") is None
|
|
assert screen.focused is None
|
|
|
|
screen.set_focus(screen.query_one("#foo"))
|
|
assert screen.focused is not None
|
|
assert screen.focus_previous("bananas") is None
|
|
assert screen.focused is None
|
|
|
|
|
|
def test_focus_next_and_previous_with_type_selector(screen: Screen):
|
|
"""Move focus with a selector that matches the currently focused node."""
|
|
screen.set_focus(screen.query_one("#Paul"))
|
|
assert screen.focused.id == "Paul"
|
|
|
|
assert screen.focus_next(Focusable).id == "baz"
|
|
assert screen.focus_next(Focusable).id == "child"
|
|
|
|
assert screen.focus_previous(Focusable).id == "baz"
|
|
assert screen.focus_previous(Focusable).id == "Paul"
|
|
assert screen.focus_previous(Focusable).id == "container1"
|
|
assert screen.focus_previous(Focusable).id == "foo"
|
|
|
|
|
|
def test_focus_next_and_previous_with_str_selector(screen: Screen):
|
|
"""Move focus with a selector that matches the currently focused node."""
|
|
screen.set_focus(screen.query_one("#foo"))
|
|
assert screen.focused.id == "foo"
|
|
|
|
assert screen.focus_next(".a").id == "foo"
|
|
assert screen.focus_next(".c").id == "Paul"
|
|
assert screen.focus_next(".c").id == "child"
|
|
|
|
assert screen.focus_previous(".c").id == "Paul"
|
|
assert screen.focus_previous(".a").id == "foo"
|
|
|
|
|
|
def test_focus_next_and_previous_with_type_selector_without_self():
|
|
"""Test moving the focus with a selector that does not match the currently focused node."""
|
|
app = App()
|
|
app._set_active()
|
|
app.push_screen(Screen())
|
|
|
|
screen = app.screen
|
|
|
|
from textual.containers import Horizontal, VerticalScroll
|
|
from textual.widgets import Button, Input, Switch
|
|
|
|
screen._add_children(
|
|
VerticalScroll(
|
|
Horizontal(
|
|
Input(id="w3"),
|
|
Switch(id="w4"),
|
|
Input(id="w5"),
|
|
Button(id="w6"),
|
|
Switch(id="w7"),
|
|
id="w2",
|
|
),
|
|
Horizontal(
|
|
Button(id="w9"),
|
|
Switch(id="w10"),
|
|
Button(id="w11"),
|
|
Input(id="w12"),
|
|
Input(id="w13"),
|
|
id="w8",
|
|
),
|
|
id="w1",
|
|
)
|
|
)
|
|
|
|
screen.set_focus(screen.query_one("#w3"))
|
|
assert screen.focused.id == "w3"
|
|
|
|
assert screen.focus_next(Button).id == "w6"
|
|
assert screen.focus_next(Switch).id == "w7"
|
|
assert screen.focus_next(Input).id == "w12"
|
|
|
|
assert screen.focus_previous(Button).id == "w11"
|
|
assert screen.focus_previous(Switch).id == "w10"
|
|
assert screen.focus_previous(Button).id == "w9"
|
|
assert screen.focus_previous(Input).id == "w5"
|
|
|
|
|
|
def test_focus_next_and_previous_with_str_selector_without_self(screen: Screen):
|
|
"""Test moving the focus with a selector that does not match the currently focused node."""
|
|
screen.set_focus(screen.query_one("#foo"))
|
|
assert screen.focused.id == "foo"
|
|
|
|
assert screen.focus_next(".c").id == "Paul"
|
|
assert screen.focus_next(".b").id == "baz"
|
|
assert screen.focus_next(".c").id == "child"
|
|
|
|
assert screen.focus_previous(".a").id == "foo"
|
|
assert screen.focus_previous(".a").id == "foo"
|
|
assert screen.focus_previous(".b").id == "baz"
|
|
|
|
|
|
async def test_focus_does_not_move_to_invisible_widgets():
|
|
"""Make sure invisible widgets don't get focused by accident.
|
|
|
|
This is kind of a regression test for https://github.com/Textualize/textual/issues/3053,
|
|
but not really.
|
|
"""
|
|
|
|
class MyApp(App):
|
|
CSS = "#inv { visibility: hidden; }"
|
|
|
|
def compose(self):
|
|
yield Button("one", id="one")
|
|
yield Button("two", id="inv")
|
|
yield Button("three", id="three")
|
|
|
|
app = MyApp()
|
|
async with app.run_test():
|
|
assert app.focused.id == "one"
|
|
assert app.screen.focus_next().id == "three"
|
|
|
|
|
|
async def test_focus_moves_to_visible_widgets_inside_invisible_containers():
|
|
"""Regression test for https://github.com/Textualize/textual/issues/3053."""
|
|
|
|
class MyApp(App):
|
|
CSS = """
|
|
#inv { visibility: hidden; }
|
|
#three { visibility: visible; }
|
|
"""
|
|
|
|
def compose(self):
|
|
yield Button(id="one")
|
|
with Container(id="inv"):
|
|
yield Button(id="three")
|
|
|
|
app = MyApp()
|
|
async with app.run_test():
|
|
assert app.focused.id == "one"
|
|
assert app.screen.focus_next().id == "three"
|
|
|
|
|
|
async def test_focus_chain_handles_inherited_visibility():
|
|
"""Regression test for https://github.com/Textualize/textual/issues/3053
|
|
|
|
This is more or less a test for the interactions between #3053 and #3071.
|
|
We want to make sure that the focus chain is computed correctly when going through
|
|
a DOM with containers with all sorts of visibilities set.
|
|
"""
|
|
|
|
class W(Widget):
|
|
can_focus = True
|
|
|
|
w1 = W(id="one")
|
|
c2 = Container(id="two")
|
|
w3 = W(id="three")
|
|
c4 = Container(id="four")
|
|
w5 = W(id="five")
|
|
c6 = Container(id="six")
|
|
w7 = W(id="seven")
|
|
c8 = Container(id="eight")
|
|
w9 = W(id="nine")
|
|
w10 = W(id="ten")
|
|
w11 = W(id="eleven")
|
|
w12 = W(id="twelve")
|
|
w13 = W(id="thirteen")
|
|
|
|
class InheritedVisibilityApp(App[None]):
|
|
CSS = """
|
|
#four, #eight, #ten {
|
|
visibility: visible;
|
|
}
|
|
|
|
#six, #thirteen {
|
|
visibility: hidden;
|
|
}
|
|
"""
|
|
|
|
def compose(self):
|
|
yield w1 # visible, inherited
|
|
with c2: # visible, inherited
|
|
yield w3 # visible, inherited
|
|
with c4: # visible, set
|
|
yield w5 # visible, inherited
|
|
with c6: # hidden, set
|
|
yield w7 # hidden, inherited
|
|
with c8: # visible, set
|
|
yield w9 # visible, inherited
|
|
yield w10 # visible, set
|
|
yield w11 # visible, inherited
|
|
yield w12 # visible, inherited
|
|
yield w13 # invisible, set
|
|
|
|
app = InheritedVisibilityApp()
|
|
async with app.run_test():
|
|
focus_chain = app.screen.focus_chain
|
|
assert focus_chain == [
|
|
w1,
|
|
w3,
|
|
w5,
|
|
w9,
|
|
w10,
|
|
w11,
|
|
w12,
|
|
]
|