mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Widget collapsible (#2989)
* Collapsible container widget. * Expose collapsible widget. * Add collapsible container example * Rename member variables as label and apply formatting * Apply hover effect * Apply formatting * Add collapsible construction example with children. * Wrap contents within Container and move _collapsed flag to Collapsible class from Summary for easier access. * Add collapsible example that is expanded by default. * Update collapsed property to be reactive * Add footer to collapse and expand all with bound keys. * Expose summary property of Collapsible * Assign ids of ollapsed, expanded label instead of classes * Add unit tests of Collapsible * Rename class Summary to Title * Rename variables of expanded/collapsed symbols and add it to arguments.. * Add documentation for Collapsible * Update symbol ids of Collapsible title * Update src/textual/widgets/_collapsible.py Correct import path Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Sort module names in alphabetical order * Clarify that collapsible is non-focusable in documentation. * Add version hint * Fix documentation of Collapsible. * Add snapshot test for collapsible widget * Stop on click event from Collapsible. * Handle Title.Toggle event to prevent event in Contents from propagating to the children or parents Collapsible widgets. * Update Collapsible default css to have 1 fraction of width instead of 100% * Update Collapsible custom symbol snapshot * Add Collapsible custom symbol snapshot as an example * Update docs/widgets/collapsible.md Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Update src/textual/widgets/_collapsible.py Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Fix typo in Collapsible docs * Rework collapsible documentation. --------- Co-authored-by: Sunyoung Yoo <luysunyoung@aifactory.page> Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com>
This commit is contained in:
47
docs/examples/widgets/collapsible.py
Normal file
47
docs/examples/widgets/collapsible.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Collapsible, Footer, Label, Markdown
|
||||||
|
|
||||||
|
LETO = """
|
||||||
|
# Duke Leto I Atreides
|
||||||
|
|
||||||
|
Head of House Atreides.
|
||||||
|
"""
|
||||||
|
|
||||||
|
JESSICA = """
|
||||||
|
# Lady Jessica
|
||||||
|
|
||||||
|
Bene Gesserit and concubine of Leto, and mother of Paul and Alia.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PAUL = """
|
||||||
|
# Paul Atreides
|
||||||
|
|
||||||
|
Son of Leto and Jessica.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
"""An example of colllapsible container."""
|
||||||
|
|
||||||
|
BINDINGS = [
|
||||||
|
("c", "collapse_or_expand(True)", "Collapse All"),
|
||||||
|
("e", "collapse_or_expand(False)", "Expand All"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose app with collapsible containers."""
|
||||||
|
yield Footer()
|
||||||
|
with Collapsible(collapsed=False, title="Leto"):
|
||||||
|
yield Label(LETO)
|
||||||
|
yield Collapsible(Markdown(JESSICA), collapsed=False, title="Jessica")
|
||||||
|
with Collapsible(collapsed=True, title="Paul"):
|
||||||
|
yield Markdown(PAUL)
|
||||||
|
|
||||||
|
def action_collapse_or_expand(self, collapse: bool) -> None:
|
||||||
|
for child in self.walk_children(Collapsible):
|
||||||
|
child.collapsed = collapse
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = CollapsibleApp()
|
||||||
|
app.run()
|
||||||
25
docs/examples/widgets/collapsible_custom_symbol.py
Normal file
25
docs/examples/widgets/collapsible_custom_symbol.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Horizontal
|
||||||
|
from textual.widgets import Collapsible, Label
|
||||||
|
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Horizontal():
|
||||||
|
with Collapsible(
|
||||||
|
collapsed_symbol=">>>",
|
||||||
|
expanded_symbol="v",
|
||||||
|
):
|
||||||
|
yield Label("Hello, world.")
|
||||||
|
|
||||||
|
with Collapsible(
|
||||||
|
collapsed_symbol=">>>",
|
||||||
|
expanded_symbol="v",
|
||||||
|
collapsed=False,
|
||||||
|
):
|
||||||
|
yield Label("Hello, world.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = CollapsibleApp()
|
||||||
|
app.run()
|
||||||
14
docs/examples/widgets/collapsible_nested.py
Normal file
14
docs/examples/widgets/collapsible_nested.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Collapsible, Label
|
||||||
|
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Collapsible(collapsed=False):
|
||||||
|
with Collapsible():
|
||||||
|
yield Label("Hello, world.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = CollapsibleApp()
|
||||||
|
app.run()
|
||||||
153
docs/widgets/collapsible.md
Normal file
153
docs/widgets/collapsible.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Collapsible
|
||||||
|
|
||||||
|
!!! tip "Added in version 0.36"
|
||||||
|
|
||||||
|
Widget that wraps its contents in a collapsible container.
|
||||||
|
|
||||||
|
- [ ] Focusable
|
||||||
|
- [x] Container
|
||||||
|
|
||||||
|
|
||||||
|
## Composing
|
||||||
|
|
||||||
|
There are two ways to wrap other widgets.
|
||||||
|
You can pass them as positional arguments to the [Collapsible][textual.widgets.Collapsible] constructor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible(Label("Hello, world."))
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can compose other widgets under the context manager:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Collapsible():
|
||||||
|
yield Label("Hello, world.")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Title
|
||||||
|
|
||||||
|
The default title "Toggle" of the `Collapsible` widget can be customized by specifying the parameter `title` of the constructor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Collapsible(title="An interesting story."):
|
||||||
|
yield Label("Interesting but verbose story.")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initial State
|
||||||
|
|
||||||
|
The initial state of the `Collapsible` widget can be customized via the parameter `collapsed` of the constructor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Collapsible(title="Contents 1", collapsed=False):
|
||||||
|
yield Label("Hello, world.")
|
||||||
|
|
||||||
|
with Collapsible(title="Contents 2", collapsed=True): # Default.
|
||||||
|
yield Label("Hello, world.")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Collapse/Expand Symbols
|
||||||
|
|
||||||
|
The symbols `►` and `▼` of the `Collapsible` widget can be customized by specifying the parameters `collapsed_symbol` and `expanded_symbol`, respectively, of the `Collapsible` constructor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Collapsible(collapsed_symbol=">>>", expanded_symbol="v"):
|
||||||
|
yield Label("Hello, world.")
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="tests/snapshot_tests/snapshot_apps/collapsible_custom_symbol.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "collapsible_custom_symbol.py"
|
||||||
|
|
||||||
|
```python
|
||||||
|
--8<-- "tests/snapshot_tests/snapshot_apps/collapsible_custom_symbol.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic example
|
||||||
|
|
||||||
|
The following example contains three `Collapsible`s in different states.
|
||||||
|
|
||||||
|
=== "All expanded"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/widgets/collapsible.py press="e"}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "All collapsed"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/widgets/collapsible.py press="c"}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Mixed"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/widgets/collapsible.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "collapsible.py"
|
||||||
|
|
||||||
|
```python
|
||||||
|
--8<-- "docs/examples/widgets/collapsible.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting Initial State
|
||||||
|
|
||||||
|
The example below shows nested `Collapsible` widgets and how to set their initial state.
|
||||||
|
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="tests/snapshot_tests/snapshot_apps/collapsible_nested.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "collapsible_nested.py"
|
||||||
|
|
||||||
|
```python hl_lines="7"
|
||||||
|
--8<-- "tests/snapshot_tests/snapshot_apps/collapsible_nested.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Symbols
|
||||||
|
|
||||||
|
The app below shows `Collapsible` widgets with custom expand/collapse symbols.
|
||||||
|
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="tests/snapshot_tests/snapshot_apps/collapsible_custom_symbol.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "collapsible_custom_symbol.py"
|
||||||
|
|
||||||
|
```python
|
||||||
|
--8<-- "tests/snapshot_tests/snapshot_apps/collapsible_custom_symbol.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reactive attributes
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| ----------- | ------ | ------- | -------------------------------------------------------------- |
|
||||||
|
| `collapsed` | `bool` | `True` | Controls the collapsed/expanded state of the widget. |
|
||||||
|
|
||||||
|
## Messages
|
||||||
|
|
||||||
|
- [Collapsible.Title.Toggle][textual.widgets.Collapsible.Title.Toggle]
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## See also
|
||||||
|
|
||||||
|
TODO: Add Accordion widgets later
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
::: textual.widgets.Collapsible
|
||||||
|
options:
|
||||||
|
heading_level: 2
|
||||||
@@ -12,6 +12,7 @@ if typing.TYPE_CHECKING:
|
|||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
from ._button import Button
|
from ._button import Button
|
||||||
from ._checkbox import Checkbox
|
from ._checkbox import Checkbox
|
||||||
|
from ._collapsible import Collapsible
|
||||||
from ._content_switcher import ContentSwitcher
|
from ._content_switcher import ContentSwitcher
|
||||||
from ._data_table import DataTable
|
from ._data_table import DataTable
|
||||||
from ._digits import Digits
|
from ._digits import Digits
|
||||||
@@ -44,10 +45,10 @@ if typing.TYPE_CHECKING:
|
|||||||
from ._tree import Tree
|
from ._tree import Tree
|
||||||
from ._welcome import Welcome
|
from ._welcome import Welcome
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Button",
|
"Button",
|
||||||
"Checkbox",
|
"Checkbox",
|
||||||
|
"Collapsible",
|
||||||
"ContentSwitcher",
|
"ContentSwitcher",
|
||||||
"DataTable",
|
"DataTable",
|
||||||
"Digits",
|
"Digits",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# This stub file must re-export every classes exposed in the __init__.py's `__all__` list:
|
# This stub file must re-export every classes exposed in the __init__.py's `__all__` list:
|
||||||
from ._button import Button as Button
|
from ._button import Button as Button
|
||||||
from ._checkbox import Checkbox as Checkbox
|
from ._checkbox import Checkbox as Checkbox
|
||||||
|
from ._collapsible import Collapsible as Collapsible
|
||||||
from ._content_switcher import ContentSwitcher as ContentSwitcher
|
from ._content_switcher import ContentSwitcher as ContentSwitcher
|
||||||
from ._data_table import DataTable as DataTable
|
from ._data_table import DataTable as DataTable
|
||||||
from ._digits import Digits as Digits
|
from ._digits import Digits as Digits
|
||||||
|
|||||||
156
src/textual/widgets/_collapsible.py
Normal file
156
src/textual/widgets/_collapsible.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
from .. import events
|
||||||
|
from ..app import ComposeResult
|
||||||
|
from ..containers import Container, Horizontal
|
||||||
|
from ..message import Message
|
||||||
|
from ..reactive import reactive
|
||||||
|
from ..widget import Widget
|
||||||
|
from ..widgets import Label
|
||||||
|
|
||||||
|
__all__ = ["Collapsible"]
|
||||||
|
|
||||||
|
|
||||||
|
class Collapsible(Widget):
|
||||||
|
"""A collapsible container."""
|
||||||
|
|
||||||
|
collapsed = reactive(True)
|
||||||
|
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
Collapsible {
|
||||||
|
width: 1fr;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Title(Horizontal):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
Title {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
Title:hover {
|
||||||
|
background: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
Title .label {
|
||||||
|
padding: 0 0 0 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Title #collapsed-symbol {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
Title.-collapsed #expanded-symbol {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
Title.-collapsed #collapsed-symbol {
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
label: str,
|
||||||
|
collapsed_symbol: str,
|
||||||
|
expanded_symbol: str,
|
||||||
|
name: str | None = None,
|
||||||
|
id: str | None = None,
|
||||||
|
classes: str | None = None,
|
||||||
|
disabled: bool = False,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
|
||||||
|
self.collapsed_symbol = collapsed_symbol
|
||||||
|
self.expanded_symbol = expanded_symbol
|
||||||
|
self.label = label
|
||||||
|
|
||||||
|
class Toggle(Message):
|
||||||
|
"""Request toggle."""
|
||||||
|
|
||||||
|
async def _on_click(self, event: events.Click) -> None:
|
||||||
|
"""Inform ancestor we want to toggle."""
|
||||||
|
event.stop()
|
||||||
|
self.post_message(self.Toggle())
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose right/down arrow and label."""
|
||||||
|
yield Label(self.expanded_symbol, classes="label", id="expanded-symbol")
|
||||||
|
yield Label(self.collapsed_symbol, classes="label", id="collapsed-symbol")
|
||||||
|
yield Label(self.label, classes="label")
|
||||||
|
|
||||||
|
class Contents(Container):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
Contents {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 0 0 0 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
Contents.-collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*children: Widget,
|
||||||
|
title: str = "Toggle",
|
||||||
|
collapsed: bool = True,
|
||||||
|
collapsed_symbol: str = "►",
|
||||||
|
expanded_symbol: str = "▼",
|
||||||
|
name: str | None = None,
|
||||||
|
id: str | None = None,
|
||||||
|
classes: str | None = None,
|
||||||
|
disabled: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a Collapsible widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*children: Contents that will be collapsed/expanded.
|
||||||
|
title: Title of the collapsed/expanded contents.
|
||||||
|
collapsed: Default status of the contents.
|
||||||
|
collapsed_symbol: Collapsed symbol before the title.
|
||||||
|
expanded_symbol: Expanded symbol before the title.
|
||||||
|
name: The name of the collapsible.
|
||||||
|
id: The ID of the collapsible in the DOM.
|
||||||
|
classes: The CSS classes of the collapsible.
|
||||||
|
disabled: Whether the collapsible is disabled or not.
|
||||||
|
"""
|
||||||
|
self._title = self.Title(
|
||||||
|
label=title,
|
||||||
|
collapsed_symbol=collapsed_symbol,
|
||||||
|
expanded_symbol=expanded_symbol,
|
||||||
|
)
|
||||||
|
self._contents_list: list[Widget] = list(children)
|
||||||
|
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
|
||||||
|
self.collapsed = collapsed
|
||||||
|
|
||||||
|
def _on_title_toggle(self, event: Title.Toggle) -> None:
|
||||||
|
event.stop()
|
||||||
|
self.collapsed = not self.collapsed
|
||||||
|
|
||||||
|
def watch_collapsed(self) -> None:
|
||||||
|
for child in self._nodes:
|
||||||
|
child.set_class(self.collapsed, "-collapsed")
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield from (
|
||||||
|
child.set_class(self.collapsed, "-collapsed")
|
||||||
|
for child in (
|
||||||
|
self._title,
|
||||||
|
self.Contents(*self._contents_list),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def compose_add_child(self, widget: Widget) -> None:
|
||||||
|
"""When using the context manager compose syntax, we want to attach nodes to the contents.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget: A Widget to add.
|
||||||
|
"""
|
||||||
|
self._contents_list.append(widget)
|
||||||
File diff suppressed because one or more lines are too long
@@ -330,6 +330,26 @@ def test_sparkline_component_classes_colors(snap_compare):
|
|||||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "sparkline_colors.py")
|
assert snap_compare(WIDGET_EXAMPLES_DIR / "sparkline_colors.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_collapsible_render(snap_compare):
|
||||||
|
assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_collapsible_collapsed(snap_compare):
|
||||||
|
assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible.py", press=["c"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_collapsible_expanded(snap_compare):
|
||||||
|
assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible.py", press=["e"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_collapsible_nested(snap_compare):
|
||||||
|
assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible_nested.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_collapsible_custom_symbol(snap_compare):
|
||||||
|
assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible_custom_symbol.py")
|
||||||
|
|
||||||
|
|
||||||
# --- CSS properties ---
|
# --- CSS properties ---
|
||||||
# We have a canonical example for each CSS property that is shown in their docs.
|
# We have a canonical example for each CSS property that is shown in their docs.
|
||||||
# If any of these change, something has likely broken, so snapshot each of them.
|
# If any of these change, something has likely broken, so snapshot each of them.
|
||||||
|
|||||||
163
tests/test_collapsible.py
Normal file
163
tests/test_collapsible.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Collapsible, Label
|
||||||
|
|
||||||
|
COLLAPSED_CLASS = "-collapsed"
|
||||||
|
|
||||||
|
|
||||||
|
def get_title(collapsible: Collapsible) -> Collapsible.Title:
|
||||||
|
return collapsible.get_child_by_type(Collapsible.Title)
|
||||||
|
|
||||||
|
|
||||||
|
def get_contents(collapsible: Collapsible) -> Collapsible.Contents:
|
||||||
|
return collapsible.get_child_by_type(Collapsible.Contents)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_collapsible():
|
||||||
|
"""It should be possible to access title and collapsed."""
|
||||||
|
collapsible = Collapsible(title="Pilot", collapsed=True)
|
||||||
|
assert collapsible._title.label == "Pilot"
|
||||||
|
assert collapsible.collapsed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_compose_default_collapsible():
|
||||||
|
"""Test default settings of Collapsible with 1 widget in contents."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible(Label("Some Contents"))
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
collapsible = pilot.app.query_one(Collapsible)
|
||||||
|
assert get_title(collapsible).label == "Toggle"
|
||||||
|
assert get_title(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
|
assert len(get_contents(collapsible).children) == 1
|
||||||
|
assert get_contents(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_compose_empty_collapsible():
|
||||||
|
"""It should be possible to create an empty Collapsible."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible()
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
collapsible = pilot.app.query_one(Collapsible)
|
||||||
|
assert len(get_contents(collapsible).children) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_compose_nested_collapsible():
|
||||||
|
"""Children Collapsibles are independent from parents Collapsibles."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Collapsible(Label("Outer"), id="outer", collapsed=False):
|
||||||
|
yield Collapsible(Label("Inner"), id="inner", collapsed=False)
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
outer: Collapsible = pilot.app.get_child_by_id("outer")
|
||||||
|
inner: Collapsible = get_contents(outer).get_child_by_id("inner")
|
||||||
|
outer.collapsed = True
|
||||||
|
assert not inner.collapsed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_compose_expanded_collapsible():
|
||||||
|
"""It should be possible to create a Collapsible with expanded contents."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible(collapsed=False)
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
collapsible = pilot.app.query_one(Collapsible)
|
||||||
|
assert not get_title(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
|
assert not get_contents(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_collapsible_collapsed_title_label():
|
||||||
|
"""Collapsed title label should be displayed."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible(Label("Some Contents"), collapsed=True)
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
title = get_title(pilot.app.query_one(Collapsible))
|
||||||
|
assert not title.get_child_by_id("expanded-symbol").display
|
||||||
|
assert title.get_child_by_id("collapsed-symbol").display
|
||||||
|
|
||||||
|
|
||||||
|
async def test_collapsible_expanded_title_label():
|
||||||
|
"""Expanded title label should be displayed."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible(Label("Some Contents"), collapsed=False)
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
title = get_title(pilot.app.query_one(Collapsible))
|
||||||
|
assert title.get_child_by_id("expanded-symbol").display
|
||||||
|
assert not title.get_child_by_id("collapsed-symbol").display
|
||||||
|
|
||||||
|
|
||||||
|
async def test_collapsible_collapsed_contents_display_false():
|
||||||
|
"""Test default settings of Collapsible with 1 widget in contents."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible(Label("Some Contents"), collapsed=True)
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
collapsible = pilot.app.query_one(Collapsible)
|
||||||
|
assert not get_contents(collapsible).display
|
||||||
|
|
||||||
|
|
||||||
|
async def test_collapsible_expanded_contents_display_true():
|
||||||
|
"""Test default settings of Collapsible with 1 widget in contents."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible(Label("Some Contents"), collapsed=False)
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
collapsible = pilot.app.query_one(Collapsible)
|
||||||
|
assert get_contents(collapsible).display
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reactive_collapsed():
|
||||||
|
"""Updating ``collapsed`` should change classes of children."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible(collapsed=False)
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
collapsible = pilot.app.query_one(Collapsible)
|
||||||
|
assert not get_title(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
|
collapsible.collapsed = True
|
||||||
|
assert get_contents(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
|
collapsible.collapsed = False
|
||||||
|
assert not get_title(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_toggle_title():
|
||||||
|
"""Clicking title should update ``collapsed``."""
|
||||||
|
|
||||||
|
class CollapsibleApp(App[None]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Collapsible(collapsed=False)
|
||||||
|
|
||||||
|
async with CollapsibleApp().run_test() as pilot:
|
||||||
|
collapsible = pilot.app.query_one(Collapsible)
|
||||||
|
assert not collapsible.collapsed
|
||||||
|
assert not get_title(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
|
|
||||||
|
await pilot.click(Collapsible.Title)
|
||||||
|
assert collapsible.collapsed
|
||||||
|
assert get_contents(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
|
|
||||||
|
await pilot.click(Collapsible.Title)
|
||||||
|
assert not collapsible.collapsed
|
||||||
|
assert not get_title(collapsible).has_class(COLLAPSED_CLASS)
|
||||||
Reference in New Issue
Block a user